@vellumai/assistant 0.12.1 → 0.12.2-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 (577) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  6. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  7. package/openapi.yaml +360 -51
  8. package/package.json +3 -1
  9. package/scripts/smoke-desktop-window-theme.ts +186 -0
  10. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  11. package/src/__tests__/anthropic-provider.test.ts +0 -63
  12. package/src/__tests__/app-compiler.test.ts +6 -0
  13. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  14. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  15. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  16. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  17. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  18. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  19. package/src/__tests__/computer-use-tools.test.ts +19 -7
  20. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  21. package/src/__tests__/config-schema.test.ts +28 -29
  22. package/src/__tests__/config-watcher.test.ts +23 -8
  23. package/src/__tests__/connection-model-compat.test.ts +2 -2
  24. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-pairing.test.ts +26 -0
  26. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  27. package/src/__tests__/conversation-queue.test.ts +48 -207
  28. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  30. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  31. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  32. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  35. package/src/__tests__/conversation-title-service.test.ts +14 -10
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  37. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  38. package/src/__tests__/credential-record-retire.test.ts +171 -1
  39. package/src/__tests__/credential-routes.test.ts +250 -3
  40. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  41. package/src/__tests__/cu-unified-flow.test.ts +14 -4
  42. package/src/__tests__/document-sync-tags.test.ts +25 -0
  43. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  45. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  46. package/src/__tests__/git-porcelain.test.ts +47 -0
  47. package/src/__tests__/heartbeat-service.test.ts +56 -40
  48. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  49. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  50. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  51. package/src/__tests__/host-cu-proxy.test.ts +596 -0
  52. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  53. package/src/__tests__/host-file-proxy.test.ts +11 -23
  54. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  55. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  56. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  57. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  58. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  59. package/src/__tests__/mcp-health-check.test.ts +8 -11
  60. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  61. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  62. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  63. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  64. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  65. package/src/__tests__/model-intents.test.ts +3 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  67. package/src/__tests__/oauth-commands-routes.test.ts +168 -0
  68. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  69. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  70. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  71. package/src/__tests__/openai-provider.test.ts +1 -0
  72. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  73. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  74. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  75. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  76. package/src/__tests__/qdrant-manager.test.ts +132 -0
  77. package/src/__tests__/resolve-app.test.ts +135 -0
  78. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  79. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  80. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  81. package/src/__tests__/service-account-token.test.ts +247 -0
  82. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  83. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +9 -4
  85. package/src/__tests__/source-fingerprint.test.ts +184 -0
  86. package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
  87. package/src/__tests__/subagent-tools.test.ts +3 -2
  88. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  89. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  90. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  91. package/src/__tests__/unicode.test.ts +13 -0
  92. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  93. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  94. package/src/__tests__/workspace-git-service.test.ts +26 -0
  95. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  97. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  98. package/src/acp/__tests__/agent-process.test.ts +258 -4
  99. package/src/acp/__tests__/auth-required.test.ts +96 -0
  100. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  101. package/src/acp/__tests__/client-handler.test.ts +90 -2
  102. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  103. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  104. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  105. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  106. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  107. package/src/acp/__tests__/session-manager.test.ts +895 -4
  108. package/src/acp/acp-claude-oauth.ts +328 -14
  109. package/src/acp/acp-credentials.ts +19 -0
  110. package/src/acp/agent-process.test.ts +4 -1
  111. package/src/acp/agent-process.ts +96 -10
  112. package/src/acp/auth-required.ts +50 -1
  113. package/src/acp/auto-install.test.ts +9 -7
  114. package/src/acp/claude-token-refresh.ts +150 -0
  115. package/src/acp/client-handler.ts +16 -3
  116. package/src/acp/model-config.test.ts +166 -0
  117. package/src/acp/model-config.ts +138 -0
  118. package/src/acp/prepare-agent-env.ts +21 -6
  119. package/src/acp/resolve-agent.test.ts +220 -6
  120. package/src/acp/resolve-agent.ts +88 -23
  121. package/src/acp/session-manager.test.ts +196 -3
  122. package/src/acp/session-manager.ts +441 -18
  123. package/src/acp/types.ts +50 -5
  124. package/src/agent/loop.ts +1 -0
  125. package/src/api/events/acp-session-model-update.test.ts +88 -0
  126. package/src/api/events/acp-session-model-update.ts +46 -0
  127. package/src/api/events/app-files-changed.ts +4 -3
  128. package/src/api/events/notification-intent.ts +2 -0
  129. package/src/api/events/user-message-echo.ts +7 -1
  130. package/src/api/index.ts +6 -0
  131. package/src/api/responses/conversation-message.ts +5 -0
  132. package/src/apps/app-store.ts +1 -1
  133. package/src/apps/resolve-app.ts +81 -0
  134. package/src/apps/source-fingerprint.ts +266 -0
  135. package/src/avatar/identity-avatar.ts +7 -2
  136. package/src/bundler/app-compiler.ts +15 -0
  137. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  138. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  139. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  140. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  141. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  142. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  143. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  144. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  145. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  146. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  147. package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
  148. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  149. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  150. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  151. package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
  152. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  153. package/src/calls/barge-in-guard.ts +118 -0
  154. package/src/calls/call-constants.ts +24 -4
  155. package/src/calls/call-controller.ts +499 -199
  156. package/src/calls/call-transport.ts +16 -0
  157. package/src/calls/media-stream-output.ts +32 -0
  158. package/src/calls/media-stream-server.ts +82 -30
  159. package/src/calls/media-stream-stt-session.ts +15 -0
  160. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  161. package/src/calls/resolve-call-tts-provider.ts +33 -2
  162. package/src/calls/tts-call-strategy.ts +9 -5
  163. package/src/calls/types.ts +2 -0
  164. package/src/calls/voice-control-protocol.ts +155 -21
  165. package/src/calls/voice-leg-coordinator.ts +189 -0
  166. package/src/calls/voice-progress-cadence.ts +423 -0
  167. package/src/calls/voice-session-bridge.ts +92 -72
  168. package/src/calls/voice-triage-escalate.ts +178 -59
  169. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  170. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  171. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  172. package/src/cli/commands/apps.help.ts +67 -13
  173. package/src/cli/commands/apps.ts +183 -0
  174. package/src/cli/commands/bash.help.ts +8 -13
  175. package/src/cli/commands/bash.ts +7 -25
  176. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  177. package/src/cli/commands/channels/index.help.ts +70 -2
  178. package/src/cli/commands/channels/index.ts +16 -3
  179. package/src/cli/commands/channels/send.ts +128 -0
  180. package/src/cli/commands/credentials.help.ts +6 -2
  181. package/src/cli/commands/credentials.ts +2 -2
  182. package/src/cli/commands/mcp.help.ts +8 -8
  183. package/src/cli/commands/mcp.ts +1 -1
  184. package/src/cli/commands/oauth/status.ts +5 -0
  185. package/src/cli/commands/roadmap.help.ts +5 -1
  186. package/src/cli/commands/roadmap.ts +1 -1
  187. package/src/cli/lib/bundled-marketplace.json +103 -90
  188. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  189. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  190. package/src/config/__tests__/platform-identity.test.ts +255 -0
  191. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  192. package/src/config/acp-agent-write.ts +43 -0
  193. package/src/config/acp-defaults.test.ts +13 -3
  194. package/src/config/acp-defaults.ts +23 -6
  195. package/src/config/acp-schema.test.ts +81 -0
  196. package/src/config/acp-schema.ts +30 -2
  197. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  198. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  199. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  200. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  201. package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
  202. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  203. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  204. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  205. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  206. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  207. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  208. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  209. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  210. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  211. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  212. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  213. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  214. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  215. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  216. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  217. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  218. package/src/config/default-profile-catalog.ts +1 -1
  219. package/src/config/env.ts +2 -2
  220. package/src/config/feature-flag-registry.json +33 -13
  221. package/src/config/loader.ts +2 -0
  222. package/src/config/memory-v3-gate.test.ts +31 -0
  223. package/src/config/memory-v3-gate.ts +20 -16
  224. package/src/config/platform-identity.ts +201 -0
  225. package/src/config/platform-rehydration.ts +11 -27
  226. package/src/config/schema.ts +2 -2
  227. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  228. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  229. package/src/config/schemas/calls.ts +1 -28
  230. package/src/config/schemas/live-voice.ts +1 -173
  231. package/src/config/schemas/mcp.ts +1 -1
  232. package/src/config/schemas/memory-retrospective.ts +10 -0
  233. package/src/config/schemas/services.ts +18 -0
  234. package/src/config/schemas/skills.ts +0 -14
  235. package/src/config/schemas/voice.ts +150 -0
  236. package/src/credential-execution/process-manager.test.ts +18 -19
  237. package/src/credential-health/credential-health-service.ts +11 -17
  238. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  239. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  240. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  241. package/src/daemon/app-change-notify.ts +45 -0
  242. package/src/daemon/config-watcher.ts +13 -194
  243. package/src/daemon/conversation-error.ts +3 -2
  244. package/src/daemon/conversation-media-retry.ts +2 -1
  245. package/src/daemon/conversation-process.ts +35 -64
  246. package/src/daemon/conversation-runtime-assembly.ts +20 -7
  247. package/src/daemon/conversation-skill-tools.ts +13 -31
  248. package/src/daemon/conversation-store.ts +1 -1
  249. package/src/daemon/conversation-surfaces.ts +21 -10
  250. package/src/daemon/conversation-tool-setup.ts +45 -59
  251. package/src/daemon/conversation.ts +0 -3
  252. package/src/daemon/dictation-profile-store.ts +2 -1
  253. package/src/daemon/handlers/shared.ts +2 -1
  254. package/src/daemon/handlers/skills.ts +6 -3
  255. package/src/daemon/host-bash-proxy.ts +8 -12
  256. package/src/daemon/host-browser-proxy.ts +15 -1
  257. package/src/daemon/host-cu-proxy.ts +224 -19
  258. package/src/daemon/host-file-proxy.ts +8 -10
  259. package/src/daemon/host-proxy-preactivation.ts +2 -1
  260. package/src/daemon/host-transfer-proxy.ts +14 -18
  261. package/src/daemon/lifecycle.ts +0 -5
  262. package/src/daemon/mcp-reload-service.ts +5 -5
  263. package/src/daemon/message-types/sync.ts +1 -0
  264. package/src/daemon/orphan-reaper.ts +1 -1
  265. package/src/daemon/providers-setup.ts +1 -1
  266. package/src/daemon/reaction-history-render.ts +2 -1
  267. package/src/daemon/shutdown-handlers.ts +1 -9
  268. package/src/daemon/skill-memory-refresh.ts +64 -0
  269. package/src/daemon/tool-side-effects.ts +7 -18
  270. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  271. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  272. package/src/desktop/desktop-chrome-policy.ts +47 -0
  273. package/src/desktop/desktop-chrome-session.ts +37 -0
  274. package/src/desktop/desktop-dependencies.test.ts +102 -0
  275. package/src/desktop/desktop-dependencies.ts +308 -0
  276. package/src/desktop/desktop-feature.test.ts +18 -0
  277. package/src/desktop/desktop-feature.ts +19 -0
  278. package/src/desktop/desktop-panel-config.test.ts +135 -0
  279. package/src/desktop/desktop-panel-config.ts +149 -0
  280. package/src/desktop/desktop-session-manager.test.ts +730 -0
  281. package/src/desktop/desktop-session-manager.ts +850 -0
  282. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  283. package/src/desktop/desktop-stream-bridge.ts +214 -0
  284. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  285. package/src/desktop/desktop-wallpaper.ts +113 -0
  286. package/src/desktop/desktop-window-drag.ts +177 -0
  287. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  288. package/src/desktop/desktop-window-manager-config.ts +238 -0
  289. package/src/desktop/desktop-window-theme.ts +170 -0
  290. package/src/heartbeat/heartbeat-service.ts +21 -3
  291. package/src/home/__tests__/feed-writer.test.ts +22 -0
  292. package/src/home/feed-writer.ts +11 -2
  293. package/src/hooks/types.ts +33 -0
  294. package/src/i18n/AGENTS.md +22 -0
  295. package/src/i18n/__tests__/i18n.test.ts +108 -0
  296. package/src/i18n/index.ts +16 -0
  297. package/src/i18n/locales.ts +93 -0
  298. package/src/i18n/messages.ts +61 -0
  299. package/src/i18n/title.ts +25 -0
  300. package/src/inbound/platform-callback-registration.ts +4 -5
  301. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  302. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  303. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  304. package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
  305. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  306. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  307. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  308. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  309. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  310. package/src/live-voice/live-voice-photo.ts +25 -6
  311. package/src/live-voice/live-voice-session.ts +297 -699
  312. package/src/live-voice/protocol.ts +60 -2
  313. package/src/live-voice/session-controls.ts +111 -0
  314. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  315. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  316. package/src/mcp/effective-config.ts +3 -3
  317. package/src/mcp/mcp-header-store.ts +29 -45
  318. package/src/mcp/spec-schema.ts +111 -0
  319. package/src/mcp/startup.ts +8 -4
  320. package/src/mcp/workspace-mcp-config.ts +188 -0
  321. package/src/messaging/provider.ts +10 -3
  322. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  323. package/src/messaging/providers/callback-routing.ts +18 -0
  324. package/src/messaging/providers/channel-transport.ts +56 -0
  325. package/src/messaging/providers/discord/transport.ts +17 -0
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  327. package/src/messaging/providers/slack/adapter.ts +7 -44
  328. package/src/messaging/providers/slack/transport.ts +16 -0
  329. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  330. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  331. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  332. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  333. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  334. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  335. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  336. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  337. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  338. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  339. package/src/notifications/adapters/macos.ts +3 -0
  340. package/src/notifications/assistant-reply-producer.ts +8 -3
  341. package/src/notifications/conversation-pairing.ts +36 -16
  342. package/src/notifications/delivered-post-record.ts +11 -1
  343. package/src/notifications/guardian-feed-projection.ts +125 -5
  344. package/src/notifications/home-feed-side-effect.ts +55 -7
  345. package/src/notifications/schedule-result-producer.ts +66 -22
  346. package/src/oauth/AGENTS.md +2 -0
  347. package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
  348. package/src/oauth/byo-connection.test.ts +105 -0
  349. package/src/oauth/byo-connection.ts +36 -6
  350. package/src/oauth/connection-resolver.test.ts +100 -0
  351. package/src/oauth/connection-resolver.ts +120 -3
  352. package/src/oauth/provider-serializer.ts +9 -0
  353. package/src/oauth/scope-utils.ts +24 -0
  354. package/src/oauth/seed-providers.ts +314 -27
  355. package/src/outbound-proxy/certs.ts +3 -5
  356. package/src/permissions/checker.test.ts +11 -6
  357. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  358. package/src/persistence/conversation-crud.ts +71 -63
  359. package/src/persistence/conversation-lineage.ts +12 -4
  360. package/src/persistence/conversation-queries.ts +2 -2
  361. package/src/persistence/conversation-title-placeholders.ts +23 -0
  362. package/src/persistence/conversation-title-service.ts +9 -26
  363. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  364. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  365. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  366. package/src/persistence/message-cursor.ts +78 -0
  367. package/src/persistence/message-reads.ts +30 -0
  368. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  369. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  370. package/src/persistence/schema/memory-core.ts +5 -0
  371. package/src/platform/client.test.ts +4 -2
  372. package/src/platform/client.ts +2 -10
  373. package/src/plugin-api/constants.ts +2 -0
  374. package/src/plugin-api/index.ts +1 -0
  375. package/src/plugin-api/types.ts +2 -0
  376. package/src/plugins/defaults/index.ts +2 -0
  377. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  378. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  379. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  380. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  381. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  382. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  383. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  384. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  385. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  386. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  387. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  388. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  389. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  390. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  391. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  392. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  393. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  394. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  395. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  396. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  397. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  398. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  399. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  400. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  401. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  402. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  403. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  404. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  405. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  406. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  407. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  408. package/src/plugins/defaults/memory/startup.ts +28 -2
  409. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  410. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  411. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  412. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  413. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  414. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  415. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  416. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  417. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  418. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  419. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  420. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  421. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  422. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  423. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  424. package/src/plugins/defaults/memory/worker.ts +6 -0
  425. package/src/plugins/mcp-servers.ts +15 -75
  426. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  427. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  428. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  429. package/src/providers/anthropic/client.ts +0 -47
  430. package/src/providers/gemini/client.ts +9 -23
  431. package/src/providers/gemini-thought-signature.test.ts +63 -0
  432. package/src/providers/gemini-thought-signature.ts +44 -0
  433. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  434. package/src/providers/inference/auth.ts +2 -3
  435. package/src/providers/inference/connection-availability.ts +27 -8
  436. package/src/providers/inference/endpoint-probe.ts +10 -1
  437. package/src/providers/inference/resolve-auth.ts +22 -4
  438. package/src/providers/inference/service-account-token.ts +248 -0
  439. package/src/providers/model-catalog.ts +34 -0
  440. package/src/providers/model-intents.ts +10 -5
  441. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  442. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  443. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  444. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  445. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  446. package/src/providers/openai/chat-completions-provider.ts +212 -79
  447. package/src/providers/openai/codex-models.ts +0 -4
  448. package/src/providers/openai/google-thought-signature.ts +320 -0
  449. package/src/providers/opencode/client.test.ts +73 -4
  450. package/src/providers/opencode/client.ts +60 -3
  451. package/src/providers/openrouter/client.ts +20 -0
  452. package/src/providers/outbound-request-sanitize.ts +96 -0
  453. package/src/providers/retry.ts +30 -19
  454. package/src/providers/unparseable-tool-args.ts +5 -1
  455. package/src/runtime/AGENTS.md +3 -1
  456. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  457. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  458. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  459. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  460. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  461. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  462. package/src/runtime/agent-wake.ts +2 -1
  463. package/src/runtime/assistant-event-hub.ts +21 -3
  464. package/src/runtime/auth/same-actor.ts +38 -3
  465. package/src/runtime/channel-send.ts +316 -0
  466. package/src/runtime/host-observe.ts +11 -1
  467. package/src/runtime/http-server.ts +160 -151
  468. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  469. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  470. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
  471. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  472. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  473. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  474. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  475. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  476. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  477. package/src/runtime/routes/acp-routes.test.ts +15 -0
  478. package/src/runtime/routes/acp-routes.ts +72 -9
  479. package/src/runtime/routes/app-management-routes.ts +90 -2
  480. package/src/runtime/routes/app-routes.ts +1 -1
  481. package/src/runtime/routes/channel-send-routes.ts +85 -0
  482. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  483. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  484. package/src/runtime/routes/conversation-routes.ts +16 -3
  485. package/src/runtime/routes/credential-routes.ts +20 -14
  486. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  487. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  488. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  489. package/src/runtime/routes/events-routes.ts +4 -0
  490. package/src/runtime/routes/host-cu-routes.ts +6 -0
  491. package/src/runtime/routes/index.ts +4 -2
  492. package/src/runtime/routes/log-export-routes.ts +2 -0
  493. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  494. package/src/runtime/routes/migration-routes.ts +6 -20
  495. package/src/runtime/routes/oauth-commands-routes.ts +50 -0
  496. package/src/runtime/routes/oauth-providers.ts +7 -0
  497. package/src/runtime/routes/platform-routes.ts +18 -15
  498. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  499. package/src/runtime/routes/workspace-routes.ts +46 -63
  500. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  501. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  502. package/src/runtime/routes/workspace-utils.ts +17 -0
  503. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  504. package/src/runtime/services/conversation-serializer.ts +16 -3
  505. package/src/runtime/sync/resource-sync-events.ts +2 -1
  506. package/src/schedule/run-script.ts +2 -1
  507. package/src/schedule/schedule-store.ts +9 -2
  508. package/src/schedule/worker.ts +1 -1
  509. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  510. package/src/security/ces-http-record-backend.ts +120 -0
  511. package/src/security/ces-rpc-record-backend.ts +7 -0
  512. package/src/security/untrusted-content.ts +3 -1
  513. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  514. package/src/skills/workspace-skill-md-signature.ts +50 -0
  515. package/src/subagent/consult-context.ts +12 -25
  516. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  517. package/src/tools/acp/list-agents.test.ts +3 -1
  518. package/src/tools/acp/spawn.test.ts +146 -11
  519. package/src/tools/acp/spawn.ts +21 -3
  520. package/src/tools/acp/status.test.ts +94 -0
  521. package/src/tools/acp/status.ts +40 -3
  522. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  523. package/src/tools/browser/browser-execution.ts +1 -1
  524. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  525. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  526. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  527. package/src/tools/browser/network-recorder.ts +2 -1
  528. package/src/tools/capability-offer.test.ts +22 -2
  529. package/src/tools/capability-offer.ts +16 -1
  530. package/src/tools/computer-use/definitions.ts +121 -2
  531. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  532. package/src/tools/credentials/metadata-store.ts +100 -0
  533. package/src/tools/filesystem/search.ts +2 -1
  534. package/src/tools/host-filesystem/edit.test.ts +2 -1
  535. package/src/tools/host-filesystem/edit.ts +2 -2
  536. package/src/tools/host-filesystem/read.test.ts +2 -1
  537. package/src/tools/host-filesystem/read.ts +2 -2
  538. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  539. package/src/tools/host-filesystem/transfer.ts +2 -2
  540. package/src/tools/host-filesystem/write.test.ts +2 -1
  541. package/src/tools/host-filesystem/write.ts +2 -2
  542. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  543. package/src/tools/host-terminal/host-shell.ts +4 -20
  544. package/src/tools/network/web-search.ts +6 -1
  545. package/src/tools/policy-context.ts +6 -7
  546. package/src/tools/skills/scaffold-managed.ts +61 -3
  547. package/src/tools/subagent/spawn.ts +5 -8
  548. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  549. package/src/tools/terminal/sanitized-bash.ts +105 -0
  550. package/src/tools/ui-surface/channel-variants.ts +10 -59
  551. package/src/tools/watch/watch-retro-report.ts +0 -8
  552. package/src/types/host-capabilities.ts +5 -3
  553. package/src/util/__tests__/language-subtag.test.ts +23 -1
  554. package/src/util/ca-bundle.ts +13 -0
  555. package/src/util/language-subtag.ts +16 -0
  556. package/src/util/platform.ts +8 -0
  557. package/src/util/unicode.ts +15 -1
  558. package/src/watch/watch-retro.ts +6 -8
  559. package/src/watch/watch-timeline.ts +2 -2
  560. package/src/workflows/run-manager.ts +2 -1
  561. package/src/workspace/git-porcelain.ts +66 -0
  562. package/src/workspace/git-service.ts +6 -55
  563. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  564. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  565. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  566. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  567. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  568. package/src/workspace/migrations/registry.ts +6 -0
  569. package/src/__tests__/app-source-watcher.test.ts +0 -218
  570. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  571. package/src/__tests__/speaker-identification.test.ts +0 -56
  572. package/src/calls/speaker-identification.ts +0 -233
  573. package/src/daemon/app-source-watcher.ts +0 -218
  574. package/src/messaging/providers/whatsapp/client.ts +0 -29
  575. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  576. package/src/util/fs-watcher-error.ts +0 -36
  577. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -50,6 +50,8 @@ describe("isConnectionCompatibleWithModel", () => {
50
50
  expect(isConnectionCompatibleWithModel(conn, "gpt-5.4-nano")).toBe(false);
51
51
  expect(isConnectionCompatibleWithModel(conn, "gpt-5.5-pro")).toBe(false);
52
52
  expect(isConnectionCompatibleWithModel(conn, "gpt-5.3-codex")).toBe(false);
53
+ expect(isConnectionCompatibleWithModel(conn, "gpt-5.4")).toBe(false);
54
+ expect(isConnectionCompatibleWithModel(conn, "gpt-5.4-mini")).toBe(false);
53
55
  });
54
56
 
55
57
  test("oauth_subscription connection is compatible with a Codex model", () => {
@@ -59,8 +61,6 @@ describe("isConnectionCompatibleWithModel", () => {
59
61
  expect(isConnectionCompatibleWithModel(conn, "gpt-5.6-terra")).toBe(true);
60
62
  expect(isConnectionCompatibleWithModel(conn, "gpt-5.6-luna")).toBe(true);
61
63
  expect(isConnectionCompatibleWithModel(conn, "gpt-5.5")).toBe(true);
62
- expect(isConnectionCompatibleWithModel(conn, "gpt-5.4")).toBe(true);
63
- expect(isConnectionCompatibleWithModel(conn, "gpt-5.4-mini")).toBe(true);
64
64
  });
65
65
 
66
66
  test("undefined model applies no gating (compatible)", () => {
@@ -165,7 +165,7 @@ describe("auto-resolution skips oauth_subscription connections for non-Codex mod
165
165
 
166
166
  test("Codex model can select the oauth_subscription connection", async () => {
167
167
  registerConnections([OPENAI_CODEX, OPENAI_KEY]);
168
- setOpenAiProfile("gpt-5.4");
168
+ setOpenAiProfile("gpt-5.5");
169
169
 
170
170
  const result = await getConfiguredProvider("mainAgent", {
171
171
  overrideProfile: "openai-any",
@@ -149,6 +149,47 @@ describe("RetryProvider — callSite resolution", () => {
149
149
  expect(headers).not.toHaveProperty("session_id");
150
150
  });
151
151
 
152
+ test("stamps a stable fallback session on OpenCode calls with no conversation", async () => {
153
+ setLlmConfig({
154
+ callSites: {
155
+ memoryRetrieval: {
156
+ provider: "opencode",
157
+ model: "mimo-v2.5-free",
158
+ },
159
+ },
160
+ });
161
+
162
+ const seen: SendMessageOptions[] = [];
163
+ const wrapped = new RetryProvider(
164
+ makeProvider("opencode", (options) => {
165
+ seen.push(options!);
166
+ }),
167
+ );
168
+
169
+ await wrapped.sendMessage(DUMMY_MESSAGES, {
170
+ config: { callSite: "memoryRetrieval" },
171
+ });
172
+ await wrapped.sendMessage(DUMMY_MESSAGES, {
173
+ config: { callSite: "memoryRetrieval", conversationId: " " },
174
+ });
175
+
176
+ const sessions = seen.map(
177
+ (options) =>
178
+ (options.config?.requestHeaders as Record<string, string>)[
179
+ "x-opencode-session"
180
+ ],
181
+ );
182
+ expect(sessions[0]).toMatch(/\S/);
183
+ expect(sessions[1]).toBe(sessions[0]!);
184
+ for (const options of seen) {
185
+ const config = options.config as Record<string, unknown>;
186
+ const headers = config.requestHeaders as Record<string, string>;
187
+ expect(headers["x-opencode-request"]).toMatch(/\S/);
188
+ expect(headers).not.toHaveProperty("session_id");
189
+ expect(config.conversationId).toBeUndefined();
190
+ }
191
+ });
192
+
152
193
  test("does not stamp OpenCode headers on openai-compatible", async () => {
153
194
  setLlmConfig({
154
195
  callSites: {
@@ -0,0 +1,176 @@
1
+ /**
2
+ * `RetryProvider` strips orphaned UTF-16 surrogates from every outbound
3
+ * request before the inner adapter sees it. An orphan (half of a surrogate
4
+ * pair left by a code-unit `.slice()`) serializes to a `\udXXX` escape that
5
+ * strict provider parsers reject with a 400, so the guard sits at the one
6
+ * layer every adapter shares instead of inside each client.
7
+ */
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+
11
+ import { stripOrphanedSurrogates } from "../../util/unicode.js";
12
+ import { RetryProvider } from "../retry.js";
13
+ import type {
14
+ Message,
15
+ Provider,
16
+ ProviderResponse,
17
+ SendMessageOptions,
18
+ } from "../types.js";
19
+
20
+ const OK_RESPONSE = {
21
+ content: [{ type: "text", text: "ok" }],
22
+ model: "test-model",
23
+ usage: { inputTokens: 1, outputTokens: 1 },
24
+ } as unknown as ProviderResponse;
25
+
26
+ const LONE_HIGH = "\uD83C";
27
+ const LONE_LOW = "\uDF89";
28
+ const EMOJI = "🎉";
29
+
30
+ function capturingProvider(): {
31
+ provider: Provider;
32
+ received: () => { messages: Message[]; options?: SendMessageOptions };
33
+ } {
34
+ let captured: { messages: Message[]; options?: SendMessageOptions } | null =
35
+ null;
36
+ const provider: Provider = {
37
+ name: "openai",
38
+ sendMessage: async (messages, options) => {
39
+ captured = { messages, options };
40
+ return OK_RESPONSE;
41
+ },
42
+ };
43
+ return {
44
+ provider,
45
+ received: () => {
46
+ if (!captured) {
47
+ throw new Error("inner provider was not called");
48
+ }
49
+ return captured;
50
+ },
51
+ };
52
+ }
53
+
54
+ describe("RetryProvider outbound surrogate sanitization", () => {
55
+ test("an orphaned surrogate in a text block, a tool result, the system prompt, and a tool description never reaches the inner adapter", async () => {
56
+ const { provider, received } = capturingProvider();
57
+ const messages: Message[] = [
58
+ {
59
+ role: "user",
60
+ content: [{ type: "text", text: `memory preview ${LONE_HIGH} cut` }],
61
+ },
62
+ {
63
+ role: "assistant",
64
+ content: [{ type: "tool_use", id: "tu1", name: "bash", input: {} }],
65
+ },
66
+ {
67
+ role: "user",
68
+ content: [
69
+ {
70
+ type: "tool_result",
71
+ tool_use_id: "tu1",
72
+ content: `shell output ${LONE_LOW} more`,
73
+ },
74
+ ],
75
+ },
76
+ ];
77
+
78
+ await new RetryProvider(provider).sendMessage(messages, {
79
+ systemPrompt: `system ${LONE_HIGH}`,
80
+ tools: [
81
+ {
82
+ name: "select_pages",
83
+ description: `pick ${LONE_LOW}`,
84
+ input_schema: { type: "object", properties: {} },
85
+ },
86
+ ],
87
+ });
88
+
89
+ const sent = received();
90
+ expect(stripOrphanedSurrogates(JSON.stringify(sent.messages))).toBe(
91
+ JSON.stringify(sent.messages),
92
+ );
93
+ const systemPrompt = sent.options?.systemPrompt ?? "";
94
+ expect(stripOrphanedSurrogates(systemPrompt)).toBe(systemPrompt);
95
+ expect(stripOrphanedSurrogates(JSON.stringify(sent.options?.tools))).toBe(
96
+ JSON.stringify(sent.options?.tools),
97
+ );
98
+ // The orphan is replaced, not dropped, so surrounding text survives.
99
+ const text = sent.messages[0]!.content[0] as { text: string };
100
+ expect(text.text).toBe("memory preview � cut");
101
+ });
102
+
103
+ test("an inline base64 payload is passed through by reference, not scanned", async () => {
104
+ const { provider, received } = capturingProvider();
105
+ // Base64 is ASCII by construction; the guard must not walk a payload that
106
+ // can run to a hundred megabytes. A deliberately corrupt payload proves
107
+ // the field was skipped rather than scanned and found clean.
108
+ const source = {
109
+ type: "base64" as const,
110
+ media_type: "image/png",
111
+ data: `AAAA${LONE_HIGH}`,
112
+ };
113
+ const messages: Message[] = [
114
+ {
115
+ role: "user",
116
+ content: [
117
+ { type: "image", source },
118
+ { type: "text", text: `caption ${LONE_LOW}` },
119
+ ],
120
+ },
121
+ ];
122
+
123
+ await new RetryProvider(provider).sendMessage(messages);
124
+
125
+ const sent = received();
126
+ const image = sent.messages[0]!.content[0] as { source: unknown };
127
+ expect(image.source).toBe(source);
128
+ const text = sent.messages[0]!.content[1] as { text: string };
129
+ expect(text.text).toBe("caption �");
130
+ });
131
+
132
+ test("token-count requests are sanitized the same way", async () => {
133
+ let counted: {
134
+ messages: Message[];
135
+ systemPrompt: string;
136
+ tools: unknown;
137
+ } | null = null;
138
+ const provider: Provider = {
139
+ name: "anthropic",
140
+ sendMessage: async () => OK_RESPONSE,
141
+ countInputTokens: async (messages, systemPrompt, tools) => {
142
+ counted = { messages, systemPrompt, tools };
143
+ return 7;
144
+ },
145
+ };
146
+ const messages: Message[] = [
147
+ { role: "user", content: [{ type: "text", text: `count ${LONE_HIGH}` }] },
148
+ ];
149
+
150
+ const total = await new RetryProvider(provider).countInputTokens!(
151
+ messages,
152
+ `system ${LONE_LOW}`,
153
+ undefined,
154
+ );
155
+
156
+ expect(total).toBe(7);
157
+ const seen = counted!;
158
+ const text = seen.messages[0]!.content[0] as { text: string };
159
+ expect(text.text).toBe("count �");
160
+ expect(seen.systemPrompt).toBe("system �");
161
+ });
162
+
163
+ test("a well-formed request passes through by reference, emoji intact", async () => {
164
+ const { provider, received } = capturingProvider();
165
+ const messages: Message[] = [
166
+ { role: "user", content: [{ type: "text", text: `hello ${EMOJI}` }] },
167
+ ];
168
+
169
+ await new RetryProvider(provider).sendMessage(messages);
170
+
171
+ const sent = received();
172
+ expect(sent.messages).toBe(messages);
173
+ const text = sent.messages[0]!.content[0] as { text: string };
174
+ expect(text.text).toContain(EMOJI);
175
+ });
176
+ });
@@ -9,7 +9,6 @@ import {
9
9
  INSUFFICIENT_CREDITS_PATTERNS,
10
10
  } from "../../util/provider-error-patterns.js";
11
11
  import { extractRetryAfterMs } from "../../util/retry.js";
12
- import { stripOrphanedSurrogatesDeep } from "../../util/unicode.js";
13
12
  import {
14
13
  clampProviderString,
15
14
  isDecodableTextMimeType,
@@ -201,43 +200,6 @@ export function deriveAnthropicReason(
201
200
  return "unknown";
202
201
  }
203
202
 
204
- /** Rate-limit the orphaned-surrogate warning so a single bad stream can't flood logs. */
205
- const ORPHAN_WARNING_THROTTLE_MS = 60_000;
206
- let lastOrphanWarningMs = 0;
207
-
208
- function logOrphanedSurrogateWarning(
209
- fixedStringCount: number,
210
- messages: Anthropic.MessageParam[],
211
- ): void {
212
- const now = Date.now();
213
- if (now - lastOrphanWarningMs < ORPHAN_WARNING_THROTTLE_MS) {
214
- return;
215
- }
216
- lastOrphanWarningMs = now;
217
- const blockTypes = new Set<string>();
218
- for (const msg of messages) {
219
- if (!Array.isArray(msg.content)) {
220
- continue;
221
- }
222
- for (const block of msg.content) {
223
- if (typeof block !== "object" || block == null) {
224
- continue;
225
- }
226
- const type = (block as { type?: string }).type;
227
- if (type) {
228
- blockTypes.add(type);
229
- }
230
- }
231
- }
232
- log.warn(
233
- {
234
- fixedStringCount,
235
- blockTypes: Array.from(blockTypes),
236
- },
237
- "stripped orphaned UTF-16 surrogates from outbound Anthropic request — upstream truncation is not surrogate-aware",
238
- );
239
- }
240
-
241
203
  /**
242
204
  * Validate an Anthropic API key by making a lightweight GET /v1/models call.
243
205
  * Returns `{ valid: true }` on success or `{ valid: false, reason: string }` on failure.
@@ -1299,15 +1261,6 @@ export class AnthropicProvider implements Provider {
1299
1261
  // previous turn exists to anchor). At most two message-level
1300
1262
  // breakpoints are placed, so the total can't drift past 4.
1301
1263
 
1302
- // Strip orphaned UTF-16 surrogates so the Anthropic JSON parser never
1303
- // sees invalid strings produced by upstream surrogate-splitting `.slice()` calls.
1304
- const sanitized = stripOrphanedSurrogatesDeep(params);
1305
- if (sanitized.changed) {
1306
- logOrphanedSurrogateWarning(sanitized.fixedStringCount, sentMessages);
1307
- params = sanitized.value;
1308
- sentMessages = params.messages;
1309
- }
1310
-
1311
1264
  // Callers can stamp `cache_control` on message blocks before the
1312
1265
  // provider sees them. Two repairs apply:
1313
1266
  // - `disableCache`: strip the marker entirely — this call opted out of
@@ -15,6 +15,7 @@ import {
15
15
  keepFileAsWorkspaceRef,
16
16
  } from "../content-block-size.js";
17
17
  import { fileBlockToProviderText } from "../file-block-text.js";
18
+ import { unsignedThoughtSignatureFallback } from "../gemini-thought-signature.js";
18
19
  import { base64Source, resolveMediaReferences } from "../media-resolve.js";
19
20
  import { PROVIDER_CATALOG } from "../model-catalog.js";
20
21
  import { recordProviderRequestDiagnostics } from "../request-diagnostics.js";
@@ -45,13 +46,6 @@ import {
45
46
  const GEMINI_CONTEXT_OVERFLOW_TOKEN_PATTERNS =
46
47
  /token.?count.*exceeds|exceeds.*maximum.*tokens|prompt.?is.?too.?long|too.?many.?(?:input.?)?tokens|input.?too.?long|context.?length.?exceeded/i;
47
48
 
48
- const GEMINI_3_UNSIGNED_TOOL_CALL_THOUGHT_SIGNATURE =
49
- "context_engineering_is_the_way_to_go";
50
-
51
- function isGemini3Model(model: string): boolean {
52
- return model.startsWith("gemini-3") || model.startsWith("models/gemini-3");
53
- }
54
-
55
49
  const THINKING_LEVEL_BY_NAME: Record<ThinkingLevelName, ThinkingLevel> = {
56
50
  minimal: ThinkingLevel.MINIMAL,
57
51
  low: ThinkingLevel.LOW,
@@ -435,7 +429,8 @@ export class GeminiProvider implements Provider {
435
429
  const maxTokens = configObj?.max_tokens as number | undefined;
436
430
  const modelOverride = configObj?.model as string | undefined;
437
431
  const usageAttributionHeaders = configObj?.usageAttributionHeaders as
438
- Record<string, string> | undefined;
432
+ | Record<string, string>
433
+ | undefined;
439
434
  const activeModel = modelOverride ?? this.model;
440
435
  const thinkingConfig = geminiModelSupportsThinking(activeModel)
441
436
  ? buildThinkingConfig(
@@ -902,28 +897,19 @@ export class GeminiProvider implements Provider {
902
897
  parts: genai.Part[],
903
898
  model: string,
904
899
  ): void {
905
- if (!isGemini3Model(model)) {
906
- return;
907
- }
908
-
909
900
  const functionCallParts = parts.filter((part) => part.functionCall);
910
- if (functionCallParts.length === 0) {
911
- return;
912
- }
913
-
914
- const hasRealThoughtSignature = functionCallParts.some((part) =>
915
- Boolean(part.thoughtSignature),
901
+ const fallback = unsignedThoughtSignatureFallback(
902
+ functionCallParts.map((part) => part.thoughtSignature),
903
+ { model },
916
904
  );
917
- if (hasRealThoughtSignature) {
905
+ if (!fallback) {
918
906
  return;
919
907
  }
920
-
921
- const firstFunctionCallPart = functionCallParts[0];
908
+ const firstFunctionCallPart = functionCallParts[fallback.index];
922
909
  if (!firstFunctionCallPart) {
923
910
  return;
924
911
  }
925
- firstFunctionCallPart.thoughtSignature =
926
- GEMINI_3_UNSIGNED_TOOL_CALL_THOUGHT_SIGNATURE;
912
+ firstFunctionCallPart.thoughtSignature = fallback.signature;
927
913
  }
928
914
 
929
915
  private supportsGeminiInlineFile(mimeType: string): boolean {
@@ -0,0 +1,63 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ GEMINI_3_UNSIGNED_TOOL_CALL_THOUGHT_SIGNATURE,
5
+ isGemini3Model,
6
+ unsignedThoughtSignatureFallback,
7
+ } from "./gemini-thought-signature.js";
8
+
9
+ describe("isGemini3Model", () => {
10
+ test("matches native Gemini 3 ids", () => {
11
+ expect(isGemini3Model("gemini-3.7-flash")).toBe(true);
12
+ expect(isGemini3Model("models/gemini-3.1-pro-preview")).toBe(true);
13
+ });
14
+
15
+ test("matches OpenAI-compatible catalog prefixes", () => {
16
+ expect(isGemini3Model("google/gemini-3.7-flash")).toBe(true);
17
+ });
18
+
19
+ test("does not match Gemini 2.5 or unrelated ids", () => {
20
+ expect(isGemini3Model("gemini-2.5-flash")).toBe(false);
21
+ expect(isGemini3Model("gpt-5.2")).toBe(false);
22
+ });
23
+ });
24
+
25
+ describe("unsignedThoughtSignatureFallback", () => {
26
+ test("stamps the dummy on the first unsigned Gemini 3 call", () => {
27
+ expect(
28
+ unsignedThoughtSignatureFallback([undefined, undefined], {
29
+ model: "gemini-3.7-flash",
30
+ }),
31
+ ).toEqual({
32
+ index: 0,
33
+ signature: GEMINI_3_UNSIGNED_TOOL_CALL_THOUGHT_SIGNATURE,
34
+ });
35
+ });
36
+
37
+ test("does not stamp when any call already has a signature", () => {
38
+ expect(
39
+ unsignedThoughtSignatureFallback(["signed-thought-1", undefined], {
40
+ model: "gemini-3.7-flash",
41
+ }),
42
+ ).toBeUndefined();
43
+ });
44
+
45
+ test("does not stamp for non-Gemini-3 models when a model id is provided", () => {
46
+ expect(
47
+ unsignedThoughtSignatureFallback([undefined], { model: "gpt-5.2" }),
48
+ ).toBeUndefined();
49
+ });
50
+
51
+ test("stamps unsigned calls when no model gate is supplied", () => {
52
+ expect(unsignedThoughtSignatureFallback([undefined])).toEqual({
53
+ index: 0,
54
+ signature: GEMINI_3_UNSIGNED_TOOL_CALL_THOUGHT_SIGNATURE,
55
+ });
56
+ });
57
+
58
+ test("does not stamp an empty function-call list", () => {
59
+ expect(
60
+ unsignedThoughtSignatureFallback([], { model: "gemini-3.7-flash" }),
61
+ ).toBeUndefined();
62
+ });
63
+ });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Gemini 3.x requires a thought signature on the first function-call part of
3
+ * each step. When history has no captured signature (unsigned tool_use from an
4
+ * earlier provider, or a client-injected call), Google documents these dummy
5
+ * values to skip validation.
6
+ */
7
+ export const GEMINI_3_UNSIGNED_TOOL_CALL_THOUGHT_SIGNATURE =
8
+ "context_engineering_is_the_way_to_go";
9
+
10
+ /**
11
+ * True for Gemini 3.x model ids, including the native `models/` prefix and
12
+ * OpenAI-compatible catalog prefixes such as `google/gemini-3.7-flash`.
13
+ */
14
+ export function isGemini3Model(model: string): boolean {
15
+ if (model.startsWith("gemini-3") || model.startsWith("models/gemini-3")) {
16
+ return true;
17
+ }
18
+ return /\/gemini-3/.test(model);
19
+ }
20
+
21
+ /**
22
+ * Decide whether unsigned function-call history should receive Google's
23
+ * documented dummy thought signature. Returns the first call's index when
24
+ * every call is unsigned. Pass `model` to restrict this to Gemini 3.x;
25
+ * omit it for retry backfills that already observed a thought-signature 4xx.
26
+ */
27
+ export function unsignedThoughtSignatureFallback(
28
+ signatures: ReadonlyArray<string | undefined | null>,
29
+ options?: { model?: string },
30
+ ): { index: 0; signature: string } | undefined {
31
+ if (options?.model !== undefined && !isGemini3Model(options.model)) {
32
+ return undefined;
33
+ }
34
+ if (signatures.length === 0) {
35
+ return undefined;
36
+ }
37
+ if (signatures.some((signature) => Boolean(signature))) {
38
+ return undefined;
39
+ }
40
+ return {
41
+ index: 0,
42
+ signature: GEMINI_3_UNSIGNED_TOOL_CALL_THOUGHT_SIGNATURE,
43
+ };
44
+ }
@@ -1,5 +1,10 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
+ import {
4
+ OPENCODE_GO_BASE_URL,
5
+ OPENCODE_REQUEST_HEADER,
6
+ OPENCODE_SESSION_HEADER,
7
+ } from "../../opencode/client.js";
3
8
  import { testInferenceConnection } from "../endpoint-probe.js";
4
9
 
5
10
  // Keyless auth so the probe never touches the vault; the fetch stub records
@@ -11,11 +16,18 @@ const CONNECTION = {
11
16
  models: [{ id: "meta/llama-3.1-8b-instruct" }],
12
17
  };
13
18
 
14
- function stubFetch(status: number, calls: { url: string; body: unknown }[]) {
19
+ type ProbeCall = {
20
+ url: string;
21
+ body: unknown;
22
+ headers: Record<string, string>;
23
+ };
24
+
25
+ function stubFetch(status: number, calls: ProbeCall[]) {
15
26
  return (async (url: RequestInfo | URL, init?: RequestInit) => {
16
27
  calls.push({
17
28
  url: String(url),
18
29
  body: JSON.parse(String(init?.body)),
30
+ headers: { ...(init?.headers as Record<string, string>) },
19
31
  });
20
32
  return new Response(status < 400 ? "{}" : "404 page not found", { status });
21
33
  }) as typeof fetch;
@@ -23,7 +35,7 @@ function stubFetch(status: number, calls: { url: string; body: unknown }[]) {
23
35
 
24
36
  describe("testInferenceConnection", () => {
25
37
  test("reports ok:false with a base-path hint on 404", async () => {
26
- const calls: { url: string; body: unknown }[] = [];
38
+ const calls: ProbeCall[] = [];
27
39
  const result = await testInferenceConnection(
28
40
  CONNECTION,
29
41
  stubFetch(404, calls),
@@ -43,7 +55,7 @@ describe("testInferenceConnection", () => {
43
55
  });
44
56
 
45
57
  test("reports ok:true on 200 from a correct base URL", async () => {
46
- const calls: { url: string; body: unknown }[] = [];
58
+ const calls: ProbeCall[] = [];
47
59
  const result = await testInferenceConnection(
48
60
  { ...CONNECTION, baseUrl: "https://integrate.api.nvidia.com/v1" },
49
61
  stubFetch(200, calls),
@@ -75,6 +87,33 @@ describe("testInferenceConnection", () => {
75
87
  expect(result?.hint).toContain("connection refused");
76
88
  });
77
89
 
90
+ test("sends OpenCode session and request headers to an OpenCode endpoint", async () => {
91
+ const calls: ProbeCall[] = [];
92
+ const result = await testInferenceConnection(
93
+ {
94
+ ...CONNECTION,
95
+ provider: "opencode",
96
+ baseUrl: OPENCODE_GO_BASE_URL,
97
+ models: [{ id: "kimi-k3" }],
98
+ },
99
+ stubFetch(200, calls),
100
+ );
101
+
102
+ expect(result).toMatchObject({ ok: true, status: 200 });
103
+ const headers = calls[0].headers;
104
+ expect(headers[OPENCODE_SESSION_HEADER]).toMatch(/\S/);
105
+ expect(headers[OPENCODE_REQUEST_HEADER]).toMatch(/\S/);
106
+ expect(headers).not.toHaveProperty("session_id");
107
+ expect(calls[0].body).not.toHaveProperty(OPENCODE_SESSION_HEADER);
108
+ });
109
+
110
+ test("does not send OpenCode headers to other providers", async () => {
111
+ const calls: ProbeCall[] = [];
112
+ await testInferenceConnection(CONNECTION, stubFetch(200, calls));
113
+ expect(calls[0].headers).not.toHaveProperty(OPENCODE_SESSION_HEADER);
114
+ expect(calls[0].headers).not.toHaveProperty(OPENCODE_REQUEST_HEADER);
115
+ });
116
+
78
117
  test("skips when there is no base URL or no model to probe with", async () => {
79
118
  const neverFetch = (async () => {
80
119
  throw new Error("should not be called");
@@ -15,9 +15,8 @@ import { VELLUM_MANAGED_PROVIDER } from "../vellum-model-routing.js";
15
15
  * - platform: route via Vellum managed proxy; no client-side credential.
16
16
  * - none: no auth (e.g. Ollama running locally).
17
17
  * - oauth_subscription: OAuth-based subscription auth (e.g. ChatGPT Codex).
18
- *
19
- * Schema-accepted variants (runtime rejects with a clear "not yet shipped" error):
20
- * - service_account: service-account credentials (Vertex AI, Bedrock).
18
+ * - service_account: Google service-account JSON key (Vertex AI); signs a
19
+ * JWT and exchanges it for a bearer token at the embedded token_uri.
21
20
  */
22
21
  export const AuthSchema = z
23
22
  .discriminatedUnion("type", [
@@ -178,14 +178,33 @@ export async function computeConnectionAvailability(
178
178
  }
179
179
 
180
180
  switch (connection.auth.type) {
181
- // Schema-accepted but not dispatchable: `resolveAuth` returns
182
- // not_implemented for service_account, so a stored credential still
183
- // cannot serve inference.
184
- case "service_account":
185
- return {
186
- status: "unsupported_auth",
187
- message: `Connection "${resolvedConnectionName}" uses service-account auth, which inference does not support yet. Pick a connection with a different auth type.`,
188
- };
181
+ case "service_account": {
182
+ const result = await getSecureKeyResultAsync(connection.auth.credential);
183
+ if (result.unreachable) {
184
+ return {
185
+ status: "unknown",
186
+ message: `The credential store is unreachable, so the credential for connection "${resolvedConnectionName}" could not be verified. Try again shortly.`,
187
+ };
188
+ }
189
+ if (result.value == null) {
190
+ return {
191
+ status: "missing_credential",
192
+ message: `Connection "${resolvedConnectionName}" has no service-account credential stored. Add one ${SETTINGS_HINT}.`,
193
+ };
194
+ }
195
+ // Validate the JSON payload before reporting ok: a stored value that is
196
+ // not a well-formed service-account object will be rejected by dispatch,
197
+ // so surface the problem here rather than letting a turn fail silently.
198
+ const { parseServiceAccountKey } =
199
+ await import("./service-account-token.js");
200
+ if (!parseServiceAccountKey(result.value)) {
201
+ return {
202
+ status: "missing_credential",
203
+ message: `Connection "${resolvedConnectionName}" has an invalid service-account credential (expected JSON with client_email, private_key, and token_uri). Update it ${SETTINGS_HINT}.`,
204
+ };
205
+ }
206
+ return { status: "ok" };
207
+ }
189
208
  case "api_key":
190
209
  case "oauth_subscription": {
191
210
  const result = await getSecureKeyResultAsync(connection.auth.credential);
@@ -10,6 +10,7 @@
10
10
  import { z } from "zod";
11
11
 
12
12
  import { getLogger } from "../../util/logger.js";
13
+ import { resolveOpenCodeRequestHeaders } from "../opencode/client.js";
13
14
  import type { Auth, ConnectionModel } from "./auth.js";
14
15
  import { resolveAuth } from "./resolve-auth.js";
15
16
 
@@ -72,12 +73,20 @@ export async function testInferenceConnection(
72
73
  }
73
74
  const authHeaders =
74
75
  resolved.resolved.kind === "header" ? resolved.resolved.headers : {};
76
+ // OpenCode Go rejects requests without a session header, which would
77
+ // report a false HTTP 400 hint for a correctly configured connection.
78
+ const providerHeaders =
79
+ connection.provider === "opencode" ? resolveOpenCodeRequestHeaders() : {};
75
80
 
76
81
  const url = `${connection.baseUrl.replace(/\/+$/, "")}/chat/completions`;
77
82
  try {
78
83
  const res = await fetchImpl(url, {
79
84
  method: "POST",
80
- headers: { ...authHeaders, "Content-Type": "application/json" },
85
+ headers: {
86
+ ...authHeaders,
87
+ ...providerHeaders,
88
+ "Content-Type": "application/json",
89
+ },
81
90
  body: JSON.stringify({
82
91
  model,
83
92
  messages: [{ role: "user", content: "ping" }],