@vellumai/assistant 0.12.4 → 0.12.5-dev.202609251017.e467a6c

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 (281) hide show
  1. package/docs/architecture/memory.md +9 -1
  2. package/docs/architecture/scheduling.md +13 -5
  3. package/docs/desktop-browser-cli.md +3 -2
  4. package/docs/guardian-request-flow.md +1 -1
  5. package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
  6. package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
  10. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
  11. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +2 -4
  12. package/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
  13. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
  14. package/openapi.yaml +152 -3
  15. package/package.json +1 -1
  16. package/src/__tests__/agent-loop.test.ts +76 -0
  17. package/src/__tests__/anthropic-error-formatting.test.ts +9 -0
  18. package/src/__tests__/background-shell-bash.test.ts +34 -0
  19. package/src/__tests__/background-shell-host-bash.test.ts +98 -1
  20. package/src/__tests__/background-tool-registry.test.ts +39 -0
  21. package/src/__tests__/channel-readiness-discord.test.ts +8 -0
  22. package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
  23. package/src/__tests__/client-os-metadata-persistence.test.ts +4 -20
  24. package/src/__tests__/conversation-agent-loop.test.ts +529 -1
  25. package/src/__tests__/conversation-error.test.ts +18 -0
  26. package/src/__tests__/conversation-pairing.test.ts +69 -0
  27. package/src/__tests__/conversation-queue.test.ts +257 -4
  28. package/src/__tests__/conversation-runtime-assembly.test.ts +27 -0
  29. package/src/__tests__/emit-signal-routing-intent.test.ts +62 -0
  30. package/src/__tests__/evict-conversations-for-reload.test.ts +21 -0
  31. package/src/__tests__/gemini-provider.test.ts +9 -0
  32. package/src/__tests__/host-cu-proxy.test.ts +79 -0
  33. package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
  34. package/src/__tests__/introduction-card-resolver.test.ts +53 -8
  35. package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
  36. package/src/__tests__/non-member-access-request.test.ts +48 -0
  37. package/src/__tests__/notification-events-source-context-probe.test.ts +48 -1
  38. package/src/__tests__/notification-platform-adapter.test.ts +97 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +103 -3
  40. package/src/__tests__/platform-callback-registration.test.ts +46 -1
  41. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  42. package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
  43. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -0
  44. package/src/__tests__/scheduler-result-notification.test.ts +521 -7
  45. package/src/__tests__/scheduler-wake.test.ts +1 -0
  46. package/src/__tests__/skill-load-inline-command.test.ts +35 -0
  47. package/src/__tests__/subagent-cron-run-attribution.test.ts +41 -2
  48. package/src/__tests__/subagent-manager-notify.test.ts +48 -0
  49. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -6
  50. package/src/__tests__/usage-attribution.test.ts +31 -0
  51. package/src/__tests__/wake-schedule-escalation.test.ts +1 -0
  52. package/src/agent/loop.ts +23 -0
  53. package/src/api/ask-question-tool.ts +50 -0
  54. package/src/api/events/generation-handoff.ts +4 -0
  55. package/src/api/events/message-complete.ts +4 -0
  56. package/src/api/events/tool-result.ts +11 -0
  57. package/src/api/index.ts +11 -0
  58. package/src/api/requests/guardian-actions.test.ts +45 -0
  59. package/src/api/requests/guardian-actions.ts +20 -0
  60. package/src/api/responses/conversation-message.ts +4 -0
  61. package/src/api/responses/home.ts +27 -0
  62. package/src/approvals/guardian-decision-primitive.ts +6 -4
  63. package/src/approvals/guardian-request-resolvers.ts +35 -30
  64. package/src/avatar/notification-avatar.test.ts +42 -34
  65. package/src/avatar/notification-avatar.ts +1 -0
  66. package/src/calls/__tests__/call-controller.test.ts +18 -0
  67. package/src/calls/__tests__/voice-control-protocol.test.ts +6 -1
  68. package/src/calls/__tests__/voice-leg-coordinator.test.ts +70 -0
  69. package/src/calls/__tests__/voice-session-bridge.test.ts +116 -4
  70. package/src/calls/__tests__/voice-triage-escalate.test.ts +77 -11
  71. package/src/calls/call-controller.ts +3 -1
  72. package/src/calls/voice-control-protocol.ts +12 -4
  73. package/src/calls/voice-escalation-judge.ts +2 -2
  74. package/src/calls/voice-leg-coordinator.ts +23 -5
  75. package/src/calls/voice-screen-annotation.ts +44 -0
  76. package/src/calls/voice-session-bridge.ts +63 -20
  77. package/src/calls/voice-triage-escalate.ts +82 -27
  78. package/src/cli/commands/__tests__/conversations-clear.test.ts +156 -0
  79. package/src/cli/commands/conversations.help.ts +7 -2
  80. package/src/cli/commands/conversations.ts +36 -30
  81. package/src/cli/commands/oauth/index.help.ts +8 -2
  82. package/src/cli/commands/oauth/request.test.ts +32 -0
  83. package/src/cli/commands/platform/index.help.ts +2 -1
  84. package/src/cli/commands/platform/index.ts +8 -2
  85. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +60 -0
  86. package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
  87. package/src/cli/lib/bundled-marketplace.json +35 -0
  88. package/src/cli/lib/bundled-plugin-packages.json +21 -0
  89. package/src/cli/lib/plugin-catalog-cache.ts +17 -2
  90. package/src/cli/lib/plugin-catalog-platform.ts +48 -9
  91. package/src/config/__tests__/default-profile-catalog.test.ts +28 -0
  92. package/src/config/__tests__/platform-identity.test.ts +59 -0
  93. package/src/config/__tests__/profile-order.test.ts +19 -0
  94. package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
  95. package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
  96. package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
  97. package/src/config/bundled-skills/screen-annotation/SKILL.md +41 -29
  98. package/src/config/bundled-skills/screen-annotation/TOOLS.json +5 -5
  99. package/src/config/call-site-defaults.ts +6 -0
  100. package/src/config/default-profile-catalog.ts +30 -1
  101. package/src/config/default-profile-names.ts +36 -2
  102. package/src/config/feature-flag-registry.json +9 -1
  103. package/src/config/platform-identity.ts +22 -5
  104. package/src/config/profile-order.ts +7 -1
  105. package/src/config/schemas/call-site-catalog.ts +7 -0
  106. package/src/config/schemas/llm.ts +21 -5
  107. package/src/config/sync-gated-profiles.ts +96 -44
  108. package/src/daemon/__tests__/abort-null-controller.test.ts +135 -1
  109. package/src/daemon/__tests__/auto-profile-router.test.ts +380 -0
  110. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +16 -0
  111. package/src/daemon/activity-trail.test.ts +206 -0
  112. package/src/daemon/activity-trail.ts +190 -0
  113. package/src/daemon/auto-profile-router.ts +431 -0
  114. package/src/daemon/computer-use-observation.test.ts +81 -0
  115. package/src/daemon/computer-use-observation.ts +23 -0
  116. package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
  117. package/src/daemon/conversation-agent-loop.ts +157 -32
  118. package/src/daemon/conversation-error.ts +19 -0
  119. package/src/daemon/conversation-lifecycle.ts +70 -7
  120. package/src/daemon/conversation-media-retry.ts +1 -2
  121. package/src/daemon/conversation-messaging.ts +31 -2
  122. package/src/daemon/conversation-process.ts +107 -16
  123. package/src/daemon/conversation-queue-manager.ts +7 -0
  124. package/src/daemon/conversation-runtime-assembly.ts +1 -0
  125. package/src/daemon/conversation-turn-finalize.ts +14 -0
  126. package/src/daemon/conversation.ts +25 -6
  127. package/src/daemon/event-loop-watchdog.test.ts +201 -2
  128. package/src/daemon/event-loop-watchdog.ts +182 -6
  129. package/src/daemon/host-cu-proxy.ts +34 -3
  130. package/src/daemon/pkb-reminder-builder.test.ts +10 -3
  131. package/src/daemon/pkb-reminder-builder.ts +1 -1
  132. package/src/daemon/wake-conversation-ops.ts +4 -2
  133. package/src/desktop/desktop-computer-use.test.ts +14 -4
  134. package/src/home/feed-types.ts +2 -0
  135. package/src/inbound/platform-callback-registration.ts +19 -6
  136. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
  137. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
  138. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +48 -1
  139. package/src/live-voice/__tests__/live-voice-vad.test.ts +34 -2
  140. package/src/live-voice/__tests__/protocol.test.ts +22 -0
  141. package/src/live-voice/continuation-judge.ts +1 -1
  142. package/src/live-voice/live-voice-session.ts +25 -19
  143. package/src/live-voice/protocol.ts +16 -5
  144. package/src/messaging/providers/discord/withdraw.ts +4 -5
  145. package/src/messaging/providers/gmail/client.ts +4 -0
  146. package/src/messaging/providers/slack/withdraw.ts +5 -5
  147. package/src/messaging/providers/telegram-bot/withdraw.ts +4 -5
  148. package/src/monitoring/__tests__/proc-fixtures.ts +28 -0
  149. package/src/monitoring/__tests__/proc-wait-state.test.ts +232 -0
  150. package/src/monitoring/__tests__/process-usage.test.ts +243 -0
  151. package/src/monitoring/proc-wait-state.ts +366 -0
  152. package/src/monitoring/process-memory.ts +4 -3
  153. package/src/monitoring/process-usage.ts +215 -0
  154. package/src/monitoring/resource-sample-types.ts +8 -0
  155. package/src/monitoring/resource-sampler.ts +17 -2
  156. package/src/monitoring/stall-capture.ts +12 -0
  157. package/src/notifications/README.md +88 -9
  158. package/src/notifications/__tests__/assistant-reply-producer.test.ts +390 -119
  159. package/src/notifications/__tests__/background-result-producer.test.ts +1272 -0
  160. package/src/notifications/__tests__/broadcaster.test.ts +292 -1
  161. package/src/notifications/__tests__/completion-policy.test.ts +243 -0
  162. package/src/notifications/__tests__/decision-engine.test.ts +51 -4
  163. package/src/notifications/__tests__/has-pending-background-work.test.ts +281 -0
  164. package/src/notifications/__tests__/home-feed-side-effect.test.ts +161 -3
  165. package/src/notifications/__tests__/resolve-visible-in-source.test.ts +110 -1
  166. package/src/notifications/__tests__/schedule-result-producer.test.ts +11 -4
  167. package/src/notifications/adapters/macos.ts +24 -12
  168. package/src/notifications/adapters/platform.ts +16 -5
  169. package/src/notifications/assistant-reply-producer.ts +96 -71
  170. package/src/notifications/background-result-producer.ts +369 -0
  171. package/src/notifications/broadcaster.ts +28 -8
  172. package/src/notifications/completion-policy.ts +115 -0
  173. package/src/notifications/completion-work.ts +46 -0
  174. package/src/notifications/conversation-pairing.ts +32 -13
  175. package/src/notifications/decision-engine.ts +18 -24
  176. package/src/notifications/emit-signal.ts +16 -1
  177. package/src/notifications/events-store.ts +4 -0
  178. package/src/notifications/has-pending-background-work.ts +91 -0
  179. package/src/notifications/home-feed-mirror.ts +44 -0
  180. package/src/notifications/home-feed-side-effect.ts +54 -28
  181. package/src/notifications/resolve-visible-in-source.ts +56 -2
  182. package/src/notifications/result-output.ts +154 -0
  183. package/src/notifications/schedule-result-producer.ts +6 -187
  184. package/src/notifications/types.ts +2 -0
  185. package/src/oauth/AGENTS.md +9 -1
  186. package/src/oauth/byo-connection.test.ts +28 -0
  187. package/src/persistence/__tests__/recent-conversation-messages.test.ts +41 -0
  188. package/src/persistence/conversation-crud.ts +26 -0
  189. package/src/persistence/conversation-types.ts +15 -37
  190. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +85 -14
  191. package/src/persistence/jobs-store.ts +229 -0
  192. package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
  193. package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
  194. package/src/persistence/steps.ts +12 -0
  195. package/src/platform/sync-avatar.test.ts +29 -0
  196. package/src/plugins/defaults/memory/AGENTS.md +5 -3
  197. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
  198. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
  199. package/src/plugins/defaults/memory/__tests__/turn-retrieval.test.ts +21 -0
  200. package/src/plugins/defaults/memory/graph/tools.ts +9 -5
  201. package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
  202. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +7 -3
  203. package/src/plugins/defaults/memory/job-handlers.ts +8 -2
  204. package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
  205. package/src/plugins/defaults/memory/tools.test.ts +17 -0
  206. package/src/plugins/defaults/memory/turn-retrieval.ts +8 -0
  207. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +24 -0
  208. package/src/plugins/defaults/memory/v3/injector.ts +4 -4
  209. package/src/plugins/types.ts +2 -0
  210. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  211. package/src/providers/anthropic/client.ts +4 -0
  212. package/src/providers/gemini/client.ts +7 -1
  213. package/src/providers/inference/auth.ts +25 -0
  214. package/src/providers/inference/profile-probe.ts +1 -0
  215. package/src/{calls/__tests__/typesafe-noul.test.ts → providers/jev/ask.test.ts} +4 -4
  216. package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +67 -34
  217. package/src/providers/openai/__tests__/api-error-normalization.test.ts +12 -0
  218. package/src/providers/openai/api-error-normalization.ts +4 -0
  219. package/src/providers/retry.ts +2 -0
  220. package/src/providers/search-provider-catalog.ts +10 -0
  221. package/src/providers/types.ts +7 -0
  222. package/src/providers/usage-tracking.ts +5 -3
  223. package/src/runtime/__tests__/agent-wake.test.ts +308 -0
  224. package/src/runtime/access-request-helper.ts +16 -9
  225. package/src/runtime/agent-wake-queue.ts +52 -0
  226. package/src/runtime/agent-wake.ts +111 -18
  227. package/src/runtime/channel-approval-types.ts +6 -38
  228. package/src/runtime/channel-approvals.ts +4 -5
  229. package/src/runtime/http-server.ts +4 -1
  230. package/src/runtime/introduction-policy.test.ts +17 -1
  231. package/src/runtime/introduction-policy.ts +25 -7
  232. package/src/runtime/migrations/vbundle-builder.ts +9 -0
  233. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +21 -1
  234. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
  235. package/src/runtime/routes/__tests__/oauth-proxy-routes.test.ts +61 -3
  236. package/src/runtime/routes/auto-profile-routes.ts +83 -0
  237. package/src/runtime/routes/conversation-management-routes.ts +1 -3
  238. package/src/runtime/routes/conversation-routes.ts +9 -0
  239. package/src/runtime/routes/guardian-action-routes.ts +8 -23
  240. package/src/runtime/routes/host-cu-routes.ts +3 -1
  241. package/src/runtime/routes/index.ts +2 -0
  242. package/src/runtime/routes/inference-profiles-routes.ts +5 -2
  243. package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
  244. package/src/runtime/routes/migration-routes.ts +8 -2
  245. package/src/runtime/routes/oauth-proxy-passthrough.test.ts +32 -0
  246. package/src/runtime/routes/platform-routes.ts +6 -1
  247. package/src/runtime/routes/user-route-dispatcher.ts +1 -2
  248. package/src/runtime/web-presence.ts +3 -4
  249. package/src/schedule/schedule-store.ts +10 -0
  250. package/src/schedule/scheduler.ts +92 -24
  251. package/src/subagent/manager.ts +32 -12
  252. package/src/telegram/__tests__/webhook-health.test.ts +8 -0
  253. package/src/telemetry/telemetry-wire-source.json +1 -1
  254. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  255. package/src/tools/background-tool-registry.ts +30 -3
  256. package/src/tools/browser/cdp-client/local-cdp-client.ts +1 -2
  257. package/src/tools/host-terminal/host-shell.ts +7 -0
  258. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
  259. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
  260. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
  261. package/src/tools/network/__tests__/web-search.test.ts +96 -0
  262. package/src/tools/network/web-fetch.ts +30 -9
  263. package/src/tools/network/web-search.ts +235 -4
  264. package/src/tools/skills/load.ts +13 -17
  265. package/src/tools/skills/scaffold-managed.ts +86 -71
  266. package/src/tools/terminal/shell-launch.test.ts +27 -1
  267. package/src/tools/terminal/shell.ts +4 -0
  268. package/src/tools/workspace-tools/loader.ts +1 -2
  269. package/src/usage/attribution.ts +18 -1
  270. package/src/util/abort-reasons.ts +2 -0
  271. package/src/util/errors.ts +1 -0
  272. package/src/util/oauth-request-body.test.ts +5 -1
  273. package/src/util/provider-error-patterns.ts +11 -0
  274. package/src/util/single-flight.test.ts +6 -0
  275. package/src/util/single-flight.ts +2 -0
  276. package/src/util/worker-pipe.test.ts +56 -10
  277. package/src/util/worker-pipe.ts +9 -4
  278. package/src/workspace/git-service.ts +6 -1
  279. package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
  280. package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
  281. package/src/workspace/migrations/registry.ts +2 -0
@@ -0,0 +1,163 @@
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
+ * Repoint legacy-shape LLM fragments (`provider: "openai-compatible"` plus a
9
+ * `provider_connection` binding) whose bound row's endpoint host is
10
+ * opencode.ai to `provider: "opencode"`.
11
+ *
12
+ * DB migration 383 restamps such rows as provider "opencode", and dispatch
13
+ * requires a bound row's provider to equal the fragment's declared provider,
14
+ * so a fragment left declaring "openai-compatible" would either mismatch or
15
+ * auto-resolve to an unrelated generic row. Entry-shape fragments (provider
16
+ * holds the row name) derive their kind from the row and need no rewrite.
17
+ * Rows are judged by their base_url host, not their provider column, so the
18
+ * result is the same whichever migration runs first.
19
+ *
20
+ * Swept in `llm.default` and `llm.profiles.*`; call-site fragments carry no
21
+ * binding (the schema strips a raw `provider_connection`).
22
+ */
23
+ export const repointOpencodeHostProviderFragmentsMigration: WorkspaceMigration =
24
+ {
25
+ id: "158-repoint-opencode-host-provider-fragments",
26
+ description:
27
+ "Repoint openai-compatible LLM fragments bound to opencode.ai connections to provider opencode",
28
+ run(workspaceDir: string): void {
29
+ const configPath = join(workspaceDir, "config.json");
30
+ if (!existsSync(configPath)) {
31
+ return;
32
+ }
33
+
34
+ // Read outside the parse catch: a transient filesystem error must reach
35
+ // the runner so the migration retries, while malformed JSON is a
36
+ // permanent state this migration cannot repair.
37
+ const rawText = readFileSync(configPath, "utf-8");
38
+
39
+ let config: Record<string, unknown>;
40
+ try {
41
+ const raw = JSON.parse(rawText);
42
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
43
+ return;
44
+ }
45
+ config = raw as Record<string, unknown>;
46
+ } catch {
47
+ return;
48
+ }
49
+
50
+ const llm = readObject(config.llm);
51
+ if (llm === null) {
52
+ return;
53
+ }
54
+
55
+ // Rows load lazily: only a legacy-shape openai-compatible fragment
56
+ // needs them. An unreadable DB then fails the run (retried next boot)
57
+ // rather than checkpointing a pass that skipped bound fragments.
58
+ let openCodeRows: Set<string> | null | undefined;
59
+ const isOpenCodeRow = (name: unknown): boolean => {
60
+ if (typeof name !== "string" || name.length === 0) {
61
+ return false;
62
+ }
63
+ if (openCodeRows === undefined) {
64
+ openCodeRows = readOpenCodeRowNames(workspaceDir);
65
+ }
66
+ if (openCodeRows === null) {
67
+ throw new Error(
68
+ "provider_connections is not readable; retrying the opencode repoint on the next run",
69
+ );
70
+ }
71
+ return openCodeRows.has(name);
72
+ };
73
+
74
+ const repoint = (fragment: Record<string, unknown> | null): boolean => {
75
+ if (
76
+ fragment === null ||
77
+ fragment.provider !== "openai-compatible" ||
78
+ !isOpenCodeRow(fragment.provider_connection)
79
+ ) {
80
+ return false;
81
+ }
82
+ fragment.provider = "opencode";
83
+ return true;
84
+ };
85
+
86
+ let changed = repoint(readObject(llm.default));
87
+ const profiles = readObject(llm.profiles);
88
+ if (profiles !== null) {
89
+ for (const rawProfile of Object.values(profiles)) {
90
+ changed = repoint(readObject(rawProfile)) || changed;
91
+ }
92
+ }
93
+ if (!changed) {
94
+ return;
95
+ }
96
+
97
+ // Write-then-rename so an interrupted write cannot leave config.json
98
+ // truncated for the retry to misread as "nothing to do".
99
+ const tmpPath = `${configPath}.migration-158.tmp`;
100
+ writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
101
+ renameSync(tmpPath, configPath);
102
+ },
103
+ // The exact-match rewrite is idempotent, so a transient failure is safe
104
+ // to retry on later startups.
105
+ retryFailedCheckpoint: true,
106
+ down(_workspaceDir: string): void {
107
+ // Forward-only: the rows these fragments bind now carry provider
108
+ // "opencode", so reverting the fragments would recreate the mismatch.
109
+ },
110
+ };
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Helpers: self-contained per workspace migrations AGENTS.md
114
+ // ---------------------------------------------------------------------------
115
+
116
+ /**
117
+ * Names of `provider_connections` rows whose base_url host is opencode.ai or
118
+ * a subdomain, or null when the DB or table is not readable. An absent DB
119
+ * file is a real state (no rows, so no fragment is bound to one).
120
+ */
121
+ function readOpenCodeRowNames(workspaceDir: string): Set<string> | null {
122
+ const dbPath = join(workspaceDir, "data", "db", "assistant.db");
123
+ if (!existsSync(dbPath)) {
124
+ return new Set();
125
+ }
126
+ let db: Database;
127
+ try {
128
+ db = new Database(dbPath, { readonly: true });
129
+ } catch {
130
+ return null;
131
+ }
132
+ try {
133
+ const rows = db
134
+ .query(
135
+ `SELECT name, base_url FROM provider_connections WHERE base_url IS NOT NULL`,
136
+ )
137
+ .all() as Array<{ name: string; base_url: string }>;
138
+ return new Set(
139
+ rows.filter((row) => isOpenCodeHost(row.base_url)).map((row) => row.name),
140
+ );
141
+ } catch {
142
+ return null;
143
+ } finally {
144
+ db.close();
145
+ }
146
+ }
147
+
148
+ function isOpenCodeHost(baseUrl: string): boolean {
149
+ let host: string;
150
+ try {
151
+ host = new URL(baseUrl).hostname.toLowerCase();
152
+ } catch {
153
+ return false;
154
+ }
155
+ return host === "opencode.ai" || host.endsWith(".opencode.ai");
156
+ }
157
+
158
+ function readObject(value: unknown): Record<string, unknown> | null {
159
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
160
+ return null;
161
+ }
162
+ return value as Record<string, unknown>;
163
+ }
@@ -0,0 +1,188 @@
1
+ import {
2
+ mkdirSync,
3
+ mkdtempSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { Database } from "bun:sqlite";
11
+ import { afterEach, describe, expect, test } from "bun:test";
12
+
13
+ import { WORKSPACE_MIGRATIONS } from "../registry.js";
14
+
15
+ const migration = WORKSPACE_MIGRATIONS.find(
16
+ (entry) => entry.id === "158-repoint-opencode-host-provider-fragments",
17
+ )!;
18
+
19
+ const workspaces: string[] = [];
20
+ afterEach(() => {
21
+ for (const workspace of workspaces.splice(0)) {
22
+ rmSync(workspace, { recursive: true, force: true });
23
+ }
24
+ });
25
+
26
+ function setup(): string {
27
+ const workspace = mkdtempSync(join(tmpdir(), "opencode-repoint-migration-"));
28
+ workspaces.push(workspace);
29
+ return workspace;
30
+ }
31
+
32
+ function writeConfig(workspace: string, data: Record<string, unknown>): void {
33
+ writeFileSync(
34
+ join(workspace, "config.json"),
35
+ JSON.stringify(data, null, 2) + "\n",
36
+ );
37
+ }
38
+
39
+ function readLlm(workspace: string): Record<string, any> {
40
+ return JSON.parse(readFileSync(join(workspace, "config.json"), "utf-8")).llm;
41
+ }
42
+
43
+ function seedRows(
44
+ workspace: string,
45
+ rows: Array<{ name: string; provider: string; baseUrl: string | null }>,
46
+ ): void {
47
+ mkdirSync(join(workspace, "data", "db"), { recursive: true });
48
+ const db = new Database(join(workspace, "data", "db", "assistant.db"));
49
+ db.run(`CREATE TABLE provider_connections (
50
+ name TEXT PRIMARY KEY,
51
+ provider TEXT NOT NULL,
52
+ auth TEXT NOT NULL,
53
+ base_url TEXT,
54
+ created_at INTEGER NOT NULL,
55
+ updated_at INTEGER NOT NULL
56
+ )`);
57
+ for (const row of rows) {
58
+ db.query(
59
+ `INSERT INTO provider_connections (name, provider, auth, base_url, created_at, updated_at)
60
+ VALUES (?, ?, '{"type":"api_key","credential":"cred"}', ?, 1, 1)`,
61
+ ).run(row.name, row.provider, row.baseUrl);
62
+ }
63
+ db.close();
64
+ }
65
+
66
+ describe("migration 158: repoint opencode-host provider fragments", () => {
67
+ test("repoints legacy-shape fragments bound to an opencode.ai row, whichever provider the row stores", () => {
68
+ const workspace = setup();
69
+ seedRows(workspace, [
70
+ {
71
+ name: "go",
72
+ provider: "openai-compatible",
73
+ baseUrl: "https://opencode.ai/zen/go/v1",
74
+ },
75
+ {
76
+ name: "zen",
77
+ provider: "opencode",
78
+ baseUrl: "https://opencode.ai/zen/v1",
79
+ },
80
+ {
81
+ name: "vllm",
82
+ provider: "openai-compatible",
83
+ baseUrl: "http://localhost:8080/v1",
84
+ },
85
+ ]);
86
+ writeConfig(workspace, {
87
+ llm: {
88
+ default: {
89
+ provider: "openai-compatible",
90
+ provider_connection: "go",
91
+ model: "m",
92
+ },
93
+ profiles: {
94
+ a: {
95
+ provider: "openai-compatible",
96
+ provider_connection: "go",
97
+ model: "m",
98
+ },
99
+ b: {
100
+ provider: "openai-compatible",
101
+ provider_connection: "zen",
102
+ model: "m",
103
+ },
104
+ c: {
105
+ provider: "openai-compatible",
106
+ provider_connection: "vllm",
107
+ model: "m",
108
+ },
109
+ entry: { provider: "go", model: "m" },
110
+ unbound: { provider: "openai-compatible", model: "m" },
111
+ dangling: {
112
+ provider: "openai-compatible",
113
+ provider_connection: "gone",
114
+ model: "m",
115
+ },
116
+ },
117
+ },
118
+ });
119
+
120
+ migration.run(workspace);
121
+
122
+ const llm = readLlm(workspace);
123
+ expect(llm.default.provider).toBe("opencode");
124
+ expect(llm.default.provider_connection).toBe("go");
125
+ expect(llm.profiles.a.provider).toBe("opencode");
126
+ expect(llm.profiles.b.provider).toBe("opencode");
127
+ expect(llm.profiles.c.provider).toBe("openai-compatible");
128
+ expect(llm.profiles.entry.provider).toBe("go");
129
+ expect(llm.profiles.unbound.provider).toBe("openai-compatible");
130
+ expect(llm.profiles.dangling.provider).toBe("openai-compatible");
131
+ });
132
+
133
+ test("no-ops without config, without a db, or with nothing bound", () => {
134
+ const noConfig = setup();
135
+ expect(() => migration.run(noConfig)).not.toThrow();
136
+
137
+ const noDb = setup();
138
+ writeConfig(noDb, {
139
+ llm: {
140
+ profiles: {
141
+ a: { provider: "openai-compatible", provider_connection: "go" },
142
+ },
143
+ },
144
+ });
145
+ migration.run(noDb);
146
+ expect(readLlm(noDb).profiles.a.provider).toBe("openai-compatible");
147
+ });
148
+
149
+ test("fails the run instead of checkpointing when the db is unreadable", () => {
150
+ const workspace = setup();
151
+ seedRows(workspace, []);
152
+ writeFileSync(
153
+ join(workspace, "data", "db", "assistant.db"),
154
+ "not a database",
155
+ );
156
+ writeConfig(workspace, {
157
+ llm: {
158
+ profiles: {
159
+ a: { provider: "openai-compatible", provider_connection: "go" },
160
+ },
161
+ },
162
+ });
163
+ expect(() => migration.run(workspace)).toThrow(/not readable/);
164
+ });
165
+
166
+ test("is idempotent", () => {
167
+ const workspace = setup();
168
+ seedRows(workspace, [
169
+ {
170
+ name: "go",
171
+ provider: "openai-compatible",
172
+ baseUrl: "https://opencode.ai/zen/go/v1",
173
+ },
174
+ ]);
175
+ writeConfig(workspace, {
176
+ llm: {
177
+ profiles: {
178
+ a: { provider: "openai-compatible", provider_connection: "go" },
179
+ },
180
+ },
181
+ });
182
+ migration.run(workspace);
183
+ const once = readFileSync(join(workspace, "config.json"), "utf-8");
184
+ migration.run(workspace);
185
+ expect(readFileSync(join(workspace, "config.json"), "utf-8")).toBe(once);
186
+ expect(readLlm(workspace).profiles.a.provider).toBe("opencode");
187
+ });
188
+ });
@@ -155,6 +155,7 @@ import { repairRetiredCodexGpt54ModelIdsMigration } from "./154-repair-retired-c
155
155
  import { moveFrontModelConfigToVoiceMigration } from "./155-move-front-model-config-to-voice.js";
156
156
  import { extractWorkspaceMcpJsonMigration } from "./156-extract-workspace-mcp-json.js";
157
157
  import { addDesktopFilesLauncherMigration } from "./157-add-desktop-files-launcher.js";
158
+ import { repointOpencodeHostProviderFragmentsMigration } from "./158-repoint-opencode-host-provider-fragments.js";
158
159
  import { renameCollidingJevProfileNameMigration } from "./159-rename-colliding-jev-profile-name.js";
159
160
  import { migrateToWorkspaceVolumeMigration } from "./migrate-to-workspace-volume.js";
160
161
  import type { WorkspaceMigration } from "./types.js";
@@ -326,5 +327,6 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
326
327
  moveFrontModelConfigToVoiceMigration,
327
328
  extractWorkspaceMcpJsonMigration,
328
329
  addDesktopFilesLauncherMigration,
330
+ repointOpencodeHostProviderFragmentsMigration,
329
331
  renameCollidingJevProfileNameMigration,
330
332
  ];