@vellumai/assistant 0.12.1 → 0.12.2-staging.1

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 (548) 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/openapi.yaml +327 -52
  6. package/package.json +3 -1
  7. package/scripts/smoke-desktop-window-theme.ts +186 -0
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  9. package/src/__tests__/anthropic-provider.test.ts +0 -63
  10. package/src/__tests__/app-compiler.test.ts +6 -0
  11. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  13. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  14. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  15. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  16. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  17. package/src/__tests__/computer-use-tools.test.ts +19 -7
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +28 -29
  20. package/src/__tests__/config-watcher.test.ts +23 -8
  21. package/src/__tests__/connection-model-compat.test.ts +2 -2
  22. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  23. package/src/__tests__/conversation-pairing.test.ts +26 -0
  24. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  25. package/src/__tests__/conversation-queue.test.ts +48 -207
  26. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  27. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  28. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  29. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  30. package/src/__tests__/conversation-title-service.test.ts +14 -10
  31. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  32. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  33. package/src/__tests__/credential-record-retire.test.ts +171 -1
  34. package/src/__tests__/credential-routes.test.ts +212 -3
  35. package/src/__tests__/credential-security-invariants.test.ts +4 -2
  36. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  37. package/src/__tests__/document-sync-tags.test.ts +25 -0
  38. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  39. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  40. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  41. package/src/__tests__/git-porcelain.test.ts +47 -0
  42. package/src/__tests__/heartbeat-service.test.ts +56 -40
  43. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  44. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  45. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  46. package/src/__tests__/host-cu-proxy.test.ts +539 -0
  47. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  48. package/src/__tests__/host-file-proxy.test.ts +11 -23
  49. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  50. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  51. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  52. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  53. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  54. package/src/__tests__/mcp-health-check.test.ts +8 -11
  55. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  56. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  57. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  58. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  59. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  60. package/src/__tests__/model-intents.test.ts +3 -0
  61. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  62. package/src/__tests__/oauth-commands-routes.test.ts +113 -0
  63. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  64. package/src/__tests__/openai-provider.test.ts +1 -0
  65. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  66. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  67. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  68. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  69. package/src/__tests__/qdrant-manager.test.ts +132 -0
  70. package/src/__tests__/resolve-app.test.ts +135 -0
  71. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  72. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  73. package/src/__tests__/service-account-token.test.ts +247 -0
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  75. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  76. package/src/__tests__/skills.test.ts +9 -4
  77. package/src/__tests__/source-fingerprint.test.ts +184 -0
  78. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  79. package/src/__tests__/subagent-tools.test.ts +3 -2
  80. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  81. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  82. package/src/__tests__/unicode.test.ts +13 -0
  83. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  84. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  85. package/src/__tests__/workspace-git-service.test.ts +26 -0
  86. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  87. package/src/acp/__tests__/agent-process.test.ts +258 -4
  88. package/src/acp/__tests__/auth-required.test.ts +96 -0
  89. package/src/acp/__tests__/client-handler.test.ts +90 -2
  90. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  91. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  92. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  93. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  94. package/src/acp/__tests__/session-manager.test.ts +895 -4
  95. package/src/acp/agent-process.test.ts +4 -1
  96. package/src/acp/agent-process.ts +96 -10
  97. package/src/acp/auth-required.ts +50 -1
  98. package/src/acp/auto-install.test.ts +9 -7
  99. package/src/acp/client-handler.ts +16 -3
  100. package/src/acp/model-config.test.ts +166 -0
  101. package/src/acp/model-config.ts +138 -0
  102. package/src/acp/resolve-agent.test.ts +220 -6
  103. package/src/acp/resolve-agent.ts +88 -23
  104. package/src/acp/session-manager.test.ts +196 -3
  105. package/src/acp/session-manager.ts +441 -18
  106. package/src/acp/types.ts +50 -5
  107. package/src/agent/loop.ts +1 -0
  108. package/src/api/events/acp-session-model-update.test.ts +88 -0
  109. package/src/api/events/acp-session-model-update.ts +46 -0
  110. package/src/api/events/app-files-changed.ts +4 -3
  111. package/src/api/events/notification-intent.ts +2 -0
  112. package/src/api/events/user-message-echo.ts +7 -1
  113. package/src/api/index.ts +6 -0
  114. package/src/api/responses/conversation-message.ts +5 -0
  115. package/src/apps/app-store.ts +1 -1
  116. package/src/apps/resolve-app.ts +81 -0
  117. package/src/apps/source-fingerprint.ts +266 -0
  118. package/src/avatar/identity-avatar.ts +7 -2
  119. package/src/bundler/app-compiler.ts +15 -0
  120. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  121. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  122. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  123. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  124. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  125. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  126. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  127. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  128. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  129. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  130. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  131. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  132. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  133. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  134. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  135. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  136. package/src/calls/barge-in-guard.ts +118 -0
  137. package/src/calls/call-constants.ts +24 -4
  138. package/src/calls/call-controller.ts +499 -199
  139. package/src/calls/call-transport.ts +16 -0
  140. package/src/calls/media-stream-output.ts +32 -0
  141. package/src/calls/media-stream-server.ts +82 -30
  142. package/src/calls/media-stream-stt-session.ts +15 -0
  143. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  144. package/src/calls/resolve-call-tts-provider.ts +33 -2
  145. package/src/calls/tts-call-strategy.ts +9 -5
  146. package/src/calls/types.ts +2 -0
  147. package/src/calls/voice-control-protocol.ts +53 -21
  148. package/src/calls/voice-leg-coordinator.ts +189 -0
  149. package/src/calls/voice-progress-cadence.ts +423 -0
  150. package/src/calls/voice-session-bridge.ts +59 -46
  151. package/src/calls/voice-triage-escalate.ts +175 -56
  152. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  153. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  154. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  155. package/src/cli/commands/apps.help.ts +67 -13
  156. package/src/cli/commands/apps.ts +183 -0
  157. package/src/cli/commands/bash.help.ts +8 -13
  158. package/src/cli/commands/bash.ts +7 -25
  159. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  160. package/src/cli/commands/channels/index.help.ts +70 -2
  161. package/src/cli/commands/channels/index.ts +16 -3
  162. package/src/cli/commands/channels/send.ts +128 -0
  163. package/src/cli/commands/credentials.help.ts +6 -2
  164. package/src/cli/commands/credentials.ts +2 -2
  165. package/src/cli/commands/mcp.help.ts +8 -8
  166. package/src/cli/commands/mcp.ts +1 -1
  167. package/src/cli/commands/roadmap.help.ts +5 -1
  168. package/src/cli/commands/roadmap.ts +1 -1
  169. package/src/cli/lib/bundled-marketplace.json +103 -90
  170. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  171. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  172. package/src/config/__tests__/platform-identity.test.ts +255 -0
  173. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  174. package/src/config/acp-agent-write.ts +43 -0
  175. package/src/config/acp-defaults.test.ts +13 -3
  176. package/src/config/acp-defaults.ts +23 -6
  177. package/src/config/acp-schema.test.ts +81 -0
  178. package/src/config/acp-schema.ts +30 -2
  179. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  180. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  181. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  182. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  183. package/src/config/bundled-skills/computer-use/SKILL.md +20 -2
  184. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  185. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  186. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  187. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  188. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  189. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  190. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  191. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  192. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  193. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  194. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  195. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  196. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  197. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  198. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  199. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  200. package/src/config/default-profile-catalog.ts +1 -1
  201. package/src/config/env.ts +2 -2
  202. package/src/config/feature-flag-registry.json +24 -12
  203. package/src/config/loader.ts +1 -0
  204. package/src/config/memory-v3-gate.test.ts +31 -0
  205. package/src/config/memory-v3-gate.ts +20 -16
  206. package/src/config/platform-identity.ts +201 -0
  207. package/src/config/platform-rehydration.ts +11 -27
  208. package/src/config/schema.ts +2 -2
  209. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  210. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  211. package/src/config/schemas/calls.ts +1 -28
  212. package/src/config/schemas/live-voice.ts +1 -173
  213. package/src/config/schemas/mcp.ts +1 -1
  214. package/src/config/schemas/memory-retrospective.ts +10 -0
  215. package/src/config/schemas/services.ts +8 -0
  216. package/src/config/schemas/skills.ts +0 -14
  217. package/src/config/schemas/voice.ts +150 -0
  218. package/src/credential-execution/process-manager.test.ts +18 -19
  219. package/src/credential-health/credential-health-service.ts +11 -17
  220. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  221. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  222. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  223. package/src/daemon/app-change-notify.ts +45 -0
  224. package/src/daemon/config-watcher.ts +13 -194
  225. package/src/daemon/conversation-error.ts +3 -2
  226. package/src/daemon/conversation-media-retry.ts +2 -1
  227. package/src/daemon/conversation-process.ts +35 -64
  228. package/src/daemon/conversation-runtime-assembly.ts +2 -1
  229. package/src/daemon/conversation-skill-tools.ts +13 -31
  230. package/src/daemon/conversation-store.ts +1 -1
  231. package/src/daemon/conversation-surfaces.ts +15 -9
  232. package/src/daemon/conversation-tool-setup.ts +37 -43
  233. package/src/daemon/conversation.ts +0 -3
  234. package/src/daemon/dictation-profile-store.ts +2 -1
  235. package/src/daemon/handlers/shared.ts +2 -1
  236. package/src/daemon/handlers/skills.ts +6 -3
  237. package/src/daemon/host-bash-proxy.ts +8 -12
  238. package/src/daemon/host-browser-proxy.ts +15 -1
  239. package/src/daemon/host-cu-proxy.ts +186 -19
  240. package/src/daemon/host-file-proxy.ts +8 -10
  241. package/src/daemon/host-proxy-preactivation.ts +2 -1
  242. package/src/daemon/host-transfer-proxy.ts +14 -18
  243. package/src/daemon/lifecycle.ts +0 -5
  244. package/src/daemon/mcp-reload-service.ts +5 -5
  245. package/src/daemon/message-types/sync.ts +1 -0
  246. package/src/daemon/orphan-reaper.ts +1 -1
  247. package/src/daemon/providers-setup.ts +1 -1
  248. package/src/daemon/reaction-history-render.ts +2 -1
  249. package/src/daemon/shutdown-handlers.ts +1 -9
  250. package/src/daemon/skill-memory-refresh.ts +64 -0
  251. package/src/daemon/tool-side-effects.ts +7 -18
  252. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  253. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  254. package/src/desktop/desktop-chrome-policy.ts +47 -0
  255. package/src/desktop/desktop-chrome-session.ts +37 -0
  256. package/src/desktop/desktop-dependencies.test.ts +102 -0
  257. package/src/desktop/desktop-dependencies.ts +308 -0
  258. package/src/desktop/desktop-feature.test.ts +18 -0
  259. package/src/desktop/desktop-feature.ts +19 -0
  260. package/src/desktop/desktop-panel-config.test.ts +135 -0
  261. package/src/desktop/desktop-panel-config.ts +149 -0
  262. package/src/desktop/desktop-session-manager.test.ts +730 -0
  263. package/src/desktop/desktop-session-manager.ts +850 -0
  264. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  265. package/src/desktop/desktop-stream-bridge.ts +214 -0
  266. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  267. package/src/desktop/desktop-wallpaper.ts +113 -0
  268. package/src/desktop/desktop-window-drag.ts +177 -0
  269. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  270. package/src/desktop/desktop-window-manager-config.ts +238 -0
  271. package/src/desktop/desktop-window-theme.ts +170 -0
  272. package/src/heartbeat/heartbeat-service.ts +21 -3
  273. package/src/home/__tests__/feed-writer.test.ts +22 -0
  274. package/src/home/feed-writer.ts +11 -2
  275. package/src/hooks/types.ts +33 -0
  276. package/src/i18n/AGENTS.md +22 -0
  277. package/src/i18n/__tests__/i18n.test.ts +108 -0
  278. package/src/i18n/index.ts +16 -0
  279. package/src/i18n/locales.ts +93 -0
  280. package/src/i18n/messages.ts +61 -0
  281. package/src/i18n/title.ts +25 -0
  282. package/src/inbound/platform-callback-registration.ts +4 -5
  283. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  284. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  285. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  286. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  287. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  288. package/src/live-voice/live-voice-photo.ts +25 -6
  289. package/src/live-voice/live-voice-session.ts +213 -693
  290. package/src/live-voice/protocol.ts +0 -2
  291. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  292. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  293. package/src/mcp/effective-config.ts +3 -3
  294. package/src/mcp/mcp-header-store.ts +29 -45
  295. package/src/mcp/spec-schema.ts +111 -0
  296. package/src/mcp/startup.ts +8 -4
  297. package/src/mcp/workspace-mcp-config.ts +188 -0
  298. package/src/messaging/provider.ts +10 -3
  299. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  300. package/src/messaging/providers/callback-routing.ts +18 -0
  301. package/src/messaging/providers/channel-transport.ts +56 -0
  302. package/src/messaging/providers/discord/transport.ts +17 -0
  303. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  304. package/src/messaging/providers/slack/adapter.ts +7 -44
  305. package/src/messaging/providers/slack/transport.ts +16 -0
  306. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  307. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  308. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  309. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  310. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  311. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  312. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  313. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  314. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  315. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  316. package/src/notifications/adapters/macos.ts +3 -0
  317. package/src/notifications/assistant-reply-producer.ts +8 -3
  318. package/src/notifications/conversation-pairing.ts +36 -16
  319. package/src/notifications/delivered-post-record.ts +11 -1
  320. package/src/notifications/guardian-feed-projection.ts +125 -5
  321. package/src/notifications/home-feed-side-effect.ts +55 -7
  322. package/src/notifications/schedule-result-producer.ts +66 -22
  323. package/src/oauth/AGENTS.md +2 -0
  324. package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
  325. package/src/oauth/byo-connection.test.ts +105 -0
  326. package/src/oauth/byo-connection.ts +36 -6
  327. package/src/oauth/connection-resolver.test.ts +73 -0
  328. package/src/oauth/connection-resolver.ts +95 -2
  329. package/src/oauth/scope-utils.ts +24 -0
  330. package/src/oauth/seed-providers.ts +204 -25
  331. package/src/outbound-proxy/certs.ts +3 -5
  332. package/src/permissions/checker.test.ts +11 -6
  333. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  334. package/src/persistence/conversation-crud.ts +71 -63
  335. package/src/persistence/conversation-lineage.ts +12 -4
  336. package/src/persistence/conversation-queries.ts +2 -2
  337. package/src/persistence/conversation-title-placeholders.ts +23 -0
  338. package/src/persistence/conversation-title-service.ts +9 -26
  339. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  340. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  341. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  342. package/src/persistence/message-cursor.ts +78 -0
  343. package/src/persistence/message-reads.ts +30 -0
  344. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  345. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  346. package/src/persistence/schema/memory-core.ts +5 -0
  347. package/src/platform/client.test.ts +4 -2
  348. package/src/platform/client.ts +2 -10
  349. package/src/plugin-api/constants.ts +2 -0
  350. package/src/plugin-api/index.ts +1 -0
  351. package/src/plugin-api/types.ts +2 -0
  352. package/src/plugins/defaults/index.ts +2 -0
  353. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  354. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  355. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  356. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  357. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  358. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  359. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  360. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  361. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  362. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  363. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  364. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  365. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  366. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  367. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  368. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  369. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  370. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  371. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  372. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  373. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  374. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  375. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  376. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  377. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  378. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  379. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  380. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  381. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  382. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  383. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  384. package/src/plugins/defaults/memory/startup.ts +28 -2
  385. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  386. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  387. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  388. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  389. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  390. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  391. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  392. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  393. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  394. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  395. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  396. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  397. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  398. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  399. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  400. package/src/plugins/defaults/memory/worker.ts +6 -0
  401. package/src/plugins/mcp-servers.ts +15 -75
  402. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  403. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  404. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  405. package/src/providers/anthropic/client.ts +0 -47
  406. package/src/providers/gemini/client.ts +9 -23
  407. package/src/providers/gemini-thought-signature.test.ts +63 -0
  408. package/src/providers/gemini-thought-signature.ts +44 -0
  409. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  410. package/src/providers/inference/auth.ts +2 -3
  411. package/src/providers/inference/connection-availability.ts +27 -8
  412. package/src/providers/inference/endpoint-probe.ts +10 -1
  413. package/src/providers/inference/resolve-auth.ts +22 -4
  414. package/src/providers/inference/service-account-token.ts +248 -0
  415. package/src/providers/model-catalog.ts +34 -0
  416. package/src/providers/model-intents.ts +10 -5
  417. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  418. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  419. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  420. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  421. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  422. package/src/providers/openai/chat-completions-provider.ts +212 -79
  423. package/src/providers/openai/codex-models.ts +0 -4
  424. package/src/providers/openai/google-thought-signature.ts +320 -0
  425. package/src/providers/opencode/client.test.ts +73 -4
  426. package/src/providers/opencode/client.ts +60 -3
  427. package/src/providers/openrouter/client.ts +20 -0
  428. package/src/providers/outbound-request-sanitize.ts +96 -0
  429. package/src/providers/retry.ts +30 -19
  430. package/src/providers/unparseable-tool-args.ts +5 -1
  431. package/src/runtime/AGENTS.md +3 -1
  432. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  433. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  434. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  435. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  436. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  437. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  438. package/src/runtime/agent-wake.ts +2 -1
  439. package/src/runtime/assistant-event-hub.ts +21 -3
  440. package/src/runtime/auth/same-actor.ts +38 -3
  441. package/src/runtime/channel-send.ts +316 -0
  442. package/src/runtime/host-observe.ts +11 -1
  443. package/src/runtime/http-server.ts +160 -151
  444. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  445. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  446. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  447. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  448. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  449. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  450. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  451. package/src/runtime/routes/acp-routes.test.ts +15 -0
  452. package/src/runtime/routes/acp-routes.ts +72 -9
  453. package/src/runtime/routes/app-management-routes.ts +73 -0
  454. package/src/runtime/routes/app-routes.ts +1 -1
  455. package/src/runtime/routes/channel-send-routes.ts +85 -0
  456. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  457. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  458. package/src/runtime/routes/conversation-routes.ts +16 -3
  459. package/src/runtime/routes/credential-routes.ts +19 -10
  460. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  461. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  462. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  463. package/src/runtime/routes/events-routes.ts +4 -0
  464. package/src/runtime/routes/host-cu-routes.ts +6 -0
  465. package/src/runtime/routes/index.ts +4 -2
  466. package/src/runtime/routes/log-export-routes.ts +2 -0
  467. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  468. package/src/runtime/routes/migration-routes.ts +6 -20
  469. package/src/runtime/routes/oauth-commands-routes.ts +36 -0
  470. package/src/runtime/routes/platform-routes.ts +18 -15
  471. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  472. package/src/runtime/routes/workspace-routes.ts +46 -63
  473. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  474. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  475. package/src/runtime/routes/workspace-utils.ts +17 -0
  476. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  477. package/src/runtime/services/conversation-serializer.ts +16 -3
  478. package/src/runtime/sync/resource-sync-events.ts +2 -1
  479. package/src/schedule/run-script.ts +2 -1
  480. package/src/schedule/schedule-store.ts +9 -2
  481. package/src/schedule/worker.ts +1 -1
  482. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  483. package/src/security/ces-http-record-backend.ts +120 -0
  484. package/src/security/ces-rpc-record-backend.ts +7 -0
  485. package/src/security/untrusted-content.ts +3 -1
  486. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  487. package/src/skills/workspace-skill-md-signature.ts +50 -0
  488. package/src/subagent/consult-context.ts +12 -25
  489. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  490. package/src/tools/acp/list-agents.test.ts +3 -1
  491. package/src/tools/acp/spawn.test.ts +146 -11
  492. package/src/tools/acp/spawn.ts +21 -3
  493. package/src/tools/acp/status.test.ts +94 -0
  494. package/src/tools/acp/status.ts +40 -3
  495. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  496. package/src/tools/browser/browser-execution.ts +1 -1
  497. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  498. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  499. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  500. package/src/tools/browser/network-recorder.ts +2 -1
  501. package/src/tools/capability-offer.test.ts +22 -2
  502. package/src/tools/capability-offer.ts +16 -1
  503. package/src/tools/computer-use/definitions.ts +121 -2
  504. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  505. package/src/tools/credentials/metadata-store.ts +100 -0
  506. package/src/tools/filesystem/search.ts +2 -1
  507. package/src/tools/host-filesystem/edit.test.ts +2 -1
  508. package/src/tools/host-filesystem/edit.ts +2 -2
  509. package/src/tools/host-filesystem/read.test.ts +2 -1
  510. package/src/tools/host-filesystem/read.ts +2 -2
  511. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  512. package/src/tools/host-filesystem/transfer.ts +2 -2
  513. package/src/tools/host-filesystem/write.test.ts +2 -1
  514. package/src/tools/host-filesystem/write.ts +2 -2
  515. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  516. package/src/tools/host-terminal/host-shell.ts +4 -20
  517. package/src/tools/network/web-search.ts +6 -1
  518. package/src/tools/policy-context.ts +6 -7
  519. package/src/tools/skills/scaffold-managed.ts +61 -3
  520. package/src/tools/subagent/spawn.ts +5 -8
  521. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  522. package/src/tools/terminal/sanitized-bash.ts +105 -0
  523. package/src/tools/watch/watch-retro-report.ts +0 -8
  524. package/src/types/host-capabilities.ts +5 -3
  525. package/src/util/__tests__/language-subtag.test.ts +23 -1
  526. package/src/util/ca-bundle.ts +13 -0
  527. package/src/util/language-subtag.ts +16 -0
  528. package/src/util/platform.ts +8 -0
  529. package/src/util/unicode.ts +15 -1
  530. package/src/watch/watch-timeline.ts +2 -2
  531. package/src/workflows/run-manager.ts +2 -1
  532. package/src/workspace/git-porcelain.ts +66 -0
  533. package/src/workspace/git-service.ts +6 -55
  534. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  535. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  536. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  537. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  538. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  539. package/src/workspace/migrations/registry.ts +6 -0
  540. package/src/__tests__/app-source-watcher.test.ts +0 -218
  541. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  542. package/src/__tests__/speaker-identification.test.ts +0 -56
  543. package/src/calls/speaker-identification.ts +0 -233
  544. package/src/daemon/app-source-watcher.ts +0 -218
  545. package/src/messaging/providers/whatsapp/client.ts +0 -29
  546. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  547. package/src/util/fs-watcher-error.ts +0 -36
  548. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -1,6 +1,10 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { baseLanguageSubtag, localizedOrDefault } from "../language-subtag.js";
3
+ import {
4
+ baseLanguageSubtag,
5
+ fixedPhraseLanguage,
6
+ localizedOrDefault,
7
+ } from "../language-subtag.js";
4
8
 
5
9
  describe("baseLanguageSubtag", () => {
6
10
  test("lowercases a bare tag", () => {
@@ -52,3 +56,21 @@ describe("localizedOrDefault", () => {
52
56
  expect(localizedOrDefault(table, "__proto__", "fallback")).toBe("fallback");
53
57
  });
54
58
  });
59
+
60
+ describe("fixedPhraseLanguage", () => {
61
+ const table = { en: "one", es: "uno" };
62
+
63
+ test("pins English when the speaker's language has no table entry", () => {
64
+ expect(fixedPhraseLanguage(table, "ko")).toBe("en");
65
+ expect(fixedPhraseLanguage(table, "ar-EG")).toBe("en");
66
+ });
67
+
68
+ test("rides the speaker's language when the table covers it", () => {
69
+ expect(fixedPhraseLanguage(table, "es")).toBeUndefined();
70
+ expect(fixedPhraseLanguage(table, "es-419")).toBeUndefined();
71
+ });
72
+
73
+ test("carries no hint when the language is unknown", () => {
74
+ expect(fixedPhraseLanguage(table, undefined)).toBeUndefined();
75
+ });
76
+ });
@@ -0,0 +1,13 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+
3
+ export async function writeCombinedCABundle(
4
+ systemBundlePath: string,
5
+ extraCAPath: string,
6
+ outputPath: string,
7
+ ): Promise<void> {
8
+ const [systemCAs, extraCAs] = await Promise.all([
9
+ readFile(systemBundlePath, "utf-8"),
10
+ readFile(extraCAPath, "utf-8"),
11
+ ]);
12
+ await writeFile(outputPath, `${systemCAs}\n${extraCAs}`, { mode: 0o644 });
13
+ }
@@ -41,3 +41,19 @@ export function localizedOrDefault<T>(
41
41
  ? (table[baseLanguageSubtag(language)!] as T)
42
42
  : fallback;
43
43
  }
44
+
45
+ /**
46
+ * The TTS language hint for a fixed phrase picked from a localized table:
47
+ * "en" when the speaker has a language the table does not cover (the
48
+ * picker fell back to English text, which must not be synthesized under
49
+ * an ar/ko/ta hint), undefined otherwise (the phrase rides the speaker's
50
+ * language, or carries no hint at all when the language is unknown).
51
+ */
52
+ export function fixedPhraseLanguage(
53
+ table: Readonly<Record<string, unknown>>,
54
+ language: string | undefined,
55
+ ): string | undefined {
56
+ return language !== undefined && !hasLocalizedEntry(table, language)
57
+ ? "en"
58
+ : undefined;
59
+ }
@@ -470,6 +470,14 @@ export function getWorkspaceConfigPath(): string {
470
470
  return join(getWorkspaceDir(), "config.json");
471
471
  }
472
472
 
473
+ /** Filename for the user-owned Agent Plugins MCP document. */
474
+ export const WORKSPACE_MCP_FILENAME = "mcp.json";
475
+
476
+ /** Returns $VELLUM_WORKSPACE_DIR/mcp.json */
477
+ export function getWorkspaceMcpConfigPath(): string {
478
+ return join(getWorkspaceDir(), WORKSPACE_MCP_FILENAME);
479
+ }
480
+
473
481
  /** Returns $VELLUM_WORKSPACE_DIR/skills */
474
482
  export function getWorkspaceSkillsDir(): string {
475
483
  return join(getWorkspaceDir(), "skills");
@@ -126,6 +126,15 @@ export interface DeepSanitizeResult<T> {
126
126
  fixedStringCount: number;
127
127
  }
128
128
 
129
+ export interface DeepSanitizeOptions {
130
+ /**
131
+ * Skip a plain-object property entirely: its value is neither scanned nor
132
+ * copied. For fields known to hold ASCII (base64 payloads), scanning is
133
+ * pure cost.
134
+ */
135
+ skipKey?: (key: string, parent: Record<string, unknown>) => boolean;
136
+ }
137
+
129
138
  /**
130
139
  * Recursively walk arrays and plain objects, replacing orphaned surrogates in
131
140
  * every string value. Non-plain objects (class instances, Date, Buffer, Map,
@@ -136,8 +145,10 @@ export interface DeepSanitizeResult<T> {
136
145
  */
137
146
  export function stripOrphanedSurrogatesDeep<T>(
138
147
  input: T,
148
+ options: DeepSanitizeOptions = {},
139
149
  ): DeepSanitizeResult<T> {
140
150
  let fixedStringCount = 0;
151
+ const { skipKey } = options;
141
152
 
142
153
  const walk = (value: unknown): { value: unknown; changed: boolean } => {
143
154
  if (typeof value === "string") {
@@ -178,7 +189,10 @@ export function stripOrphanedSurrogatesDeep<T>(
178
189
  let next: Record<string, unknown> | null = null;
179
190
  for (let i = 0; i < keys.length; i++) {
180
191
  const key = keys[i]!;
181
- const result = walk(source[key]);
192
+ const result =
193
+ skipKey?.(key, source) === true
194
+ ? { value: source[key], changed: false }
195
+ : walk(source[key]);
182
196
  if (result.changed && next === null) {
183
197
  next = {};
184
198
  for (let j = 0; j < i; j++) {
@@ -366,8 +366,8 @@ export function appendNarration(
366
366
  * timeline of a session where observation stalled is simply a sparser one.
367
367
  *
368
368
  * The screenshot is stored only when `attachScreenshot` asks for it, and
369
- * carrying one is not asking. The host captures a screenshot on every observe
370
- * with no opt-out, so an observation always has pixels available and a policy
369
+ * carrying one is not asking. Watch requests a screenshot on every observe, so
370
+ * an observation normally has pixels available and a policy
371
371
  * of "store what arrives" is a policy of storing every frame. Which frames are
372
372
  * worth an image is a cadence decision, and it belongs to the caller driving
373
373
  * the session rather than to the row writer.
@@ -33,6 +33,7 @@ import type { TrustContext } from "../daemon/trust-context-types.js";
33
33
  import { wakeAgentForOpportunity } from "../runtime/agent-wake.js";
34
34
  import { broadcastMessage } from "../runtime/assistant-event-hub.js";
35
35
  import { getLogger } from "../util/logger.js";
36
+ import { safeStringSlice } from "../util/unicode.js";
36
37
  import type { CapabilityManifest } from "./capabilities.js";
37
38
  import {
38
39
  normalizeCapabilityManifest,
@@ -611,7 +612,7 @@ function truncateForSummary(s: string, runId: string): string {
611
612
  }
612
613
  const omitted = s.length - MAX_SUMMARY_RESULT_CHARS;
613
614
  return (
614
- `${s.slice(0, MAX_SUMMARY_RESULT_CHARS)}… ` +
615
+ `${safeStringSlice(s, 0, MAX_SUMMARY_RESULT_CHARS)}… ` +
615
616
  `[truncated ${omitted} chars — fetch the full result with manage_workflows ` +
616
617
  `{ action: "get_result", run_id: "${runId}" }]`
617
618
  );
@@ -0,0 +1,66 @@
1
+ /**
2
+ * NUL-terminated `git status --porcelain -z` parsing.
3
+ *
4
+ * Status reporting and batched staging share this parser so rename/copy
5
+ * origin paths cannot drift between the two readers.
6
+ */
7
+
8
+ export interface PorcelainEntry {
9
+ /** Two-character XY status from porcelain v1. */
10
+ status: string;
11
+ /** Path git reports for this record (the destination for rename/copy). */
12
+ path: string;
13
+ /** Pre-rename/copy path. Present when the record is a rename or copy. */
14
+ origin?: string;
15
+ }
16
+
17
+ /**
18
+ * Parse NUL-terminated `git status --porcelain -z` output into status/path
19
+ * records. NUL termination is required so paths with special characters
20
+ * (non-ASCII, quotes, newlines) arrive verbatim instead of C-style quoted.
21
+ *
22
+ * A rename/copy record is followed by a bare origin-path entry, captured on
23
+ * {@link PorcelainEntry.origin}.
24
+ */
25
+ export function parsePorcelainZ(stdout: string): PorcelainEntry[] {
26
+ const entries = stdout.split("\0");
27
+ const parsed: PorcelainEntry[] = [];
28
+ for (let i = 0; i < entries.length; i++) {
29
+ const entry = entries[i] ?? "";
30
+ if (entry.length < 4) {
31
+ continue;
32
+ }
33
+ const status = entry.substring(0, 2);
34
+ const path = entry.substring(3);
35
+ if (status[0] === "R" || status[0] === "C") {
36
+ i++;
37
+ const origin = entries[i];
38
+ if (origin && origin.length > 0) {
39
+ parsed.push({ status, path, origin });
40
+ continue;
41
+ }
42
+ }
43
+ parsed.push({ status, path });
44
+ }
45
+ return parsed;
46
+ }
47
+
48
+ /**
49
+ * Every path git status knows about, including rename/copy origins.
50
+ * Origins still need to be staged so a deletion is not left behind when a
51
+ * rename is split across add batches.
52
+ */
53
+ export function collectDirtyPathsFromPorcelain(stdout: string): string[] {
54
+ const paths: string[] = [];
55
+ const seen = new Set<string>();
56
+ for (const entry of parsePorcelainZ(stdout)) {
57
+ for (const path of [entry.path, entry.origin]) {
58
+ if (!path || seen.has(path)) {
59
+ continue;
60
+ }
61
+ seen.add(path);
62
+ paths.push(path);
63
+ }
64
+ }
65
+ return paths;
66
+ }
@@ -17,6 +17,10 @@ import { getLogger } from "../util/logger.js";
17
17
  import { Mutex } from "../util/mutex.js";
18
18
  import { addToPathEnv, getExtraToolPathDirs } from "../util/platform.js";
19
19
  import { PromiseGuard } from "../util/promise-guard.js";
20
+ import {
21
+ collectDirtyPathsFromPorcelain,
22
+ parsePorcelainZ,
23
+ } from "./git-porcelain.js";
20
24
 
21
25
  const execFileAsync = promisify(execFile);
22
26
  const log = getLogger("workspace-git");
@@ -51,7 +55,7 @@ function cleanGitEnv(workspaceDir: string): Record<string, string> {
51
55
  * Patterns excluded from workspace git tracking.
52
56
  * These are written to .gitignore on init and appended to existing .gitignore files.
53
57
  */
54
- const WORKSPACE_GITIGNORE_RULES = [
58
+ export const WORKSPACE_GITIGNORE_RULES = [
55
59
  // Runtime state directories
56
60
  "data/db/",
57
61
  "data/qdrant/",
@@ -226,59 +230,6 @@ done
226
230
  exit 0
227
231
  `;
228
232
 
229
- /**
230
- * Parse NUL-terminated `git status --porcelain -z` output into status/path
231
- * pairs. NUL termination is required so paths with special characters
232
- * (non-ASCII, quotes, newlines) arrive verbatim instead of C-style quoted.
233
- * A rename/copy record is followed by a bare origin-path entry, which is
234
- * skipped.
235
- */
236
- function parsePorcelainZ(
237
- stdout: string,
238
- ): Array<{ status: string; path: string }> {
239
- const entries = stdout.split("\0");
240
- const parsed: Array<{ status: string; path: string }> = [];
241
- for (let i = 0; i < entries.length; i++) {
242
- const entry = entries[i] ?? "";
243
- if (entry.length < 4) {
244
- continue;
245
- }
246
- const status = entry.substring(0, 2);
247
- parsed.push({ status, path: entry.substring(3) });
248
- if (status[0] === "R" || status[0] === "C") {
249
- i++;
250
- }
251
- }
252
- return parsed;
253
- }
254
-
255
- /**
256
- * Every path git status knows about, including rename/copy origins.
257
- * Origins are skipped by {@link parsePorcelainZ} (they have no status
258
- * prefix) but still need to be staged so a deletion is not left behind
259
- * when a rename is split across add batches.
260
- */
261
- function collectDirtyPathsFromPorcelain(stdout: string): string[] {
262
- const entries = stdout.split("\0");
263
- const paths: string[] = [];
264
- for (let i = 0; i < entries.length; i++) {
265
- const entry = entries[i] ?? "";
266
- if (entry.length < 4) {
267
- continue;
268
- }
269
- const status = entry.substring(0, 2);
270
- paths.push(entry.substring(3));
271
- if (status[0] === "R" || status[0] === "C") {
272
- i++;
273
- const origin = entries[i];
274
- if (origin && origin.length > 0) {
275
- paths.push(origin);
276
- }
277
- }
278
- }
279
- return paths;
280
- }
281
-
282
233
  /** Properties added by Node's child_process errors. */
283
234
  interface ExecError extends Error {
284
235
  killed?: boolean;
@@ -433,7 +384,7 @@ export class WorkspaceGitService {
433
384
  *
434
385
  * This method is always called inside the mutex, so no git operation from
435
386
  * our code can be concurrently holding the lock. However, an external git
436
- * process (user running `git add`, IDE tooling, etc.) could legitimately
387
+ * git process (user running `git add`, IDE tooling, etc.) could legitimately
437
388
  * hold the lock. We use `lsof` to check — if any process has the file
438
389
  * open, we leave it alone. If no process holds it, it's stale (crashed
439
390
  * process) and safe to remove.
@@ -0,0 +1,258 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { Database } from "bun:sqlite";
4
+
5
+ import type { WorkspaceMigration } from "./types.js";
6
+
7
+ /**
8
+ * Repair `gpt-5.4` and `gpt-5.4-mini` pins on fragments that dispatch
9
+ * through the ChatGPT subscription.
10
+ *
11
+ * OpenAI serves neither model under ChatGPT sign-in: the Codex endpoint
12
+ * rejects them with HTTP 400 ("not supported when using Codex with a
13
+ * ChatGPT account"), so both are out of `CODEX_SUBSCRIPTION_MODEL_IDS`.
14
+ * API-key access is unaffected, so a fragment on any other route keeps
15
+ * its model.
16
+ *
17
+ * Two stored shapes dispatch through the subscription. The `chatgpt`
18
+ * routing identity fails `LLMSchema.superRefine` once the allowlist drops
19
+ * the model, and the loader's per-section salvage then resets the whole
20
+ * `llm` section, losing every profile and call-site tuning: this is the
21
+ * repair that prevents data loss. A profile bound to the subscription row
22
+ * (row kind `chatgpt`, or an `oauth_subscription` auth on the
23
+ * pre-DB-migration-366 row shape) bypasses the auto-resolution compat
24
+ * gate and 400s on every request; the row is named by the legacy
25
+ * `provider_connection` binding when present (dispatch honors it first,
26
+ * and the name is only ever a row name), otherwise by an entry-name
27
+ * `provider`. Both shapes are swept in `llm.default`, `llm.callSites.*`,
28
+ * and `llm.profiles.*`; call-site fragments carry no binding (the schema
29
+ * strips a raw `provider_connection`), so only their declared provider
30
+ * counts.
31
+ *
32
+ * A providerless call-site pin is deliberately left alone. It overlays
33
+ * whichever profile wins the site for a turn: on an API-key or managed
34
+ * winner it still serves the model, and on a subscription winner the
35
+ * request already fails upstream and keeps failing, with no config loss
36
+ * either way. Rewriting such pins would change API-key users' model and
37
+ * price to cover a winner that may never be selected.
38
+ *
39
+ * Replacements: `gpt-5.4` becomes `gpt-5.5` (its successor) and
40
+ * `gpt-5.4-mini` becomes `gpt-5.6-luna` (the subscription's Balanced model;
41
+ * on the subscription every model bills the same).
42
+ */
43
+ export const repairRetiredCodexGpt54ModelIdsMigration: WorkspaceMigration = {
44
+ id: "154-repair-retired-codex-gpt-5-4-model-ids",
45
+ description:
46
+ "Repair gpt-5.4 and gpt-5.4-mini pins on ChatGPT-subscription LLM fragments in workspace config",
47
+ run(workspaceDir: string): void {
48
+ const configPath = join(workspaceDir, "config.json");
49
+ if (!existsSync(configPath)) {
50
+ return;
51
+ }
52
+
53
+ // Read outside the parse catch: a transient filesystem error (EIO,
54
+ // EACCES) must reach the runner so the migration retries, while
55
+ // malformed JSON is a permanent state this migration cannot repair.
56
+ const rawText = readFileSync(configPath, "utf-8");
57
+
58
+ let config: Record<string, unknown>;
59
+ try {
60
+ const raw = JSON.parse(rawText);
61
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
62
+ return;
63
+ }
64
+ config = raw as Record<string, unknown>;
65
+ } catch {
66
+ return;
67
+ }
68
+
69
+ const llm = readObject(config.llm);
70
+ if (llm === null) {
71
+ return;
72
+ }
73
+
74
+ // Entry rows load lazily: only a stale fragment naming a row needs
75
+ // them. An unreadable DB then fails the run (retried next boot) rather
76
+ // than checkpointing a pass that skips entry-bound profiles.
77
+ let rows: Map<string, boolean> | null | undefined;
78
+ const isSubscriptionRow = (name: unknown): boolean => {
79
+ if (typeof name !== "string" || name.length === 0) {
80
+ return false;
81
+ }
82
+ if (rows === undefined) {
83
+ rows = readConnectionRows(workspaceDir);
84
+ }
85
+ if (rows === null) {
86
+ throw new Error(
87
+ "provider_connections is not readable; retrying the model-ID repair on the next run",
88
+ );
89
+ }
90
+ return rows.get(name) === true;
91
+ };
92
+ // A `provider` value: the identity dispatches to the subscription, a
93
+ // known vendor never does, and any other value is an entry name.
94
+ const isSubscriptionProvider = (provider: unknown): boolean =>
95
+ provider === CHATGPT_IDENTITY ||
96
+ (typeof provider === "string" &&
97
+ !KNOWN_PROVIDERS.has(provider) &&
98
+ isSubscriptionRow(provider));
99
+ // The binding is authoritative when present, since dispatch honors it
100
+ // ahead of the declared provider; the identity is repaired regardless
101
+ // (the schema rejects the pair whatever the binding).
102
+ const isBound = (fragment: Record<string, unknown>): boolean => {
103
+ if (fragment.provider === CHATGPT_IDENTITY) {
104
+ return true;
105
+ }
106
+ const binding = fragment.provider_connection;
107
+ return typeof binding === "string" && binding.length > 0
108
+ ? isSubscriptionRow(binding)
109
+ : isSubscriptionProvider(fragment.provider);
110
+ };
111
+ const isProviderBound = (fragment: Record<string, unknown>): boolean =>
112
+ isSubscriptionProvider(fragment.provider);
113
+
114
+ let changed = false;
115
+
116
+ changed = repairFragment(readObject(llm.default), isBound) || changed;
117
+
118
+ const callSites = readObject(llm.callSites);
119
+ if (callSites !== null) {
120
+ for (const rawConfig of Object.values(callSites)) {
121
+ changed =
122
+ repairFragment(readObject(rawConfig), isProviderBound) || changed;
123
+ }
124
+ }
125
+
126
+ const profiles = readObject(llm.profiles);
127
+ if (profiles !== null) {
128
+ for (const rawProfile of Object.values(profiles)) {
129
+ changed = repairFragment(readObject(rawProfile), isBound) || changed;
130
+ }
131
+ }
132
+
133
+ if (!changed) {
134
+ return;
135
+ }
136
+
137
+ // Write-then-rename so an interrupted write cannot leave config.json
138
+ // truncated: a torn in-place write would parse as invalid JSON on the
139
+ // retry, which the catch above treats as "nothing to do", letting the
140
+ // runner checkpoint the migration as completed against a corrupt file.
141
+ const tmpPath = `${configPath}.migration-154.tmp`;
142
+ writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
143
+ renameSync(tmpPath, configPath);
144
+ },
145
+ // The exact-match rewrite is idempotent, so a transient failure (full
146
+ // disk, I/O error) is safe to retry on later startups.
147
+ retryFailedCheckpoint: true,
148
+ down(_workspaceDir: string): void {
149
+ // Forward-only: reintroducing the retired model IDs would fail schema
150
+ // validation once the allowlist no longer carries them.
151
+ },
152
+ };
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Helpers: self-contained per workspace migrations AGENTS.md
156
+ // ---------------------------------------------------------------------------
157
+
158
+ const CHATGPT_IDENTITY = "chatgpt";
159
+
160
+ // Frozen snapshot of `KNOWN_LLM_PROVIDERS`: the vendor and identity values
161
+ // that dispatch without a connection row, so none of them is an entry name.
162
+ const KNOWN_PROVIDERS = new Set([
163
+ "anthropic",
164
+ "openai",
165
+ "gemini",
166
+ "ollama",
167
+ "fireworks",
168
+ "openrouter",
169
+ "vercel-ai-gateway",
170
+ "openai-compatible",
171
+ "minimax",
172
+ "atlascloud",
173
+ "together",
174
+ "litellm",
175
+ "opencode",
176
+ "baseten",
177
+ "poolside",
178
+ "vellum",
179
+ "chatgpt",
180
+ ]);
181
+
182
+ const REPLACEMENTS: ReadonlyMap<string, string> = new Map([
183
+ ["gpt-5.4", "gpt-5.5"],
184
+ ["gpt-5.4-mini", "gpt-5.6-luna"],
185
+ ]);
186
+
187
+ function repairFragment(
188
+ fragment: Record<string, unknown> | null,
189
+ isSubscriptionRouted: (fragment: Record<string, unknown>) => boolean,
190
+ ): boolean {
191
+ if (fragment === null || typeof fragment.model !== "string") {
192
+ return false;
193
+ }
194
+ const replacement = REPLACEMENTS.get(fragment.model);
195
+ if (replacement === undefined) {
196
+ return false;
197
+ }
198
+ if (!isSubscriptionRouted(fragment)) {
199
+ return false;
200
+ }
201
+ fragment.model = replacement;
202
+ return true;
203
+ }
204
+
205
+ /**
206
+ * `provider_connections` row name to whether the row dispatches to the
207
+ * ChatGPT subscription, or null when the DB or table is not readable. The
208
+ * caller fails the run on null: entry-name providers must be judged against
209
+ * real rows, never guessed. An absent DB file is a real state (no rows, so
210
+ * every entry name is dangling).
211
+ */
212
+ function readConnectionRows(workspaceDir: string): Map<string, boolean> | null {
213
+ const dbPath = join(workspaceDir, "data", "db", "assistant.db");
214
+ if (!existsSync(dbPath)) {
215
+ return new Map();
216
+ }
217
+ let db: Database;
218
+ try {
219
+ db = new Database(dbPath, { readonly: true });
220
+ } catch {
221
+ return null;
222
+ }
223
+ try {
224
+ const rows = db
225
+ .query(`SELECT name, provider, auth FROM provider_connections`)
226
+ .all() as Array<{ name: string; provider: string; auth: string }>;
227
+ return new Map(
228
+ rows.map((row) => [
229
+ row.name,
230
+ row.provider === CHATGPT_IDENTITY || isSubscriptionAuth(row.auth),
231
+ ]),
232
+ );
233
+ } catch {
234
+ return null;
235
+ } finally {
236
+ db.close();
237
+ }
238
+ }
239
+
240
+ function isSubscriptionAuth(auth: string): boolean {
241
+ try {
242
+ const parsed: unknown = JSON.parse(auth);
243
+ return (
244
+ parsed !== null &&
245
+ typeof parsed === "object" &&
246
+ (parsed as { type?: unknown }).type === "oauth_subscription"
247
+ );
248
+ } catch {
249
+ return false;
250
+ }
251
+ }
252
+
253
+ function readObject(value: unknown): Record<string, unknown> | null {
254
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
255
+ return null;
256
+ }
257
+ return value as Record<string, unknown>;
258
+ }
@@ -0,0 +1,78 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { getLogger } from "../../util/logger.js";
5
+ import type { WorkspaceMigration } from "./types.js";
6
+
7
+ const log = getLogger("workspace-migration-155");
8
+
9
+ /**
10
+ * Move `liveVoice.frontModel` to `voice.frontModel` in `config.json`.
11
+ *
12
+ * The front-door tuning (endpointing budgets and progress narration cadence)
13
+ * is read by phone calls as well as live voice, so it lives under a
14
+ * transport-agnostic key. A `voice.frontModel` already present wins field by
15
+ * field; the `liveVoice` block is removed either way so raw-config saves do
16
+ * not write it back.
17
+ */
18
+ export const moveFrontModelConfigToVoiceMigration: WorkspaceMigration = {
19
+ id: "155-move-front-model-config-to-voice",
20
+ description: "Move liveVoice.frontModel to voice.frontModel in config.json",
21
+ run(workspaceDir: string): void {
22
+ const configPath = join(workspaceDir, "config.json");
23
+ if (!existsSync(configPath)) {
24
+ return;
25
+ }
26
+
27
+ let config: Record<string, unknown>;
28
+ try {
29
+ const raw = JSON.parse(readFileSync(configPath, "utf-8"));
30
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
31
+ return;
32
+ }
33
+ config = raw as Record<string, unknown>;
34
+ } catch {
35
+ return;
36
+ }
37
+
38
+ const liveVoice = readObject(config.liveVoice);
39
+ if (liveVoice === null || !Object.hasOwn(liveVoice, "frontModel")) {
40
+ return;
41
+ }
42
+ const legacy = readObject(liveVoice.frontModel);
43
+ delete liveVoice.frontModel;
44
+
45
+ if (legacy !== null) {
46
+ const voice = readObject(config.voice) ?? {};
47
+ const current = readObject(voice.frontModel) ?? {};
48
+ const legacyProgress = readObject(legacy.progress) ?? {};
49
+ const currentProgress = readObject(current.progress) ?? {};
50
+ const merged: Record<string, unknown> = { ...legacy, ...current };
51
+ if (
52
+ Object.keys(legacyProgress).length > 0 ||
53
+ Object.keys(currentProgress).length > 0
54
+ ) {
55
+ merged.progress = { ...legacyProgress, ...currentProgress };
56
+ }
57
+ voice.frontModel = merged;
58
+ config.voice = voice;
59
+ }
60
+
61
+ const tmpPath = `${configPath}.migration-155.tmp`;
62
+ writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
63
+ renameSync(tmpPath, configPath);
64
+ log.info("Moved liveVoice.frontModel to voice.frontModel in config.json");
65
+ },
66
+ retryFailedCheckpoint: true,
67
+ down(_workspaceDir: string): void {
68
+ // Forward-only: the schema reads voice.frontModel and strips unknown
69
+ // liveVoice keys, so a block moved back is dropped on the next load.
70
+ },
71
+ };
72
+
73
+ function readObject(value: unknown): Record<string, unknown> | null {
74
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
75
+ return null;
76
+ }
77
+ return value as Record<string, unknown>;
78
+ }