@vellumai/assistant 0.11.3 → 0.11.4-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 (341) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/docs/architecture/memory.md +11 -0
  3. package/docs/architecture/turn-actor.md +70 -0
  4. package/docs/flux-turn-detection-spike.md +243 -0
  5. package/docs/stt-provider-onboarding.md +3 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  8. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  10. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  11. package/openapi.yaml +139 -37
  12. package/package.json +1 -1
  13. package/scripts/voice-ttft-spike.ts +3 -3
  14. package/src/__tests__/app-compiler.test.ts +38 -3
  15. package/src/__tests__/attachments-store.test.ts +21 -12
  16. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  17. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  18. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  19. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  20. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  21. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  22. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  23. package/src/__tests__/config-schema.test.ts +25 -10
  24. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  26. package/src/__tests__/conversation-agent-loop.test.ts +28 -20
  27. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  28. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  29. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  30. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  31. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  32. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  33. package/src/__tests__/conversation-queue.test.ts +177 -4
  34. package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
  35. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  36. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  37. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  38. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  39. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  40. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  41. package/src/__tests__/edit-propagation.test.ts +0 -7
  42. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  43. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  44. package/src/__tests__/injector-chain.test.ts +63 -41
  45. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  46. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  47. package/src/__tests__/llm-schema.test.ts +5 -2
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  49. package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
  50. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  51. package/src/__tests__/mtime-cache.test.ts +1 -1
  52. package/src/__tests__/non-member-access-request.test.ts +0 -20
  53. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
  55. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  56. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  57. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  58. package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
  59. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  60. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  61. package/src/__tests__/skills.test.ts +32 -0
  62. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  63. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  64. package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
  65. package/src/__tests__/turn-events-store.test.ts +43 -0
  66. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  67. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  68. package/src/__tests__/user-plugin-loader.test.ts +1 -1
  69. package/src/__tests__/visible-app-context.test.ts +16 -9
  70. package/src/__tests__/voice-config-update.test.ts +40 -0
  71. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  72. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  73. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  74. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  75. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  76. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  77. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  78. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  79. package/src/acp/__tests__/auth-required.test.ts +161 -0
  80. package/src/acp/acp-claude-oauth.ts +19 -2
  81. package/src/acp/agent-process.test.ts +100 -0
  82. package/src/acp/agent-process.ts +29 -26
  83. package/src/acp/auth-required.ts +102 -0
  84. package/src/acp/session-manager.test.ts +119 -0
  85. package/src/acp/session-manager.ts +68 -2
  86. package/src/api/events/acp-auth-required.ts +55 -0
  87. package/src/api/index.ts +7 -0
  88. package/src/api/surfaces.ts +7 -3
  89. package/src/apps/app-store.ts +3 -0
  90. package/src/bundler/package-resolver.ts +2 -30
  91. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  92. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  93. package/src/calls/call-controller.ts +19 -3
  94. package/src/calls/call-setup-flow.ts +0 -1
  95. package/src/calls/media-stream-stt-session.ts +15 -0
  96. package/src/calls/voice-session-bridge.ts +71 -16
  97. package/src/calls/voice-triage-escalate.ts +104 -2
  98. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  99. package/src/channels/config.ts +13 -0
  100. package/src/channels/plugin-channel-declarations.ts +108 -0
  101. package/src/channels/types.ts +30 -0
  102. package/src/cli/AGENTS.md +5 -2
  103. package/src/cli/commands/credentials.help.ts +2 -2
  104. package/src/cli/commands/inference-providers.ts +1 -1
  105. package/src/cli/commands/mcp.help.ts +13 -4
  106. package/src/cli/commands/mcp.ts +9 -0
  107. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  108. package/src/cli/commands/memory/index.help.ts +43 -1
  109. package/src/cli/commands/memory/memory-v3.ts +64 -0
  110. package/src/cli/commands/stt.help.ts +27 -2
  111. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  112. package/src/cli/lib/bundled-marketplace.json +13 -0
  113. package/src/cli/lib/upgrade-plugin.ts +42 -0
  114. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  115. package/src/config/__tests__/default-provider.test.ts +6 -1
  116. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  117. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  118. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  119. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  120. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  121. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  122. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  123. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  124. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  125. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  126. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  127. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  128. package/src/config/call-site-defaults.ts +4 -5
  129. package/src/config/default-profile-catalog.ts +83 -12
  130. package/src/config/default-profile-names.ts +4 -1
  131. package/src/config/default-provider-resolution.ts +4 -0
  132. package/src/config/llm-context-resolution.ts +11 -3
  133. package/src/config/llm-resolver.ts +28 -1
  134. package/src/config/profile-materialization.ts +70 -22
  135. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  136. package/src/config/schemas/call-site-catalog.ts +4 -4
  137. package/src/config/schemas/live-voice.ts +57 -23
  138. package/src/config/schemas/llm.ts +59 -32
  139. package/src/config/schemas/mcp.ts +23 -0
  140. package/src/config/schemas/plugin-updates.ts +6 -2
  141. package/src/config/schemas/stt.ts +1 -0
  142. package/src/context/outbound-sanitize.ts +96 -1
  143. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  144. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  145. package/src/daemon/conversation-agent-loop.ts +17 -6
  146. package/src/daemon/conversation-messaging.ts +5 -1
  147. package/src/daemon/conversation-notifiers.ts +9 -1
  148. package/src/daemon/conversation-process.ts +36 -6
  149. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  150. package/src/daemon/conversation-surfaces.ts +27 -5
  151. package/src/daemon/conversation-tool-setup.ts +1 -2
  152. package/src/daemon/conversation.ts +48 -0
  153. package/src/daemon/interactive-turn-sender.ts +59 -0
  154. package/src/daemon/mcp-reload-service.ts +36 -6
  155. package/src/daemon/process-message.ts +24 -24
  156. package/src/daemon/providers-setup.ts +6 -3
  157. package/src/daemon/trust-context-types.ts +29 -0
  158. package/src/daemon/wake-conversation-ops.ts +3 -2
  159. package/src/documents/document-store.ts +138 -5
  160. package/src/hooks/hook-loader.ts +3 -3
  161. package/src/hooks/registry.ts +50 -6
  162. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  163. package/src/inbound/oauth-callback-url.ts +61 -0
  164. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  165. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  166. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  167. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  168. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  169. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  170. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  171. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  172. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  173. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  174. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  175. package/src/live-voice/live-voice-archive.ts +2 -0
  176. package/src/live-voice/live-voice-metrics.ts +57 -32
  177. package/src/live-voice/live-voice-photo.ts +1 -2
  178. package/src/live-voice/live-voice-session.ts +535 -314
  179. package/src/live-voice/progress-narration.ts +277 -0
  180. package/src/live-voice/protocol.ts +21 -1
  181. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  182. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  183. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  184. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  185. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  186. package/src/mcp/client.ts +16 -11
  187. package/src/mcp/effective-config.ts +113 -0
  188. package/src/mcp/manager.ts +11 -6
  189. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  190. package/src/mcp/mcp-oauth-provider.ts +205 -240
  191. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  192. package/src/monitoring/plugin-auto-update.ts +128 -24
  193. package/src/notifications/signal.ts +1 -0
  194. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  195. package/src/permissions/confirmation-guardian-request.ts +2 -2
  196. package/src/permissions/question-guardian-request.test.ts +14 -6
  197. package/src/permissions/question-guardian-request.ts +1 -2
  198. package/src/persistence/attachments-store.ts +8 -1
  199. package/src/persistence/bookmark-crud.ts +3 -7
  200. package/src/persistence/conversation-attention-store.ts +16 -45
  201. package/src/persistence/conversation-crud.ts +33 -4
  202. package/src/persistence/conversation-lineage.ts +9 -0
  203. package/src/persistence/conversation-queries.ts +108 -41
  204. package/src/persistence/delivery-crud.ts +38 -29
  205. package/src/persistence/external-conversation-store.ts +32 -4
  206. package/src/persistence/llm-request-log-store.ts +4 -10
  207. package/src/persistence/llm-usage-store.ts +8 -3
  208. package/src/persistence/message-reads.test.ts +197 -0
  209. package/src/persistence/message-reads.ts +211 -0
  210. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  211. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  212. package/src/persistence/real-user-turn-filter.ts +27 -3
  213. package/src/persistence/steps.ts +9 -0
  214. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  215. package/src/plugin-api/conversation-turn.ts +168 -5
  216. package/src/plugin-api/index.ts +21 -5
  217. package/src/plugin-api/vision-support.test.ts +1 -1
  218. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  219. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  220. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  224. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  225. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  226. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  227. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  228. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  229. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  230. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  231. package/src/plugins/defaults/memory/indexer.ts +3 -1
  232. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  233. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  234. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  235. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  236. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  237. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  238. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  239. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  240. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  241. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  242. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  243. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  244. package/src/plugins/defaults/memory/worker.ts +6 -3
  245. package/src/plugins/external-plugin-loader.ts +47 -0
  246. package/src/plugins/mcp-servers.ts +361 -0
  247. package/src/plugins/mtime-cache.ts +23 -49
  248. package/src/plugins/worker-plugin-surface.ts +33 -0
  249. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  250. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  251. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  252. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  253. package/src/providers/call-site-routing.ts +30 -3
  254. package/src/providers/connection-resolution.ts +194 -11
  255. package/src/providers/inference/auth.ts +6 -0
  256. package/src/providers/inference/connection-availability.ts +24 -2
  257. package/src/providers/inference/connections.ts +2 -0
  258. package/src/providers/model-catalog.ts +3 -3
  259. package/src/providers/model-intents.ts +28 -8
  260. package/src/providers/openai/chat-completions-provider.ts +5 -6
  261. package/src/providers/openai/codex-models.ts +2 -1
  262. package/src/providers/provider-send-message.ts +32 -3
  263. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  264. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  265. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  266. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  267. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  268. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  269. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  270. package/src/providers/speech-to-text/resolve.ts +25 -2
  271. package/src/routes/worker.ts +17 -5
  272. package/src/runtime/access-request-helper.ts +9 -12
  273. package/src/runtime/agent-wake.ts +3 -3
  274. package/src/runtime/pre-first-message-gate.ts +4 -0
  275. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  276. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  277. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  278. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  279. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  280. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  281. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  282. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  283. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  284. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  285. package/src/runtime/routes/channel-route-shared.ts +0 -6
  286. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  287. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  288. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  289. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  290. package/src/runtime/routes/default-provider-routes.ts +15 -0
  291. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  292. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  294. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  295. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  296. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  297. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  298. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  299. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  300. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  301. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  302. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  303. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  304. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  305. package/src/runtime/routes/log-export-routes.ts +3 -0
  306. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  307. package/src/runtime/routes/plugins-routes.ts +21 -3
  308. package/src/runtime/routes/stt-routes.ts +31 -25
  309. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  310. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  311. package/src/runtime/routes/user-route-import.ts +108 -0
  312. package/src/schedule/worker.ts +6 -0
  313. package/src/security/oauth2.ts +6 -22
  314. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  315. package/src/stt/__tests__/types.test.ts +94 -0
  316. package/src/stt/daemon-batch-transcriber.ts +10 -0
  317. package/src/stt/stt-stream-session.ts +8 -4
  318. package/src/stt/types.ts +103 -0
  319. package/src/subagent/manager.ts +1 -3
  320. package/src/subagent/types.ts +7 -6
  321. package/src/tools/acp/spawn.test.ts +97 -0
  322. package/src/tools/acp/spawn.ts +32 -0
  323. package/src/tools/document/document-tool.ts +12 -3
  324. package/src/tools/registry.ts +2 -1
  325. package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
  326. package/src/tools/workflows/run-workflow.ts +1 -2
  327. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  328. package/src/tts/reasoning-tag-filter.ts +89 -0
  329. package/src/util/think-tag-stream.ts +95 -0
  330. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  331. package/src/workspace/custom-profile-ensure.ts +4 -24
  332. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  333. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  334. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  335. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  336. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  337. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  338. package/src/workspace/migrations/registry.ts +10 -0
  339. package/src/workspace/provider-commit-message-generator.ts +7 -5
  340. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  341. package/src/live-voice/front-decision.ts +0 -476
@@ -10,7 +10,10 @@ import type {
10
10
  import { APP_VERSION } from "../version.js";
11
11
  import { getDb } from "./db-connection.js";
12
12
  import { rawAll } from "./raw-query.js";
13
- import { realUserTurnContentFilter } from "./real-user-turn-filter.js";
13
+ import {
14
+ realUserTurnContentFilter,
15
+ realUserTurnContentFilterSql,
16
+ } from "./real-user-turn-filter.js";
14
17
  import {
15
18
  buildScheduleAttributionSubquery,
16
19
  buildScheduleRunWindowExists,
@@ -300,6 +303,9 @@ export function queryUnreportedUsageEvents(
300
303
  // branch from a turn far earlier than the fork's creation time, so
301
304
  // they anchor on the fork boundary message instead; child creation is
302
305
  // the fallback when the boundary message is absent.
306
+ // The turn-index subqueries below count user-role rows only. User rows are
307
+ // always written finalized (only assistant turns stream), so no completeness
308
+ // predicate is needed; do not copy this shape for assistant-row counts.
303
309
  const parentTurnCutoffSql = sql<number>`CASE
304
310
  WHEN ${conversations.parentConversationId} IS NOT NULL THEN ${conversations.createdAt}
305
311
  ELSE COALESCE(
@@ -879,8 +885,7 @@ export function getUsageGroupBreakdown(
879
885
  SELECT COUNT(*) FROM messages AS m2
880
886
  WHERE m2.conversation_id = e.conversation_id
881
887
  AND m2.role = 'user'
882
- AND m2.content NOT LIKE '%"type":"tool\\_result"%' ESCAPE '\\'
883
- AND m2.content NOT LIKE '%"type":"web\\_search\\_tool\\_result"%' ESCAPE '\\'
888
+ AND ${realUserTurnContentFilterSql("m2")}
884
889
  )
885
890
  END AS turn_count
886
891
  FROM llm_usage_events e
@@ -0,0 +1,197 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import { eq } from "drizzle-orm";
4
+
5
+ import { createConversation } from "./conversation-crud.js";
6
+ import { getDb } from "./db-connection.js";
7
+ import { initializeDb } from "./db-init.js";
8
+ import {
9
+ countMessagesByRoleForConversations,
10
+ existingMessageIds,
11
+ latestAssistantMessage,
12
+ latestAssistantMessageBefore,
13
+ latestUserMessageRawContent,
14
+ messageConversationId,
15
+ } from "./message-reads.js";
16
+ import { messages } from "./schema/index.js";
17
+
18
+ await initializeDb();
19
+
20
+ let seq = 0;
21
+ function insertRow(opts: {
22
+ conversationId: string;
23
+ role: string;
24
+ createdAt: number;
25
+ content?: string;
26
+ finalized?: 0 | 1;
27
+ }): string {
28
+ const id = `msg-${String(++seq).padStart(4, "0")}`;
29
+ getDb()
30
+ .insert(messages)
31
+ .values({
32
+ id,
33
+ conversationId: opts.conversationId,
34
+ role: opts.role,
35
+ content: opts.content ?? JSON.stringify([{ type: "text", text: "hi" }]),
36
+ createdAt: opts.createdAt,
37
+ metadata: null,
38
+ finalized: opts.finalized ?? 1,
39
+ })
40
+ .run();
41
+ return id;
42
+ }
43
+
44
+ function seedConversation(): string {
45
+ return createConversation("Message reads test").id;
46
+ }
47
+
48
+ describe("message-reads", () => {
49
+ beforeEach(() => {
50
+ const db = getDb();
51
+ db.run("DELETE FROM messages");
52
+ db.run("DELETE FROM conversations");
53
+ });
54
+
55
+ test("latestAssistantMessage returns the newest finalized assistant row", () => {
56
+ const conv = seedConversation();
57
+ insertRow({ conversationId: conv, role: "user", createdAt: 1_000 });
58
+ const older = insertRow({
59
+ conversationId: conv,
60
+ role: "assistant",
61
+ createdAt: 2_000,
62
+ });
63
+ const newer = insertRow({
64
+ conversationId: conv,
65
+ role: "assistant",
66
+ createdAt: 3_000,
67
+ });
68
+
69
+ expect(latestAssistantMessage(conv)?.id).toBe(newer);
70
+ expect(latestAssistantMessage(conv)?.id).not.toBe(older);
71
+ expect(latestAssistantMessage("missing-conv")).toBeNull();
72
+ });
73
+
74
+ test("latestAssistantMessage never anchors on a streaming row", () => {
75
+ const conv = seedConversation();
76
+ const finalizedReply = insertRow({
77
+ conversationId: conv,
78
+ role: "assistant",
79
+ createdAt: 1_000,
80
+ });
81
+ insertRow({
82
+ conversationId: conv,
83
+ role: "assistant",
84
+ createdAt: 2_000,
85
+ finalized: 0,
86
+ });
87
+
88
+ // The streaming row is newest but is not a reply yet; the watermark must
89
+ // sit on the completed one until the turn finishes.
90
+ expect(latestAssistantMessage(conv)?.id).toBe(finalizedReply);
91
+ });
92
+
93
+ test("a streaming row qualifies on its own once finalized", () => {
94
+ const conv = seedConversation();
95
+ insertRow({ conversationId: conv, role: "assistant", createdAt: 1_000 });
96
+ const streaming = insertRow({
97
+ conversationId: conv,
98
+ role: "assistant",
99
+ createdAt: 2_000,
100
+ finalized: 0,
101
+ });
102
+
103
+ getDb()
104
+ .update(messages)
105
+ .set({ finalized: 1 })
106
+ .where(eq(messages.id, streaming))
107
+ .run();
108
+
109
+ expect(latestAssistantMessage(conv)?.id).toBe(streaming);
110
+ });
111
+
112
+ test("latestAssistantMessageBefore is strict and skips streaming rows", () => {
113
+ const conv = seedConversation();
114
+ const first = insertRow({
115
+ conversationId: conv,
116
+ role: "assistant",
117
+ createdAt: 1_000,
118
+ });
119
+ insertRow({
120
+ conversationId: conv,
121
+ role: "assistant",
122
+ createdAt: 2_000,
123
+ finalized: 0,
124
+ });
125
+ insertRow({ conversationId: conv, role: "assistant", createdAt: 3_000 });
126
+
127
+ // Strictly before 3000: the streaming row at 2000 is passed over in
128
+ // favour of the completed row at 1000.
129
+ expect(latestAssistantMessageBefore(conv, 3_000)?.id).toBe(first);
130
+ // Strictly before 1000: nothing.
131
+ expect(latestAssistantMessageBefore(conv, 1_000)).toBeNull();
132
+ });
133
+
134
+ test("latestUserMessageRawContent resolves same-timestamp rows by insertion order", () => {
135
+ const conv = seedConversation();
136
+ insertRow({
137
+ conversationId: conv,
138
+ role: "user",
139
+ createdAt: 1_000,
140
+ content: "first",
141
+ });
142
+ insertRow({
143
+ conversationId: conv,
144
+ role: "user",
145
+ createdAt: 1_000,
146
+ content: "second",
147
+ });
148
+ insertRow({ conversationId: conv, role: "assistant", createdAt: 2_000 });
149
+
150
+ expect(latestUserMessageRawContent(conv)).toBe("second");
151
+ expect(latestUserMessageRawContent("missing-conv")).toBeNull();
152
+ });
153
+
154
+ test("countMessagesByRoleForConversations aggregates per conversation", () => {
155
+ const convA = seedConversation();
156
+ const convB = seedConversation();
157
+ insertRow({ conversationId: convA, role: "assistant", createdAt: 1_000 });
158
+ insertRow({ conversationId: convA, role: "assistant", createdAt: 2_000 });
159
+ insertRow({ conversationId: convA, role: "user", createdAt: 3_000 });
160
+ insertRow({ conversationId: convB, role: "assistant", createdAt: 4_000 });
161
+
162
+ const stats = countMessagesByRoleForConversations(
163
+ [convA, convB],
164
+ "assistant",
165
+ );
166
+ expect(stats.get(convA)).toEqual({ count: 2, lastAt: 2_000 });
167
+ expect(stats.get(convB)).toEqual({ count: 1, lastAt: 4_000 });
168
+ expect(countMessagesByRoleForConversations([], "assistant").size).toBe(0);
169
+ });
170
+
171
+ test("existingMessageIds returns only ids that exist", () => {
172
+ const conv = seedConversation();
173
+ const present = insertRow({
174
+ conversationId: conv,
175
+ role: "user",
176
+ createdAt: 1_000,
177
+ });
178
+
179
+ const existing = existingMessageIds([present, "msg-not-there"]);
180
+ expect(existing.has(present)).toBe(true);
181
+ expect(existing.has("msg-not-there")).toBe(false);
182
+ expect(existingMessageIds([]).size).toBe(0);
183
+ });
184
+
185
+ test("messageConversationId resolves ownership in any state", () => {
186
+ const conv = seedConversation();
187
+ const streaming = insertRow({
188
+ conversationId: conv,
189
+ role: "assistant",
190
+ createdAt: 1_000,
191
+ finalized: 0,
192
+ });
193
+
194
+ expect(messageConversationId(streaming)).toBe(conv);
195
+ expect(messageConversationId("msg-not-there")).toBeNull();
196
+ });
197
+ });
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Named read accessors for the `messages` table.
3
+ *
4
+ * Every row in `messages` is conversation content in some state of
5
+ * completeness: `finalized = 1` rows are immutable history, `finalized = 0`
6
+ * rows are still being written and their `content` may be a `{ ref }` into an
7
+ * in-flight delta file that disappears when the turn completes. Ad-hoc queries
8
+ * tend to encode a visibility decision without stating it; each accessor here
9
+ * states its decision in its name and contract, so a caller (and a reviewer)
10
+ * chooses between "completed history" and "any row" deliberately.
11
+ *
12
+ * Conversation-history reads (transcripts, lineage, pagination) stay with
13
+ * `getMessages` / `getMessagesAfter` / `getMessagesPaginated` in
14
+ * `conversation-crud.ts`, which also own fork-lineage resolution. This module
15
+ * is for the narrower point lookups and aggregates that other persistence
16
+ * modules need.
17
+ *
18
+ * Correlated subqueries embedded inside larger SQL (usage accounting's
19
+ * turn-index arithmetic, search candidate joins, `message_count` projections)
20
+ * are out of scope: extracting them would restructure their host queries. They
21
+ * carry their visibility decision as a comment at the site instead.
22
+ */
23
+
24
+ import { and, desc, eq, inArray, lt, sql } from "drizzle-orm";
25
+
26
+ import { type DrizzleDb, getDb } from "./db-connection.js";
27
+ import { messages } from "./schema/index.js";
28
+
29
+ /** `(id, createdAt)` of a message, the shape cursor and watermark logic needs. */
30
+ export interface MessageRef {
31
+ id: string;
32
+ createdAt: number;
33
+ }
34
+
35
+ /**
36
+ * Structural read handle: anything that can `select` like the shared
37
+ * connection, which includes the connection itself and an open transaction.
38
+ * Every accessor takes `opts.db` with this type and defaults to the shared
39
+ * connection, so a caller holding its own handle (a transaction, an injected
40
+ * or in-memory database) reads the same database it writes.
41
+ */
42
+ export type MessageReadHandle = Pick<DrizzleDb, "select">;
43
+
44
+ /**
45
+ * The newest completed assistant message in a conversation, or null when it
46
+ * has none.
47
+ *
48
+ * Finalized-only: an assistant row that is still streaming is not a reply yet,
49
+ * so consumers tracking "the latest reply" (read/unread watermarks, previews)
50
+ * must not anchor on it. The row qualifies on its own once the turn completes.
51
+ *
52
+ * `db` lets a caller inside a transaction read through its own handle.
53
+ */
54
+ export function latestAssistantMessage(
55
+ conversationId: string,
56
+ opts?: { db?: MessageReadHandle },
57
+ ): MessageRef | null {
58
+ return newestFinalizedAssistantRow(conversationId, opts?.db ?? getDb());
59
+ }
60
+
61
+ /**
62
+ * The newest completed assistant message strictly before `createdAt`, or null.
63
+ * Strict comparison: rewind logic that landed on a same-timestamp sibling
64
+ * would classify the latest reply as already seen.
65
+ */
66
+ export function latestAssistantMessageBefore(
67
+ conversationId: string,
68
+ createdAt: number,
69
+ opts?: { db?: MessageReadHandle },
70
+ ): MessageRef | null {
71
+ return newestFinalizedAssistantRow(
72
+ conversationId,
73
+ opts?.db ?? getDb(),
74
+ createdAt,
75
+ );
76
+ }
77
+
78
+ function newestFinalizedAssistantRow(
79
+ conversationId: string,
80
+ db: MessageReadHandle,
81
+ strictlyBefore?: number,
82
+ ): MessageRef | null {
83
+ const row = db
84
+ .select({ id: messages.id, createdAt: messages.createdAt })
85
+ .from(messages)
86
+ .where(
87
+ and(
88
+ eq(messages.conversationId, conversationId),
89
+ eq(messages.role, "assistant"),
90
+ eq(messages.finalized, 1),
91
+ ...(strictlyBefore === undefined
92
+ ? []
93
+ : [lt(messages.createdAt, strictlyBefore)]),
94
+ ),
95
+ )
96
+ .orderBy(desc(messages.createdAt), desc(messages.id))
97
+ .limit(1)
98
+ .get();
99
+ return row ?? null;
100
+ }
101
+
102
+ /**
103
+ * Raw `content` of the last user-role row in a conversation, in insertion
104
+ * order (`rowid DESC`, not `createdAt`), or null when there is none.
105
+ *
106
+ * Insertion order is load-bearing for the undo path this serves: same-
107
+ * millisecond rows must resolve to the one written last. User rows are always
108
+ * written finalized, so no completeness predicate applies; only assistant
109
+ * turns stream.
110
+ */
111
+ export function latestUserMessageRawContent(
112
+ conversationId: string,
113
+ opts?: { db?: MessageReadHandle },
114
+ ): string | null {
115
+ const row = (opts?.db ?? getDb())
116
+ .select({ content: messages.content })
117
+ .from(messages)
118
+ .where(
119
+ and(
120
+ eq(messages.conversationId, conversationId),
121
+ eq(messages.role, "user"),
122
+ ),
123
+ )
124
+ .orderBy(sql`rowid DESC`)
125
+ .limit(1)
126
+ .get();
127
+ return row?.content ?? null;
128
+ }
129
+
130
+ /**
131
+ * Per-conversation count and latest timestamp of rows with `role`, for the
132
+ * given conversations.
133
+ *
134
+ * Any-state deliberately: these aggregates feed list ordering and previews,
135
+ * where a streaming reply still counts as activity. A caller needing
136
+ * completed-only aggregates should say so here rather than post-filtering.
137
+ */
138
+ export function countMessagesByRoleForConversations(
139
+ conversationIds: string[],
140
+ role: string,
141
+ opts?: { db?: MessageReadHandle },
142
+ ): Map<string, { count: number; lastAt: number }> {
143
+ if (conversationIds.length === 0) {
144
+ return new Map();
145
+ }
146
+ const rows = (opts?.db ?? getDb())
147
+ .select({
148
+ conversationId: messages.conversationId,
149
+ count: sql<number>`COUNT(*)`.as("count"),
150
+ lastAt: sql<number>`MAX(${messages.createdAt})`.as("last_at"),
151
+ })
152
+ .from(messages)
153
+ .where(
154
+ and(
155
+ inArray(messages.conversationId, conversationIds),
156
+ eq(messages.role, role),
157
+ ),
158
+ )
159
+ .groupBy(messages.conversationId)
160
+ .all();
161
+ return new Map(
162
+ rows.map((r) => [
163
+ r.conversationId,
164
+ { count: Number(r.count), lastAt: Number(r.lastAt) },
165
+ ]),
166
+ );
167
+ }
168
+
169
+ /**
170
+ * Which of `messageIds` exist as rows, in any state.
171
+ *
172
+ * Any-state deliberately: existence is the question (orphan detection for
173
+ * rows referencing messages), and a streaming row exists. An empty input
174
+ * returns an empty set without querying.
175
+ */
176
+ export function existingMessageIds(
177
+ messageIds: string[],
178
+ opts?: { db?: MessageReadHandle },
179
+ ): Set<string> {
180
+ if (messageIds.length === 0) {
181
+ return new Set();
182
+ }
183
+ return new Set(
184
+ (opts?.db ?? getDb())
185
+ .select({ id: messages.id })
186
+ .from(messages)
187
+ .where(inArray(messages.id, messageIds))
188
+ .all()
189
+ .map((r) => r.id),
190
+ );
191
+ }
192
+
193
+ /**
194
+ * The conversation that owns `messageId`, in any state, or null when the
195
+ * message does not exist.
196
+ *
197
+ * Any-state deliberately: callers hold an id the user acted on (a bookmark on
198
+ * a rendered row, an attachment link), and a rendered row may still be
199
+ * streaming.
200
+ */
201
+ export function messageConversationId(
202
+ messageId: string,
203
+ opts?: { db?: MessageReadHandle },
204
+ ): string | null {
205
+ const row = (opts?.db ?? getDb())
206
+ .select({ conversationId: messages.conversationId })
207
+ .from(messages)
208
+ .where(eq(messages.id, messageId))
209
+ .get();
210
+ return row?.conversationId ?? null;
211
+ }
@@ -0,0 +1,120 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import * as schema from "../schema.js";
7
+ import { migrateChatgptSubscriptionRowIdentity } from "./366-chatgpt-subscription-row-identity.js";
8
+
9
+ function createTestDb() {
10
+ const sqlite = new Database(":memory:");
11
+ sqlite.exec(/*sql*/ `
12
+ CREATE TABLE provider_connections (
13
+ name TEXT PRIMARY KEY,
14
+ provider TEXT NOT NULL,
15
+ auth TEXT NOT NULL,
16
+ label TEXT,
17
+ base_url TEXT,
18
+ models TEXT,
19
+ created_at INTEGER NOT NULL,
20
+ updated_at INTEGER NOT NULL
21
+ );
22
+ `);
23
+ return { sqlite, db: drizzle(sqlite, { schema }) };
24
+ }
25
+
26
+ function insertRow(
27
+ sqlite: Database,
28
+ name: string,
29
+ provider: string,
30
+ auth: string,
31
+ ): void {
32
+ sqlite
33
+ .query(
34
+ /*sql*/ `INSERT INTO provider_connections (name, provider, auth, created_at, updated_at)
35
+ VALUES (?, ?, ?, 1, 1)`,
36
+ )
37
+ .run(name, provider, auth);
38
+ }
39
+
40
+ function readProvider(sqlite: Database, name: string): string | undefined {
41
+ const row = sqlite
42
+ .query(`SELECT provider FROM provider_connections WHERE name = ?`)
43
+ .get(name) as { provider: string } | null;
44
+ return row?.provider;
45
+ }
46
+
47
+ describe("migration 366: chatgpt-subscription row identity", () => {
48
+ test("flips the subscription row to provider chatgpt", () => {
49
+ const { sqlite, db } = createTestDb();
50
+ insertRow(
51
+ sqlite,
52
+ "chatgpt-subscription",
53
+ "openai",
54
+ '{"type":"oauth_subscription","credential":"credential/chatgpt/access_token"}',
55
+ );
56
+
57
+ migrateChatgptSubscriptionRowIdentity(db);
58
+
59
+ expect(readProvider(sqlite, "chatgpt-subscription")).toBe("chatgpt");
60
+ });
61
+
62
+ test("leaves a claiming row with key auth untouched", () => {
63
+ const { sqlite, db } = createTestDb();
64
+ insertRow(
65
+ sqlite,
66
+ "chatgpt-subscription",
67
+ "openai",
68
+ '{"type":"api_key","credential":"vault/openai"}',
69
+ );
70
+
71
+ migrateChatgptSubscriptionRowIdentity(db);
72
+
73
+ expect(readProvider(sqlite, "chatgpt-subscription")).toBe("openai");
74
+ });
75
+
76
+ test("leaves other rows untouched", () => {
77
+ const { sqlite, db } = createTestDb();
78
+ insertRow(sqlite, "openai-key", "openai", '{"type":"api_key"}');
79
+ insertRow(sqlite, "vellum", "vellum", '{"type":"platform"}');
80
+
81
+ migrateChatgptSubscriptionRowIdentity(db);
82
+
83
+ expect(readProvider(sqlite, "openai-key")).toBe("openai");
84
+ expect(readProvider(sqlite, "vellum")).toBe("vellum");
85
+ });
86
+
87
+ test("leaves unparseable auth untouched", () => {
88
+ const { sqlite, db } = createTestDb();
89
+ insertRow(sqlite, "chatgpt-subscription", "openai", "not json");
90
+
91
+ migrateChatgptSubscriptionRowIdentity(db);
92
+
93
+ expect(readProvider(sqlite, "chatgpt-subscription")).toBe("openai");
94
+ });
95
+
96
+ test("is idempotent", () => {
97
+ const { sqlite, db } = createTestDb();
98
+ insertRow(
99
+ sqlite,
100
+ "chatgpt-subscription",
101
+ "openai",
102
+ '{"type":"oauth_subscription"}',
103
+ );
104
+
105
+ migrateChatgptSubscriptionRowIdentity(db);
106
+ migrateChatgptSubscriptionRowIdentity(db);
107
+
108
+ expect(readProvider(sqlite, "chatgpt-subscription")).toBe("chatgpt");
109
+ });
110
+
111
+ test("no-ops without the table or the row", () => {
112
+ const bare = new Database(":memory:");
113
+ expect(() =>
114
+ migrateChatgptSubscriptionRowIdentity(drizzle(bare, { schema })),
115
+ ).not.toThrow();
116
+
117
+ const { db } = createTestDb();
118
+ expect(() => migrateChatgptSubscriptionRowIdentity(db)).not.toThrow();
119
+ });
120
+ });
@@ -0,0 +1,62 @@
1
+ import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
2
+
3
+ /**
4
+ * Stamp the ChatGPT-subscription row with its own provider identity.
5
+ *
6
+ * The row is the subscription route by definition (auth modality = provider
7
+ * identity), but it historically stored `provider: "openai"` and was found
8
+ * by name, which left provider "openai" ambiguous on disk: a BYOK api_key
9
+ * row and the subscription row read identically at the provider column.
10
+ * With the row carrying `provider: "chatgpt"`, auth type is derivable from
11
+ * the provider for every row, and provider-keyed scans for "openai" no
12
+ * longer surface the subscription row.
13
+ *
14
+ * Scoped tightly: only the canonical row name with oauth_subscription auth
15
+ * is flipped. A claiming row under the same name with key auth is an
16
+ * ordinary connection and keeps its provider. Idempotent: a flipped row no
17
+ * longer matches, and the table-exists guard covers databases from before
18
+ * migration 243.
19
+ */
20
+ export function migrateChatgptSubscriptionRowIdentity(
21
+ database: DrizzleDb,
22
+ ): void {
23
+ const raw = getSqliteFrom(database);
24
+
25
+ const tableExists = raw
26
+ .query(
27
+ `SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'provider_connections'`,
28
+ )
29
+ .get();
30
+ if (!tableExists) {
31
+ return;
32
+ }
33
+
34
+ const row = raw
35
+ .query(
36
+ `SELECT provider, auth FROM provider_connections WHERE name = 'chatgpt-subscription'`,
37
+ )
38
+ .get() as { provider: string; auth: string } | null;
39
+ if (!row || row.provider !== "openai") {
40
+ return;
41
+ }
42
+
43
+ let authType: unknown;
44
+ try {
45
+ const parsed: unknown = JSON.parse(row.auth);
46
+ authType =
47
+ parsed !== null && typeof parsed === "object"
48
+ ? (parsed as { type?: unknown }).type
49
+ : undefined;
50
+ } catch {
51
+ return;
52
+ }
53
+ if (authType !== "oauth_subscription") {
54
+ return;
55
+ }
56
+
57
+ raw
58
+ .prepare(
59
+ `UPDATE provider_connections SET provider = 'chatgpt', updated_at = ? WHERE name = 'chatgpt-subscription'`,
60
+ )
61
+ .run(Date.now());
62
+ }
@@ -19,8 +19,32 @@ import { sql } from "drizzle-orm";
19
19
  export function realUserTurnContentFilter(
20
20
  alias: string,
21
21
  ): ReturnType<typeof sql> {
22
- return sql.raw(
23
- `${alias}.content NOT LIKE '%"type":"tool\\_result"%' ESCAPE '\\' ` +
24
- `AND ${alias}.content NOT LIKE '%"type":"web\\_search\\_tool\\_result"%' ESCAPE '\\'`,
22
+ return sql.raw(realUserTurnContentFilterSql(alias));
23
+ }
24
+
25
+ /**
26
+ * Raw-string form of {@link realUserTurnContentFilter}, for queries built as
27
+ * plain SQL template strings (`rawAll` sites). Same fragment, one source.
28
+ *
29
+ * `finalized = 1`: the grouped tool-result row is a USER-role row reserved
30
+ * in-flight (`ensureToolResultRowReserved`), and while it streams its content
31
+ * is a `{ ref }` pointer that does not contain the tool_result text the
32
+ * exclusions match. Without this predicate it counts as a real user turn
33
+ * mid-tool-execution and stops counting once the content folds inline.
34
+ *
35
+ * Bare `[]` content is deliberately NOT excluded: a hidden send persists its
36
+ * user row with empty display content (`processMessage` with
37
+ * `displayContent: ""`) and still runs a turn, so `[]` rows are legitimate
38
+ * turns. The cost is a narrow pre-existing window in the writer-creation
39
+ * fallback, where the tool-result row is born finalized with a `[]`
40
+ * placeholder and counts until the first content write lands; that window is
41
+ * rare (writer creation failure only) and transient, and closing it needs a
42
+ * signal that distinguishes the placeholder from a real empty-display turn.
43
+ */
44
+ export function realUserTurnContentFilterSql(alias: string): string {
45
+ return (
46
+ `${alias}.finalized = 1 ` +
47
+ `AND ${alias}.content NOT LIKE '%"type":"tool\\_result"%' ESCAPE '\\' ` +
48
+ `AND ${alias}.content NOT LIKE '%"type":"web\\_search\\_tool\\_result"%' ESCAPE '\\'`
25
49
  );
26
50
  }
@@ -473,6 +473,7 @@ import { migrateAddConversationSubagentKind } from "./migrations/362-add-convers
473
473
  import { migrateBackfillScheduleInferenceProfile } from "./migrations/363-backfill-schedule-inference-profile.js";
474
474
  import { migrateAddScheduleSourceKey } from "./migrations/364-add-schedule-source-key.js";
475
475
  import { migrateAddConversationForkStrategy } from "./migrations/365-add-conversation-fork-strategy.js";
476
+ import { migrateChatgptSubscriptionRowIdentity } from "./migrations/366-chatgpt-subscription-row-identity.js";
476
477
  import type { MigrationStep } from "./migrations/run-migrations.js";
477
478
 
478
479
  export const migrationSteps: MigrationStep[] = [
@@ -1573,4 +1574,12 @@ export const migrationSteps: MigrationStep[] = [
1573
1574
  },
1574
1575
  migrateAddScheduleSourceKey,
1575
1576
  migrateAddConversationForkStrategy,
1577
+ {
1578
+ name: "migrateChatgptSubscriptionRowIdentity",
1579
+ run: migrateChatgptSubscriptionRowIdentity,
1580
+ // The table-exists guard treats a missing table as nothing-to-do, so the
1581
+ // creating migration must be checkpointed first or a repair flow could
1582
+ // permanently checkpoint the no-op.
1583
+ dependsOn: ["migrateCreateProviderConnections"],
1584
+ },
1576
1585
  ];