super-dsh 0.1.6 → 0.1.10-e

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 (260) hide show
  1. package/README.md +25 -3
  2. package/agent-agy/cordis.patch.yml +7 -4
  3. package/agent-agy/dist/agent.d.ts +12 -11
  4. package/agent-agy/dist/agy-auth-gateway.d.ts +124 -0
  5. package/agent-agy/dist/agy-auth-gateway.js +233 -0
  6. package/agent-agy/dist/agy-auth.d.ts +153 -0
  7. package/agent-agy/dist/agy-auth.js +179 -0
  8. package/agent-agy/dist/agy-cli-client.d.ts +39 -9
  9. package/agent-agy/dist/agy-cli-client.js +82 -18
  10. package/agent-agy/dist/agy-store.d.ts +8 -0
  11. package/agent-agy/dist/index.d.ts +46 -16
  12. package/agent-agy/dist/index.js +82 -56
  13. package/agent-agy/dist/models.d.ts +70 -0
  14. package/agent-agy/dist/models.js +102 -3
  15. package/agent-agy/package.json +7 -5
  16. package/agent-agy-sdk/cordis.patch.yml +58 -0
  17. package/agent-agy-sdk/dist/adapter.d.ts +15 -0
  18. package/agent-agy-sdk/dist/adapter.js +42 -0
  19. package/agent-agy-sdk/dist/agent-preset-agy.d.ts +27 -0
  20. package/agent-agy-sdk/dist/agent-preset-agy.js +32 -0
  21. package/agent-agy-sdk/dist/agent.d.ts +138 -0
  22. package/agent-agy-sdk/dist/agent.js +494 -0
  23. package/{agent-agy → agent-agy-sdk}/dist/agy-client.d.ts +26 -6
  24. package/{agent-agy → agent-agy-sdk}/dist/agy-client.js +43 -25
  25. package/agent-agy-sdk/dist/agy-sessions.d.ts +25 -0
  26. package/agent-agy-sdk/dist/agy-sessions.js +107 -0
  27. package/agent-agy-sdk/dist/agy-store.d.ts +39 -0
  28. package/agent-agy-sdk/dist/agy-store.js +83 -0
  29. package/agent-agy-sdk/dist/inbox.d.ts +26 -0
  30. package/agent-agy-sdk/dist/inbox.js +53 -0
  31. package/agent-agy-sdk/dist/index.d.ts +93 -0
  32. package/agent-agy-sdk/dist/index.js +476 -0
  33. package/agent-agy-sdk/dist/models.d.ts +41 -0
  34. package/agent-agy-sdk/dist/models.js +74 -0
  35. package/agent-agy-sdk/dist/sdk-venv.d.ts +76 -0
  36. package/agent-agy-sdk/dist/sdk-venv.js +173 -0
  37. package/agent-agy-sdk/dist/world-plugin.d.ts +10 -0
  38. package/agent-agy-sdk/dist/world-plugin.js +82 -0
  39. package/agent-agy-sdk/package.json +64 -0
  40. package/agent-claude/dist/adapter.js +106 -31
  41. package/agent-claude/dist/agent.js +154 -17
  42. package/agent-claude/dist/claude-client.js +84 -43
  43. package/agent-claude/dist/claude-launch.js +101 -0
  44. package/agent-claude/dist/claude-native-config.js +169 -0
  45. package/agent-claude/dist/claude-provider-gateway.js +340 -0
  46. package/agent-claude/dist/claude-providers.js +391 -0
  47. package/agent-claude/dist/claude-route.js +152 -0
  48. package/agent-claude/dist/index.js +214 -37
  49. package/agent-claude/dist/models.js +14 -0
  50. package/agent-claude/dist/sdk-runtime.js +320 -0
  51. package/agent-claude/package.json +6 -4
  52. package/agent-codex/README.md +141 -36
  53. package/agent-codex/cordis.patch.yml +14 -10
  54. package/agent-codex/dist/adapter.js +64 -28
  55. package/agent-codex/dist/agent.js +255 -81
  56. package/agent-codex/dist/app-server-approvals.js +165 -0
  57. package/agent-codex/dist/app-server-client.js +723 -0
  58. package/agent-codex/dist/app-server-events.js +432 -0
  59. package/agent-codex/dist/app-server-protocol.js +95 -0
  60. package/agent-codex/dist/app-server-wire.js +299 -0
  61. package/agent-codex/dist/codex-launch.js +78 -0
  62. package/agent-codex/dist/codex-native-config.js +392 -0
  63. package/agent-codex/dist/codex-provider-gateway.js +402 -0
  64. package/agent-codex/dist/codex-providers.js +376 -0
  65. package/agent-codex/dist/codex-route.js +86 -0
  66. package/agent-codex/dist/index.js +256 -115
  67. package/agent-codex/dist/permission.js +75 -1
  68. package/agent-codex/package.json +1 -2
  69. package/agent-codex-sdk/README.md +79 -0
  70. package/agent-codex-sdk/cordis.patch.yml +81 -0
  71. package/agent-codex-sdk/dist/adapter.js +91 -0
  72. package/agent-codex-sdk/dist/agent-preset-codex.js +32 -0
  73. package/agent-codex-sdk/dist/agent.js +1085 -0
  74. package/agent-codex-sdk/dist/codex-native-config.js +392 -0
  75. package/agent-codex-sdk/dist/codex-provider-gateway.js +402 -0
  76. package/agent-codex-sdk/dist/codex-providers.js +376 -0
  77. package/agent-codex-sdk/dist/codex-route.js +86 -0
  78. package/{agent-codex/dist/codex-client.js → agent-codex-sdk/dist/codex-sdk-client.js} +253 -32
  79. package/agent-codex-sdk/dist/codex-sdk-launch.js +92 -0
  80. package/agent-codex-sdk/dist/codex-store.js +180 -0
  81. package/agent-codex-sdk/dist/inbox.js +53 -0
  82. package/agent-codex-sdk/dist/index.js +739 -0
  83. package/agent-codex-sdk/dist/permission.js +71 -0
  84. package/agent-codex-sdk/dist/sdk-runtime.js +307 -0
  85. package/agent-codex-sdk/dist/session-map.js +167 -0
  86. package/agent-codex-sdk/dist/world-plugin.js +93 -0
  87. package/agent-codex-sdk/package.json +79 -0
  88. package/agent-codex-sdk/scripts/fetch-sdk-runtime.mjs +57 -0
  89. package/agent-omp/README.md +72 -55
  90. package/agent-omp/dist/agent.js +36 -11
  91. package/agent-omp/dist/index.js +92 -52
  92. package/agent-omp/dist/lazy-rpc.js +11 -4
  93. package/agent-omp/dist/omp-cli.js +114 -97
  94. package/agent-omp/dist/omp-store.js +12 -4
  95. package/agent-omp/dist/permission.js +70 -5
  96. package/agent-omp/dist/rpc-client.js +409 -0
  97. package/agent-omp/dist/world-plugin.js +36 -3
  98. package/agent-omp/package.json +2 -6
  99. package/agent-omp-sdk/README.md +103 -0
  100. package/agent-omp-sdk/cordis.patch.yml +108 -0
  101. package/agent-omp-sdk/dist/adapter.d.ts +21 -0
  102. package/agent-omp-sdk/dist/adapter.js +36 -0
  103. package/agent-omp-sdk/dist/agent-preset-omp.d.ts +31 -0
  104. package/agent-omp-sdk/dist/agent-preset-omp.js +32 -0
  105. package/agent-omp-sdk/dist/agent.d.ts +64 -0
  106. package/agent-omp-sdk/dist/agent.js +1178 -0
  107. package/agent-omp-sdk/dist/discovery.d.ts +57 -0
  108. package/agent-omp-sdk/dist/discovery.js +116 -0
  109. package/agent-omp-sdk/dist/inbox.d.ts +28 -0
  110. package/agent-omp-sdk/dist/inbox.js +53 -0
  111. package/agent-omp-sdk/dist/index.d.ts +60 -0
  112. package/agent-omp-sdk/dist/index.js +632 -0
  113. package/agent-omp-sdk/dist/knobs.d.ts +34 -0
  114. package/agent-omp-sdk/dist/knobs.js +50 -0
  115. package/agent-omp-sdk/dist/lazy-rpc.d.ts +98 -0
  116. package/agent-omp-sdk/dist/lazy-rpc.js +178 -0
  117. package/agent-omp-sdk/dist/mobile/zoom-guard.d.ts +187 -0
  118. package/agent-omp-sdk/dist/mobile/zoom-guard.js +294 -0
  119. package/agent-omp-sdk/dist/mobile-boot.d.ts +49 -0
  120. package/agent-omp-sdk/dist/mobile-boot.js +38 -0
  121. package/agent-omp-sdk/dist/models.d.ts +39 -0
  122. package/agent-omp-sdk/dist/models.js +299 -0
  123. package/agent-omp-sdk/dist/omp-cli.d.ts +67 -0
  124. package/agent-omp-sdk/dist/omp-cli.js +164 -0
  125. package/agent-omp-sdk/dist/omp-disk-discovery.d.ts +27 -0
  126. package/agent-omp-sdk/dist/omp-disk-discovery.js +178 -0
  127. package/agent-omp-sdk/dist/omp-executable.d.ts +2 -0
  128. package/agent-omp-sdk/dist/omp-executable.js +30 -0
  129. package/agent-omp-sdk/dist/omp-store.d.ts +134 -0
  130. package/agent-omp-sdk/dist/omp-store.js +526 -0
  131. package/agent-omp-sdk/dist/pairing.d.ts +7 -0
  132. package/agent-omp-sdk/dist/pairing.js +45 -0
  133. package/agent-omp-sdk/dist/permission.d.ts +138 -0
  134. package/agent-omp-sdk/dist/permission.js +161 -0
  135. package/agent-omp-sdk/dist/protocol.d.ts +189 -0
  136. package/agent-omp-sdk/dist/rpc-types.d.ts +77 -0
  137. package/agent-omp-sdk/dist/rpc-types.js +1 -0
  138. package/agent-omp-sdk/dist/sdk-client.d.ts +160 -0
  139. package/{agent-omp → agent-omp-sdk}/dist/sdk-client.js +108 -56
  140. package/agent-omp-sdk/dist/sdk-runtime.d.ts +211 -0
  141. package/agent-omp-sdk/dist/sdk-runtime.js +418 -0
  142. package/agent-omp-sdk/dist/sidecar-client.d.ts +87 -0
  143. package/agent-omp-sdk/dist/sidecar-client.js +197 -0
  144. package/agent-omp-sdk/dist/store/adapter.d.ts +17 -0
  145. package/agent-omp-sdk/dist/store/adapter.js +47 -0
  146. package/agent-omp-sdk/dist/store/db.d.ts +70 -0
  147. package/agent-omp-sdk/dist/store/db.js +210 -0
  148. package/agent-omp-sdk/dist/store/index.d.ts +23 -0
  149. package/agent-omp-sdk/dist/store/index.js +59 -0
  150. package/agent-omp-sdk/dist/store/reconcile.d.ts +15 -0
  151. package/agent-omp-sdk/dist/store/reconcile.js +51 -0
  152. package/agent-omp-sdk/dist/store/schema.d.ts +8 -0
  153. package/agent-omp-sdk/dist/store/schema.js +40 -0
  154. package/agent-omp-sdk/dist/transport.d.ts +49 -0
  155. package/agent-omp-sdk/dist/transport.js +35 -0
  156. package/agent-omp-sdk/dist/world-home.d.ts +4 -0
  157. package/agent-omp-sdk/dist/world-home.js +23 -0
  158. package/agent-omp-sdk/dist/world-plugin.d.ts +10 -0
  159. package/agent-omp-sdk/dist/world-plugin.js +137 -0
  160. package/agent-omp-sdk/package.json +87 -0
  161. package/client/index.js +1883 -287
  162. package/cordis.patch.yml +80 -0
  163. package/package.json +23 -9
  164. package/superd-hub/dist/a2a-delivery.d.ts +189 -0
  165. package/superd-hub/dist/a2a-delivery.js +240 -0
  166. package/superd-hub/dist/a2a-host.d.ts +76 -0
  167. package/superd-hub/dist/a2a-host.js +145 -0
  168. package/superd-hub/dist/a2a-ingest.d.ts +31 -0
  169. package/superd-hub/dist/a2a-ingest.js +636 -0
  170. package/superd-hub/dist/a2a-log.d.ts +17 -0
  171. package/superd-hub/dist/a2a-log.js +42 -0
  172. package/superd-hub/dist/a2a-row.d.ts +65 -0
  173. package/superd-hub/dist/a2a-row.js +162 -0
  174. package/superd-hub/dist/a2a-service.d.ts +357 -0
  175. package/superd-hub/dist/a2a-service.js +980 -0
  176. package/superd-hub/dist/a2a-wire.d.ts +220 -0
  177. package/superd-hub/dist/a2a-wire.js +305 -0
  178. package/superd-hub/dist/agent-ids.d.ts +10 -0
  179. package/superd-hub/dist/agent-ids.js +68 -0
  180. package/superd-hub/dist/agent-roster.d.ts +29 -18
  181. package/superd-hub/dist/agent-roster.js +54 -15
  182. package/superd-hub/dist/agent-status.d.ts +119 -0
  183. package/superd-hub/dist/agent-status.js +386 -0
  184. package/superd-hub/dist/client/agent-settings.d.ts +210 -0
  185. package/superd-hub/dist/client/agent-settings.js +300 -0
  186. package/superd-hub/dist/client/mesh-api.d.ts +134 -0
  187. package/superd-hub/dist/client/mesh-api.js +219 -0
  188. package/superd-hub/dist/client/mesh-groups.d.ts +63 -0
  189. package/superd-hub/dist/client/mesh-groups.js +66 -0
  190. package/superd-hub/dist/client/roster.d.ts +48 -0
  191. package/superd-hub/dist/client/roster.js +102 -0
  192. package/superd-hub/dist/client/runtime-settings-api.d.ts +76 -0
  193. package/superd-hub/dist/client/runtime-settings-api.js +137 -0
  194. package/superd-hub/dist/client/switch.d.ts +165 -0
  195. package/superd-hub/dist/client/switch.js +295 -0
  196. package/superd-hub/dist/exec-pty-channel.d.ts +58 -0
  197. package/superd-hub/dist/exec-pty-channel.js +162 -0
  198. package/superd-hub/dist/exec-pty-framing.d.ts +32 -0
  199. package/superd-hub/dist/exec-pty-framing.js +81 -0
  200. package/superd-hub/dist/exec-pty-rpc.d.ts +21 -0
  201. package/superd-hub/dist/exec-pty-rpc.js +128 -0
  202. package/superd-hub/dist/exec-pty-session.d.ts +76 -0
  203. package/superd-hub/dist/exec-pty-session.js +234 -0
  204. package/superd-hub/dist/exec.d.ts +56 -0
  205. package/superd-hub/dist/exec.js +225 -0
  206. package/superd-hub/dist/gateway.d.ts +39 -2
  207. package/superd-hub/dist/gateway.js +62 -3
  208. package/superd-hub/dist/hub-tool.d.ts +174 -0
  209. package/superd-hub/dist/hub-tool.js +464 -0
  210. package/superd-hub/dist/index.d.ts +15 -1
  211. package/superd-hub/dist/index.js +16 -0
  212. package/superd-hub/dist/machines.d.ts +45 -0
  213. package/superd-hub/dist/machines.js +125 -0
  214. package/superd-hub/dist/mesh-federate.d.ts +138 -0
  215. package/superd-hub/dist/mesh-federate.js +223 -0
  216. package/superd-hub/dist/mesh-gateway.d.ts +67 -0
  217. package/superd-hub/dist/mesh-gateway.js +408 -0
  218. package/superd-hub/dist/mesh-link.d.ts +274 -0
  219. package/superd-hub/dist/mesh-link.js +441 -0
  220. package/superd-hub/dist/mesh-rpc.d.ts +123 -0
  221. package/superd-hub/dist/mesh-rpc.js +250 -0
  222. package/superd-hub/dist/mux-client.d.ts +71 -0
  223. package/superd-hub/dist/mux-client.js +281 -0
  224. package/superd-hub/dist/pack-version.d.ts +2 -0
  225. package/superd-hub/dist/pack-version.js +30 -0
  226. package/superd-hub/dist/roster.d.ts +6 -0
  227. package/superd-hub/dist/rpc.d.ts +18 -2
  228. package/superd-hub/dist/rpc.js +34 -4
  229. package/superd-hub/dist/spawn-world.js +3 -1
  230. package/superd-hub/dist/switch-core.d.ts +62 -25
  231. package/superd-hub/dist/switch-core.js +53 -30
  232. package/superd-hub/dist/system-head.d.ts +22 -12
  233. package/superd-hub/dist/system-head.js +100 -84
  234. package/superd-hub/dist/wire-endpoints.d.ts +264 -0
  235. package/superd-hub/dist/wire-endpoints.js +180 -0
  236. package/superd-hub/lib/client/index.js +1883 -287
  237. package/superd-hub/package.json +11 -2
  238. package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
  239. package/agent-agy/dist/agent-preset-projection.js +0 -33
  240. package/agent-claude/dist/agent-preset-projection.js +0 -30
  241. package/agent-codex/dist/agent-preset-projection.js +0 -37
  242. package/agent-codex/dist/app-home.js +0 -65
  243. package/agent-codex/dist/models.js +0 -152
  244. package/agent-codex/dist/spike.js +0 -46
  245. package/agent-hermes/dist/agent-preset-projection.js +0 -34
  246. package/agent-omp/dist/agent-preset-projection.js +0 -36
  247. package/agent-omp/dist/app-home.js +0 -86
  248. package/agent-omp/dist/sidecar-client.js +0 -195
  249. package/agent-pi/dist/agent-preset-projection.js +0 -29
  250. package/superd-hub/dist/client-shim.d.ts +0 -28
  251. package/superd-hub/dist/client-shim.js +0 -251
  252. package/superd-hub/dist/index-pass.d.ts +0 -22
  253. package/superd-hub/dist/index-pass.js +0 -47
  254. package/superd-hub/dist/labels.d.ts +0 -45
  255. package/superd-hub/dist/labels.js +0 -83
  256. /package/{agent-agy → agent-agy-sdk}/bridge/agy_bridge.py +0 -0
  257. /package/{agent-agy → agent-agy-sdk}/bridge/gemini_openai_server.mjs +0 -0
  258. /package/{agent-codex/dist/codex-events.js → agent-codex-sdk/dist/codex-sdk-events.js} +0 -0
  259. /package/{agent-omp → agent-omp-sdk}/dist/protocol.js +0 -0
  260. /package/{agent-omp → agent-omp-sdk}/sidecar/main.ts +0 -0
@@ -39,11 +39,14 @@
39
39
  */
40
40
  import { randomUUID } from "node:crypto";
41
41
  import { agentEvents } from "@deepseek-ai/dsh-agent";
42
+ import { canonicalHeader, headerEquals } from "@deepseek-ai/dsh-session";
42
43
  import { LlmAttemptId, ToolCallId, createAssistantMessage, createSystemMessage, createToolResultMessage, createUserMessage, } from "@deepseek-ai/dsh-llm";
43
44
  import { createScope } from "@deepseek-ai/dsh-scope";
44
45
  import { toClaudeContent } from "./content.js";
45
46
  import { Inbox } from "./inbox.js";
46
47
  import { resetProjectionState } from "./claude-events.js";
48
+ import { NATIVE_LAUNCH } from "./claude-launch.js";
49
+ import { resolveRoute, routeSeed } from "./claude-route.js";
47
50
  import { CLAUDE_PROVIDER_ID } from "./adapter.js";
48
51
  import { claudePermissionMode, presetFromEvents } from "./permission.js";
49
52
  import { catalogFromInit, modelEntryFromSdk, readModelCatalog, setModelCatalog } from "./models.js";
@@ -235,6 +238,9 @@ export class ClaudeAgent {
235
238
  inbox;
236
239
  ctx;
237
240
  client;
241
+ /** Unsubscriber for the LIVE client's wire listener — released before a swap closes the old child. */
242
+ clientUnsubscribe = undefined;
243
+ clientFactory;
238
244
  loopCtx;
239
245
  scope;
240
246
  dispatch;
@@ -262,7 +268,12 @@ export class ClaudeAgent {
262
268
  systemMessageDone = false;
263
269
  /** Last `request/context` route key (`provider/model`) — dedupes per-turn emits. */
264
270
  lastRouteKey = null;
265
- /** Live v2 assistant-stream publication for the in-flight wire message. */
271
+ /** The route the OPEN turn runs, captured when the turn opened. */
272
+ turnRoute = undefined;
273
+ /** The route the LIVE client was last told to run (`provider/model`) — a work-saver. */
274
+ routeKey = null;
275
+ /** Whether THIS agent instance has already logged a `request/header` snapshot. */
276
+ requestHeaderLogged = false;
266
277
  streamBridge;
267
278
  assistantAttemptCounter = 0;
268
279
  assistantStreamRevision = 0;
@@ -295,12 +306,13 @@ export class ClaudeAgent {
295
306
  commandsSignature = "";
296
307
  /** Releases the current mirror before it is rebuilt or the agent is disposed. */
297
308
  commandsDisposer = undefined;
298
- constructor(loopCtx, id, options, session, client, onIdleExit, runtimeInfo) {
309
+ constructor(loopCtx, id, options, session, client, onIdleExit, runtimeInfo, clientFactory) {
299
310
  this.loopCtx = loopCtx;
300
311
  this.id = id;
301
312
  this.options = options;
302
313
  this.session = withSystemHead(session, () => createSystemMessage(""));
303
314
  this.client = client;
315
+ this.clientFactory = clientFactory;
304
316
  this.onIdleExit = onIdleExit;
305
317
  this.runtimeInfo = runtimeInfo;
306
318
  this.dispatch = agentEvents(loopCtx, this);
@@ -318,7 +330,7 @@ export class ClaudeAgent {
318
330
  this.attachCommandRuntime(commandCtx.commands);
319
331
  });
320
332
  this.lastTurn = session.snapshotEvents().findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
321
- client.on((event) => this.handleEvent(event));
333
+ this.clientUnsubscribe = client.on((event) => this.handleEvent(event));
322
334
  // Session-start projection reset: the module-level tool-name map must never
323
335
  // leak another session's tool into this one (Task 4 review finding).
324
336
  resetProjectionState();
@@ -420,6 +432,12 @@ export class ClaudeAgent {
420
432
  }
421
433
  this.markIdle();
422
434
  this.endActivity();
435
+ // Release the live client's wire listener BEFORE its child closes (Task 9
436
+ // ledger): without this the dispose path can still receive the superseded
437
+ // client's stale `agent_end` — the same bug class `reattach()` guards
438
+ // against on the swap path.
439
+ this.clientUnsubscribe?.();
440
+ this.clientUnsubscribe = undefined;
423
441
  this.client.close();
424
442
  resetProjectionState();
425
443
  // Release the command mirror explicitly (the agent scope would also dispose
@@ -618,8 +636,14 @@ export class ClaudeAgent {
618
636
  * than hanging, then unwinds the reservation.
619
637
  */
620
638
  async startTurn(message) {
639
+ let route;
621
640
  try {
622
- await this.client.ensureStarted();
641
+ // Resolve the route and put it in force BEFORE the client is touched: a
642
+ // provider move is a different child, and a mid-turn selection change must
643
+ // land on the NEXT turn's applyRoute — never kill the running turn.
644
+ route = this.resolveRoute();
645
+ await this.applyRoute(route);
646
+ this.turnRoute = route;
623
647
  await this.bootstrapSessionIdentity();
624
648
  }
625
649
  catch (error) {
@@ -637,9 +661,11 @@ export class ClaudeAgent {
637
661
  if (this.disposed)
638
662
  return;
639
663
  this.session.append("turn/start", { turn: this.dashTurn });
640
- // Route metadata for the next request (`request/context`) lands before
641
- // the turn's messages, exactly as the reference loop stamps it.
642
- this.emitRequestContext();
664
+ // Route metadata for the next request lands before the turn's messages,
665
+ // exactly as the reference loop stamps it.
666
+ if (route !== undefined)
667
+ this.commitRequestHeader(route);
668
+ this.emitRequestContext(route);
643
669
  this.appendUserMessage(message);
644
670
  // Flush any idle-injection that was parked before this turn.
645
671
  for (const entry of this.remoteQueue.splice(0)) {
@@ -743,25 +769,132 @@ export class ClaudeAgent {
743
769
  * effective provider/model plus the model's context window when genuinely
744
770
  * known. Turn-enclosed (no step required) and deduped by route key.
745
771
  */
746
- emitRequestContext() {
772
+ emitRequestContext(route) {
747
773
  if (!this.turnOpen)
748
774
  return;
749
- const model = this.observedMessageModel || this.observedDefaultModel || this.options.model || "";
775
+ // A resolved route names the raw provider/model pair (the route the selector
776
+ // picked); a missing route falls back to the observed model (the degraded
777
+ // single-provider shape), preserving the pre-multi-provider metadata.
778
+ const model = route !== undefined
779
+ ? route.model
780
+ : (this.observedMessageModel || this.observedDefaultModel || this.options.model || "");
750
781
  if (model === "")
751
782
  return;
752
- const key = `${CLAUDE_PROVIDER_ID}/${model}`;
783
+ const provider = route?.provider ?? CLAUDE_PROVIDER_ID;
784
+ const key = `${provider}/${model}`;
753
785
  if (key === this.lastRouteKey)
754
786
  return;
755
787
  this.lastRouteKey = key;
756
- const entry = readModelCatalog().models.find((candidate) => candidate.id === model);
757
- const contextWindow = entry?.contextWindow;
788
+ const contextWindow = route !== undefined
789
+ ? (this.runtimeInfo?.contextWindowFor?.(route.provider, route.model)
790
+ ?? readModelCatalog().models.find((candidate) => candidate.id === route.model)?.contextWindow)
791
+ : undefined;
758
792
  this.session.append("request/context", {
759
- provider: CLAUDE_PROVIDER_ID,
793
+ provider,
760
794
  model,
761
795
  ...(contextWindow !== undefined && contextWindow > 0 ? { contextWindow } : {}),
762
796
  });
763
797
  trace(`request/context ${key}${contextWindow === undefined ? "" : ` ctx=${contextWindow}`}`);
764
798
  }
799
+ /**
800
+ * Resolve the route this session must run, mirroring the harness's own
801
+ * precedence (`pending ?? logged ?? preferred ?? fallback`). Each candidate is
802
+ * already a raw provider/model pair — the provider IS the route id and the
803
+ * model IS the raw wire id (the codex twin's shape). A provider id with no
804
+ * store entry reads as "launch native, inject nothing".
805
+ */
806
+ resolveRoute() {
807
+ const projections = this.loopCtx.get("sessionProjections");
808
+ const service = this.ctx.get("agentDefaultModel");
809
+ return resolveRoute({
810
+ pending: projections?.stateOf?.(this.session, "modelSelection")?.pending,
811
+ logged: this.session.requestHeader()?.config,
812
+ preferred: service?.currentSelection?.(),
813
+ fallback: routeSeed(this.options),
814
+ });
815
+ }
816
+ /**
817
+ * Put the resolved route in force on the client, swapping the child when the
818
+ * provider moved. Three cases, in increasing cost:
819
+ * 1. nothing spawned yet — pin the launch and let the first prompt spawn
820
+ * (the lazy-spawn gate: choosing a model must not cost a process);
821
+ * 2. same provider — `setModel` rides the live query with the RAW id;
822
+ * 3. different provider — the child is replaced (`reattach`).
823
+ */
824
+ async applyRoute(route) {
825
+ const launch = route === undefined ? undefined : this.runtimeInfo?.launchFor?.(route.provider, route.model);
826
+ // An unknown provider id (a legacy session pinned before the multi-provider
827
+ // rework) maps to "no injection": the user's own settings.json decides.
828
+ const launchSpec = launch ?? NATIVE_LAUNCH;
829
+ const providerId = launch === undefined ? null : route?.provider ?? null;
830
+ const client = this.client;
831
+ const key = `${providerId ?? "native"}/${route?.model ?? ""}`;
832
+ if (!client.spawned) {
833
+ client.setLaunch(launchSpec);
834
+ await client.ensureStarted();
835
+ }
836
+ else if ((client.appliedProvider ?? null) !== providerId) {
837
+ await this.reattach(launchSpec, providerId, route);
838
+ }
839
+ else if (route !== undefined) {
840
+ await client.setModel(route.model).catch((error) => {
841
+ trace(`set_model ${key} failed: ${String(error)}`);
842
+ });
843
+ }
844
+ this.routeKey = key;
845
+ }
846
+ /**
847
+ * Replace the client at a turn boundary: close the old one and build a fresh
848
+ * child through the injected factory, resuming the SAME SDK session id and
849
+ * re-wiring the event listener and the live permission mode.
850
+ */
851
+ async reattach(launch, _providerId, _route) {
852
+ const old = this.client;
853
+ const threadId = old.threadId;
854
+ this.clientUnsubscribe?.();
855
+ old.close();
856
+ if (this.clientFactory === undefined) {
857
+ throw new Error("agent-claude: cannot reattach — no clientFactory was injected");
858
+ }
859
+ const replacement = this.clientFactory({ ...(threadId !== null ? { resumeSessionId: threadId } : {}), launch });
860
+ this.clientUnsubscribe = replacement.on((event) => this.handleEvent(event));
861
+ this.client = replacement;
862
+ // Re-apply the live permission mode onto the fresh child (the old client's
863
+ // buffered control calls died with it).
864
+ const mode = this.runtimeInfo?.claudeMode ?? claudePermissionMode(presetFromEvents(this.session.snapshotEvents()));
865
+ if (mode !== undefined)
866
+ void replacement.setPermissionMode(mode).catch(() => { });
867
+ await replacement.ensureStarted();
868
+ }
869
+ /**
870
+ * Log the request header the NEXT request runs under, so the harness's
871
+ * `modelSelection` projection retires a pending switch when it matches.
872
+ */
873
+ commitRequestHeader(route) {
874
+ const header = canonicalHeader({
875
+ config: {
876
+ provider: route.provider,
877
+ model: route.model,
878
+ },
879
+ });
880
+ const baseline = this.session.requestHeader();
881
+ if (!this.requestHeaderLogged) {
882
+ this.session.append("request/header", { header, reason: baseline === undefined ? "initial" : "resume" });
883
+ this.requestHeaderLogged = true;
884
+ return;
885
+ }
886
+ if (baseline === undefined || !headerEquals(baseline, header)) {
887
+ this.session.append("request/header", { header, reason: "change" });
888
+ }
889
+ }
890
+ /** Test-only thin shell over {@link applyRoute}. */
891
+ async applyRouteForTest(route) {
892
+ await this.applyRoute(route);
893
+ }
894
+ /** Test-only thin shell over {@link resolveRoute}. */
895
+ resolveRouteForTest() {
896
+ return this.resolveRoute();
897
+ }
765
898
  /** The open stream bridge for this message, starting one on first use. */
766
899
  ensureStreamBridge() {
767
900
  if (this.streamBridge === undefined || this.streamBridge.ended) {
@@ -856,7 +989,7 @@ export class ClaudeAgent {
856
989
  const assistant = createAssistantMessage({
857
990
  content: blocks,
858
991
  // The OBSERVED model (what the CLI actually ran), not the requested one.
859
- source: { provider: CLAUDE_PROVIDER_ID, model: this.observedMessageModel || this.observedDefaultModel || this.options.model || "" },
992
+ source: { provider: this.turnRoute?.provider ?? CLAUDE_PROVIDER_ID, model: this.observedMessageModel || this.observedDefaultModel || this.options.model || "" },
860
993
  });
861
994
  const event = this.session.append("assistant/message", {
862
995
  turn: this.dashTurn,
@@ -905,13 +1038,17 @@ export class ClaudeAgent {
905
1038
  observeModelCatalog(init) {
906
1039
  if (typeof init.model === "string" && init.model !== "")
907
1040
  this.observedDefaultModel = init.model;
1041
+ // The observed catalog stays RAW: the fixed `claude` route serves exactly
1042
+ // what the CLI reported, and curated provider models live under their own
1043
+ // routes (2026-10-04 F-A) — no composite minting here.
908
1044
  const entries = catalogFromInit(init);
909
1045
  if (entries.length > 0)
910
1046
  setModelCatalog(entries, this.observedDefaultModel);
911
1047
  void this.client.supportedModels().then((models) => {
912
1048
  const full = Array.isArray(models) ? models.flatMap((m) => modelEntryFromSdk(m)) : [];
913
- if (full.length > 0)
914
- setModelCatalog(full, this.observedDefaultModel);
1049
+ if (full.length === 0)
1050
+ return;
1051
+ setModelCatalog(full, this.observedDefaultModel);
915
1052
  }).catch((error) => {
916
1053
  trace(`supportedModels() failed: ${String(error)}`);
917
1054
  });
@@ -1133,7 +1270,7 @@ export class ClaudeAgent {
1133
1270
  this.commitStepStart();
1134
1271
  const assistant = createAssistantMessage({
1135
1272
  content: blocks,
1136
- source: { provider: CLAUDE_PROVIDER_ID, model: String(event.fallbackModel ?? this.options.model ?? "") },
1273
+ source: { provider: this.turnRoute?.provider ?? CLAUDE_PROVIDER_ID, model: String(event.fallbackModel ?? this.options.model ?? "") },
1137
1274
  });
1138
1275
  this.session.append("assistant/message", {
1139
1276
  turn: this.dashTurn,
@@ -8,6 +8,15 @@
8
8
  * methods exist at all), drives the bridge lazily per session, and fans every
9
9
  * message out through `projectClaudeEvent` (claude-events.ts).
10
10
  *
11
+ * ## The SDK is runtime-self-fetched, not imported
12
+ *
13
+ * The SDK is not a static dependency of this package: the default factory
14
+ * resolves it at the first real prompt via `loadClaudeQuery(worldHome)`
15
+ * (sdk-runtime.ts), which fetches the pinned tree into the agent world's
16
+ * home. `ClaudeFactory` stays SYNCHRONOUS — the await lives in factory
17
+ * RESOLUTION, never between query construction and the `this.query`
18
+ * assignment (see `ensureStarted`).
19
+ *
11
20
  * ## The two hard deviations from the codex precedent
12
21
  *
13
22
  * 1. **No shared singleton.** `agent-codex` multiplexes all clients through one
@@ -54,38 +63,31 @@
54
63
  * every push is queued in order and there is no separate follow-up channel.
55
64
  * `steer` differs by pushing with `priority: "now"`.
56
65
  */
57
- import { existsSync } from "node:fs";
58
- import { homedir } from "node:os";
59
- import { join } from "node:path";
60
- import { query as officialQuery } from "@anthropic-ai/claude-agent-sdk";
66
+ import { loadClaudeQuery, resolveClaudeBinary } from "./sdk-runtime.js";
61
67
  import { InputQueue } from "./input-queue.js";
62
68
  import { projectClaudeEvent } from "./claude-events.js";
63
- /** Host Claude Code binary driven through the SDK bridge (spec ruling R5).
64
- * PATH-resolved like any host CLI: $CLAUDE_EXECUTABLE wins, else a bare
65
- * "claude" the SDK spawns through PATH. No machine-specific path ships. */
66
- export const DEFAULT_CLAUDE_EXECUTABLE = "claude";
67
- const defaultFactory = (o) => officialQuery({
68
- prompt: o.prompt,
69
- options: o.options,
70
- });
71
- const defaultExecutableResolver = () => process.env.CLAUDE_EXECUTABLE ?? resolveClaudeExecutable();
72
- /** PATH first, then the common native-installer locations (a service unit
73
- * PATH may not include ~/.local/bin — dev3 2026-09-22: "native binary not
74
- * found at claude" mid-session). */
75
- function resolveClaudeExecutable() {
76
- for (const dir of (process.env.PATH ?? "").split(":").filter(Boolean)) {
77
- if (existsSync(join(dir, "claude")))
78
- return DEFAULT_CLAUDE_EXECUTABLE;
79
- }
80
- for (const candidate of [join(homedir(), ".local/bin/claude"), "/usr/local/bin/claude"]) {
81
- if (existsSync(candidate))
82
- return candidate;
83
- }
84
- return DEFAULT_CLAUDE_EXECUTABLE;
85
- }
86
- let factory = defaultFactory;
69
+ import { NATIVE_LAUNCH } from "./claude-launch.js";
70
+ /** The default executable resolution: Task 1's `resolveClaudeBinary` —
71
+ * `$CLAUDE_EXECUTABLE` (only while it exists) > `claude` on PATH >
72
+ * `~/.local/bin/claude` > `/usr/local/bin/claude`. `undefined` means "no
73
+ * local binary": `pathToClaudeCodeExecutable` is left unset and the SDK's
74
+ * bundled platform payload is the CLI (bundled-payload mode). */
75
+ const defaultExecutableResolver = () => resolveClaudeBinary();
76
+ let factory; // test injection only
87
77
  let executableResolver = defaultExecutableResolver;
88
78
  let refcount = 0;
79
+ /** The factory to construct queries with: a test-injected one wins (the sync
80
+ * seam); otherwise the SDK is self-fetched into `worldHome`'s durable runtime
81
+ * on first use (sdk-runtime.ts). */
82
+ async function resolveFactory(worldHome) {
83
+ if (factory)
84
+ return factory;
85
+ if (worldHome === undefined) {
86
+ throw new Error("agent-claude: client needs worldHome to fetch its SDK runtime (the provider passes dshHomePath)");
87
+ }
88
+ const sdkQuery = await loadClaudeQuery(worldHome);
89
+ return (o) => sdkQuery({ prompt: o.prompt, options: o.options });
90
+ }
89
91
  /** Replace the SDK entry point (tests inject a fake query). */
90
92
  export function setClaudeFactory(next) {
91
93
  factory = next;
@@ -101,7 +103,7 @@ export function claudeClientRefCount() {
101
103
  /** Restore module-level seams and refcount (test isolation only). */
102
104
  export function resetClaudeClientState() {
103
105
  refcount = 0;
104
- factory = defaultFactory;
106
+ factory = undefined;
105
107
  executableResolver = defaultExecutableResolver;
106
108
  }
107
109
  /**
@@ -114,13 +116,15 @@ export function resetClaudeClientState() {
114
116
  * is the caller's to trace; the boot catalog stays as-is.
115
117
  */
116
118
  export async function probeClaudeModels(config = {}) {
119
+ const queryFactory = await resolveFactory(config.worldHome);
120
+ const executable = executableResolver();
117
121
  const queue = new InputQueue();
118
- const query = factory({
122
+ const query = queryFactory({
119
123
  prompt: queue,
120
124
  options: {
121
125
  cwd: config.cwd ?? process.cwd(),
122
126
  env: { ...process.env },
123
- pathToClaudeCodeExecutable: executableResolver(),
127
+ ...(executable !== undefined ? { pathToClaudeCodeExecutable: executable } : {}),
124
128
  persistSession: false,
125
129
  },
126
130
  });
@@ -145,10 +149,13 @@ export async function probeClaudeModels(config = {}) {
145
149
  }
146
150
  export class ClaudeSdkClient {
147
151
  cwdPath;
152
+ worldHome;
148
153
  claudeSessionId;
149
154
  resumeSessionId;
150
155
  canUseTool;
151
156
  allowDangerouslySkipPermissions;
157
+ /** The provider-pinning launch this client will spawn under (NATIVE_LAUNCH until `setLaunch`). */
158
+ launch;
152
159
  /** True while a user turn is in flight (set on push, cleared on `turn_end`/`agent_end`). */
153
160
  turnActive = false;
154
161
  queue;
@@ -164,10 +171,12 @@ export class ClaudeSdkClient {
164
171
  throw new Error("agent-claude: claudeSessionId and resumeSessionId are mutually exclusive");
165
172
  }
166
173
  this.cwdPath = config.cwd ?? process.cwd();
174
+ this.worldHome = config.worldHome;
167
175
  this.claudeSessionId = config.claudeSessionId;
168
176
  this.resumeSessionId = config.resumeSessionId;
169
177
  this.canUseTool = config.canUseTool;
170
178
  this.allowDangerouslySkipPermissions = config.allowDangerouslySkipPermissions;
179
+ this.launch = config.launch ?? NATIVE_LAUNCH;
171
180
  refcount += 1;
172
181
  }
173
182
  /**
@@ -186,6 +195,21 @@ export class ClaudeSdkClient {
186
195
  get threadId() {
187
196
  return this.threadIdValue;
188
197
  }
198
+ /** The provider id this client was last told to spawn under (constructor fact; `null` = native). */
199
+ get appliedProvider() {
200
+ return this.launch.providerId;
201
+ }
202
+ /**
203
+ * Pin the launch this client will spawn under. Only legal before the query
204
+ * exists: once the CLI subprocess is up, a provider move is a different child
205
+ * (the agent swaps the whole client at a turn boundary — see `#reattach`).
206
+ */
207
+ setLaunch(launch) {
208
+ if (this.query !== undefined) {
209
+ throw new Error("agent-claude: setLaunch after spawn is not allowed (a provider move swaps the client)");
210
+ }
211
+ this.launch = launch;
212
+ }
189
213
  /** The client's own running/streaming attestation (idle-TTL revalidation gate). */
190
214
  async getState() {
191
215
  return { isStreaming: this.turnActive };
@@ -221,22 +245,27 @@ export class ClaudeSdkClient {
221
245
  };
222
246
  }
223
247
  /**
224
- * Lazily construct the query (zero-IO until this runs). Everything up to the
225
- * `this.query = query` assignment is synchronous, so concurrent first prompts
226
- * can never build two queries: the first caller assigns `this.query` before
227
- * any `await` yields control. This single-query invariant therefore depends
228
- * on the `ClaudeFactory` contract staying synchronous (it must return the
229
- * `Query` without awaiting). The reader loop is started fire-and-forget before
230
- * any pending control call is replayed, so a throwing control can never leave
231
- * the CLI undrained.
248
+ * Lazily construct the query (zero-IO until this runs). The factory is
249
+ * resolved first — an await (a test-injected seam, or the world-home SDK
250
+ * self-fetch) — and a caller that loses the race during that await reuses
251
+ * the winner's query at the re-check. From the construct line through the
252
+ * `this.query = query` assignment everything is synchronous (zero awaits),
253
+ * so concurrent first prompts can never build two queries. The single-query
254
+ * invariant therefore still depends on the `ClaudeFactory` contract staying
255
+ * synchronous (it must return the `Query` without awaiting). The reader loop
256
+ * is started fire-and-forget before any pending control call is replayed, so
257
+ * a throwing control can never leave the CLI undrained.
232
258
  */
233
259
  async ensureStarted() {
234
260
  if (this.query !== undefined)
235
261
  return;
262
+ const f = await resolveFactory(this.worldHome); // await BEFORE the sync section
263
+ if (this.query !== undefined)
264
+ return; // lost the race to a concurrent first prompt — reuse its query
236
265
  const queue = new InputQueue();
237
- const query = factory({ prompt: queue, options: this.buildOptions() });
266
+ const query = f({ prompt: queue, options: this.buildOptions() });
238
267
  this.queue = queue;
239
- this.query = query;
268
+ this.query = query; // sync section: zero awaits before this assignment (invariant)
240
269
  void this.runReader(query);
241
270
  const controls = this.pendingControls;
242
271
  this.pendingControls = [];
@@ -303,11 +332,23 @@ export class ClaudeSdkClient {
303
332
  }
304
333
  // --- internals ------------------------------------------------------------
305
334
  buildOptions() {
335
+ const launchExecutable = this.launch.pathToClaudeCodeExecutable;
336
+ const executable = launchExecutable !== undefined && launchExecutable !== "" ? launchExecutable : executableResolver();
306
337
  return {
307
338
  cwd: this.cwdPath,
308
- env: { ...process.env },
309
- pathToClaudeCodeExecutable: executableResolver(),
339
+ // The launch env holds ADDITIONS only; spreading process.env first keeps
340
+ // PATH/HOME and the user's own key alive (NATIVE_LAUNCH carries an empty
341
+ // env, so the native shape is exactly `{ ...process.env }`).
342
+ env: { ...process.env, ...this.launch.env },
343
+ // The `--settings` command-line tier (a JSON string) outranks the user's
344
+ // native settings.json env, so a provider-pinned launch rides it here
345
+ // (2026-10-04 F-B: without it the native env pins the endpoint).
346
+ ...(this.launch.settings !== undefined ? { settings: this.launch.settings } : {}),
347
+ // undefined (no local binary) leaves the key unset — bundled-payload
348
+ // mode lets the SDK spawn its own platform payload as the CLI.
349
+ ...(executable !== undefined ? { pathToClaudeCodeExecutable: executable } : {}),
310
350
  persistSession: true,
351
+ ...(this.launch.model !== undefined ? { model: this.launch.model } : {}),
311
352
  ...(this.claudeSessionId !== undefined ? { sessionId: this.claudeSessionId } : {}),
312
353
  ...(this.resumeSessionId !== undefined ? { resume: this.resumeSessionId } : {}),
313
354
  ...(this.canUseTool !== undefined ? { canUseTool: this.canUseTool } : {}),
@@ -0,0 +1,101 @@
1
+ /**
2
+ * claude-launch.ts — how a provider reaches the Claude Code CLI
3
+ * (2026-10-04 multi-provider rework).
4
+ *
5
+ * Claude Code is a single-provider harness: its own `settings.json` env block
6
+ * names exactly one `ANTHROPIC_BASE_URL` with one key, so the LIST of
7
+ * configured providers lives in `claude-providers.ts` instead. This module is
8
+ * the other end of that store: given the route the next turn will run (DSH's
9
+ * own per-session model selection, read by `claude-route.ts`), the resolved
10
+ * provider entry, and the resolved host binary, it produces the slice Task 9
11
+ * merges into the SDK's `Options`.
12
+ *
13
+ * The child's environment is the ONE channel a provider's variables enter the
14
+ * process through (spec §2.2) — AND, for a provider-pinned launch, the same env
15
+ * is re-carried as a `--settings`-tier JSON string so the command-line tier
16
+ * outranks the user's native `~/.claude/settings.json` env (2026-10-04 F-B).
17
+ * Concretely, the env keys are:
18
+ *
19
+ * - `ANTHROPIC_BASE_URL` — always, from the entry (validation guarantees a
20
+ * non-empty endpoint; an entry without one is not an entry).
21
+ * - `ANTHROPIC_AUTH_TOKEN` for `authToken` (Bearer), `ANTHROPIC_API_KEY` for
22
+ * `apiKey` (x-api-key) — exactly one, chosen by the entry's auth KIND, with
23
+ * the stored value verbatim. The kind names the channel, so the variable is
24
+ * present even when the value is empty ("keyed outside Super D"): the CLI
25
+ * reads both names with `.trim() || undefined` (measured in
26
+ * `@anthropic-ai/claude-agent-sdk` 2.1.263), so an empty value is unset
27
+ * rather than a fabricated secret.
28
+ * - `ANTHROPIC_MODEL` — the RAW route model id (`glm-5.2`), never the
29
+ * selector's composite key (`glm/glm-5.2`); the composite is a DSH catalog
30
+ * id and has no meaning on the wire.
31
+ * - `ANTHROPIC_DEFAULT_{SONNET,OPUS,HAIKU}_MODEL` — ONLY for aliases the
32
+ * entry actually declares, and never an empty-string one: an alias the user
33
+ * did not write must not be invented, or the child silently answers on a
34
+ * model they never chose.
35
+ *
36
+ * The returned env holds ADDITIONS only. The caller composes the child
37
+ * environment as `{ ...process.env, ...launch.env }` — the spread AFTER
38
+ * `process.env` is the contract, and it is why this function never reads or
39
+ * mutates `process.env` itself: it is a pure declaration, and the caller's
40
+ * precedence is what makes it effective. `NATIVE_LAUNCH` is the same
41
+ * declaration with nothing in it: `providerId: null` and an empty env, for a
42
+ * route no provider entry answers (a legacy bare-id selection, the native
43
+ * settings-only shape).
44
+ *
45
+ * The same reasoning makes `pathToClaudeCodeExecutable` optional and
46
+ * conditional: it is set only when a host binary was actually resolved, so an
47
+ * unresolved binary omits the key entirely and lets the SDK fall back to its
48
+ * bundled payload (spec §2.1) instead of spawning an empty path.
49
+ *
50
+ * @module agent-claude/claude-launch
51
+ */
52
+ /** The launch for "no provider injection at all" (the native settings apply). */
53
+ export const NATIVE_LAUNCH = { providerId: null, env: {} };
54
+ /** The variable each auth kind authenticates through. */
55
+ const AUTH_ENV = {
56
+ authToken: "ANTHROPIC_AUTH_TOKEN",
57
+ apiKey: "ANTHROPIC_API_KEY",
58
+ };
59
+ /** The alias slots an entry may declare, in catalog order. */
60
+ const ALIAS_ENV = [
61
+ ["sonnet", "ANTHROPIC_DEFAULT_SONNET_MODEL"],
62
+ ["opus", "ANTHROPIC_DEFAULT_OPUS_MODEL"],
63
+ ["haiku", "ANTHROPIC_DEFAULT_HAIKU_MODEL"],
64
+ ];
65
+ /**
66
+ * Turn one resolved provider into the SDK `Options` slice for one route.
67
+ *
68
+ * Pure: reads no file, no env, no clock, and spawns nothing. The provider id
69
+ * comes from the ENTRY (the caller resolves the entry BY `route.provider`, so
70
+ * the label and the injected data stay one fact); the model comes from the
71
+ * ROUTE, because that is what the session selected.
72
+ *
73
+ * @param route - the route the next turn runs (raw model half).
74
+ * @param entry - the provider entry that route resolved to.
75
+ * @param hostBinary - the resolved Claude Code binary, or `undefined` to let
76
+ * the SDK use its bundled payload.
77
+ * @returns the env additions, the wire model, and the optional binary path.
78
+ */
79
+ export function claudeLaunchFor(route, entry, hostBinary) {
80
+ const env = {
81
+ ANTHROPIC_BASE_URL: entry.baseUrl,
82
+ [AUTH_ENV[entry.auth.kind]]: entry.auth.value,
83
+ ANTHROPIC_MODEL: route.model,
84
+ };
85
+ // Declared aliases only: `undefined` means the entry never named that slot,
86
+ // and "" is not a model id — emitting either would invent a choice.
87
+ for (const [slot, name] of ALIAS_ENV) {
88
+ const value = entry.aliases?.[slot];
89
+ if (value !== undefined && value !== "")
90
+ env[name] = value;
91
+ }
92
+ return {
93
+ providerId: entry.id,
94
+ env,
95
+ settings: JSON.stringify({ env }),
96
+ model: route.model,
97
+ // Spread, not `pathToClaudeCodeExecutable: hostBinary`: an absent key must
98
+ // not exist at all, or it would ride a spread into Options as `undefined`.
99
+ ...(hostBinary !== undefined && hostBinary !== "" ? { pathToClaudeCodeExecutable: hostBinary } : {}),
100
+ };
101
+ }