super-dsh 0.1.9 → 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 (178) 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 +1204 -93
  162. package/cordis.patch.yml +64 -0
  163. package/package.json +17 -8
  164. package/superd-hub/dist/client/agent-settings.d.ts +210 -0
  165. package/superd-hub/dist/client/agent-settings.js +300 -0
  166. package/superd-hub/dist/client/roster.js +6 -0
  167. package/superd-hub/dist/client/runtime-settings-api.d.ts +76 -0
  168. package/superd-hub/dist/client/runtime-settings-api.js +137 -0
  169. package/superd-hub/dist/client/switch.d.ts +165 -0
  170. package/superd-hub/dist/client/switch.js +295 -0
  171. package/superd-hub/lib/client/index.js +1204 -93
  172. package/agent-codex/dist/models.js +0 -152
  173. package/agent-omp/dist/sidecar-client.js +0 -195
  174. /package/{agent-agy → agent-agy-sdk}/bridge/agy_bridge.py +0 -0
  175. /package/{agent-agy → agent-agy-sdk}/bridge/gemini_openai_server.mjs +0 -0
  176. /package/{agent-codex/dist/codex-events.js → agent-codex-sdk/dist/codex-sdk-events.js} +0 -0
  177. /package/{agent-omp → agent-omp-sdk}/dist/protocol.js +0 -0
  178. /package/{agent-omp → agent-omp-sdk}/sidecar/main.ts +0 -0
@@ -15,37 +15,162 @@
15
15
  * (SDK env is REPLACE semantics — Task 1 spike fact, so the spread is
16
16
  * mandatory) and carries NO `CODEX_HOME`: the CLI resolves its own native
17
17
  * `~/.codex` (user ruling 2026-09-17 — never redirect the native app home at
18
- * spawn; tests redirect via `process.env.CODEX_HOME`). The SDK constructor
19
- * itself is injectable via {@link setCodexFactory}.
18
+ * spawn; tests redirect via `process.env.CODEX_HOME`).
19
+ *
20
+ * 2026-10-04 user ruling: this package declares NO SDK dependency — the whole
21
+ * runtime is fetched into the world home on the first real prompt
22
+ * (sdk-runtime.ts). The default factory therefore returns a zero-IO
23
+ * {@link DeferredCodex} that materializes the real SDK inside `ensureStarted()`
24
+ * (and, defensively, on the first `runStreamed`): constructing a client — which
25
+ * the provider does at session-create time — touches no disk and no network,
26
+ * and a session that never gets a prompt never fetches anything.
27
+ * {@link setCodexFactory} replaces that construction wholesale, so the unit
28
+ * suite stays green with the SDK absent.
20
29
  *
21
30
  * Wire events flow through `projectThreadEvent` (codex-events.ts) before
22
31
  * reaching listeners, so Task 7's agent.ts switch is unchanged from omp.
23
32
  */
24
- import { Codex } from "@openai/codex-sdk";
25
- import { projectThreadEvent } from "./codex-events.js";
26
- let codexFactory = (options) => new Codex(options);
27
- /** Replace the SDK constructor (tests inject a fake Codex). */
33
+ import { loadCodexSdk } from "./sdk-runtime.js";
34
+ import { NATIVE_SDK_LAUNCH } from "./codex-sdk-launch.js";
35
+ import { projectThreadEvent } from "./codex-sdk-events.js";
36
+ /**
37
+ * The production handle: a stand-in that constructs the REAL SDK entry on first
38
+ * materialization. Deferring is what keeps session-create at zero IO while
39
+ * still letting the fetch happen exactly once, from `ensureStarted()`.
40
+ */
41
+ class DeferredCodex {
42
+ #build;
43
+ #real = null;
44
+ constructor(build) {
45
+ this.#build = build;
46
+ }
47
+ /** Fetch (first time) + construct the real instance; resolves when usable. */
48
+ warm() {
49
+ return this.#realCodex().then(() => undefined);
50
+ }
51
+ startThread(options) {
52
+ return new DeferredThread(() => this.#realCodex().then((real) => real.startThread(options)));
53
+ }
54
+ resumeThread(id, options) {
55
+ return new DeferredThread(() => this.#realCodex().then((real) => real.resumeThread(id, options)));
56
+ }
57
+ #realCodex() {
58
+ if (this.#real === null) {
59
+ // A rejected bootstrap must not be cached: the runtime fetch heals on the
60
+ // next call (a failure leaves no marker), so a retried prompt can succeed.
61
+ this.#real = this.#build().catch((error) => {
62
+ this.#real = null;
63
+ throw error;
64
+ });
65
+ }
66
+ return this.#real;
67
+ }
68
+ }
69
+ /** The thread half of {@link DeferredCodex}: materializes on first `runStreamed`. */
70
+ class DeferredThread {
71
+ #build;
72
+ #target = null;
73
+ #materialize = null;
74
+ constructor(build) {
75
+ this.#build = build;
76
+ }
77
+ get id() {
78
+ return this.#target?.id ?? null;
79
+ }
80
+ async runStreamed(input, turnOptions) {
81
+ // Local narrowing: the materializer resolves inside a callback, so TS
82
+ // cannot see the field become non-null across the await.
83
+ let target = this.#target;
84
+ if (target === null) {
85
+ this.#materialize ??= this.#build()
86
+ .then((resolved) => {
87
+ this.#target = resolved;
88
+ return resolved;
89
+ })
90
+ .catch((error) => {
91
+ this.#materialize = null;
92
+ throw error;
93
+ });
94
+ target = await this.#materialize;
95
+ }
96
+ return target.runStreamed(input, turnOptions);
97
+ }
98
+ }
99
+ /**
100
+ * The default factory: the deferred handle wired to the durable runtime. A
101
+ * world home is mandatory on this path — deriving one from ambient env inside
102
+ * the client is the prod-home trap the adapter rules forbid (the provider owns
103
+ * the resolution and passes it in).
104
+ */
105
+ const defaultCodexFactory = (options, worldHome) => {
106
+ if (worldHome === "") {
107
+ throw new Error("codex-sdk: the client needs the world home to fetch its SDK runtime into (the provider passes dshHomePath)");
108
+ }
109
+ return new DeferredCodex(async () => {
110
+ const sdk = await loadCodexSdk(worldHome);
111
+ // `options` already carries the provider's config overrides (when it has
112
+ // any) and the env the caller composed — the SDK is constructed exactly as
113
+ // the pinning launch asked, on first materialization.
114
+ return new sdk.Codex(options);
115
+ });
116
+ };
117
+ let codexFactory = defaultCodexFactory;
118
+ /** Replace the SDK construction (tests inject a fake: zero fetch, zero disk). */
28
119
  export function setCodexFactory(factory) {
29
120
  codexFactory = factory;
30
121
  }
31
122
  // ---------------------------------------------------------------------------
32
- // Shared Codex instance (module-level singleton, refcounted — omp sidecar pattern)
123
+ // Shared Codex instances (module-level, refcounted PER PROVIDER — omp sidecar
124
+ // pattern, 2026-10-04 multi-provider rework)
33
125
  // ---------------------------------------------------------------------------
34
- let sharedCodex = null;
126
+ /**
127
+ * Registry key for "let the native `config.toml` decide". Also the key every
128
+ * client starts on: the route is not known until the first turn resolves it.
129
+ */
130
+ const NATIVE_KEY = "native";
131
+ /**
132
+ * One entry per PROVIDER, not one for the process: a provider is a property of
133
+ * the config object the SDK is constructed with, so clients on different
134
+ * providers must not share an instance. Clients on the same provider still do —
135
+ * that is what keeps a many-session world at one SDK handle per provider.
136
+ */
137
+ const sharedCodex = new Map();
35
138
  let refcount = 0;
36
- function acquire() {
37
- if (sharedCodex === null) {
38
- sharedCodex = codexFactory({ env: { ...process.env } });
139
+ /**
140
+ * Refcount one provider's handle, constructing it on first use. The construction
141
+ * is cheap by contract: the default factory returns a {@link DeferredCodex}
142
+ * (zero disk, zero network) and a test factory returns a plain fake. The REAL
143
+ * instance — and therefore the runtime fetch — is materialized later, from
144
+ * `ensureStarted()`.
145
+ *
146
+ * The first launch for a key wins. That is sound because the key IS the
147
+ * provider: two launches on one key carry the same provider table, the same
148
+ * catalog path and the same synthetic key name, so the handle they would build
149
+ * is the same handle.
150
+ */
151
+ function acquire(key, worldHome, launch) {
152
+ let entry = sharedCodex.get(key);
153
+ if (entry === undefined) {
154
+ entry = {
155
+ codex: codexFactory({ env: { ...process.env }, ...(Object.keys(launch.config).length === 0 ? {} : { config: launch.config }) }, worldHome, launch),
156
+ refs: 0,
157
+ };
158
+ sharedCodex.set(key, entry);
39
159
  }
160
+ entry.refs += 1;
40
161
  refcount += 1;
41
- return sharedCodex;
162
+ return entry.codex;
42
163
  }
43
- function release() {
164
+ function release(key) {
165
+ const entry = sharedCodex.get(key);
166
+ if (entry === undefined)
167
+ return;
168
+ entry.refs -= 1;
44
169
  refcount = Math.max(0, refcount - 1);
45
- if (refcount === 0)
46
- sharedCodex = null;
170
+ if (entry.refs <= 0)
171
+ sharedCodex.delete(key);
47
172
  }
48
- /** Test/observability hook: current shared-instance refcount (0 = none alive). */
173
+ /** Test/observability hook: current handle refcount across providers (0 = none alive). */
49
174
  export function codexClientRefCount() {
50
175
  return refcount;
51
176
  }
@@ -84,7 +209,22 @@ function parseSpawnArgs(args) {
84
209
  }
85
210
  // ---------------------------------------------------------------------------
86
211
  export class CodexSdkClient {
212
+ /** The handle in force; swapped when the route moves to another provider. */
87
213
  #codex;
214
+ /**
215
+ * The world home the SDK runtime is fetched into. Held (not just borrowed at
216
+ * spawn) because a provider switch constructs the NEXT handle from it.
217
+ */
218
+ #worldHome;
219
+ /**
220
+ * The launch the next turn must run under, and the registry key it maps to.
221
+ * `applyLaunch` is the only writer; both start native, because the route is
222
+ * not known until the first turn resolves it.
223
+ */
224
+ #launch = NATIVE_SDK_LAUNCH;
225
+ #launchKey = NATIVE_KEY;
226
+ /** The key `#codex` is currently a handle of (what `close()` must release). */
227
+ #appliedProvider = NATIVE_KEY;
88
228
  #listeners = new Set();
89
229
  #failureListeners = new Set();
90
230
  #threadOptions;
@@ -102,8 +242,11 @@ export class CodexSdkClient {
102
242
  #lastUsage = null;
103
243
  /** Bumped by newSession()/close() to orphan runs started on an earlier thread. */
104
244
  #sessionEpoch = 0;
105
- constructor(codex, cwd, approvalMode, resumeThreadId) {
245
+ /** One-shot latch for `ensureStarted()` — the runtime fetch happens there. */
246
+ #startPromise = null;
247
+ constructor(codex, cwd, approvalMode, resumeThreadId, worldHome = "") {
106
248
  this.#codex = codex;
249
+ this.#worldHome = worldHome;
107
250
  this.#approvalMode = approvalMode;
108
251
  // skipGitRepoCheck: adapters spawn from arbitrary test/worktree cwds (Task 1 spike precedent).
109
252
  // Launch-only approval mapping (Task 7): present only when the provider
@@ -119,17 +262,25 @@ export class CodexSdkClient {
119
262
  resumeThreadId === undefined ? codex.startThread(this.#threadOptions) : codex.resumeThread(resumeThreadId, this.#threadOptions);
120
263
  }
121
264
  /**
122
- * Start a session on the shared Codex instance and resolve once ready.
265
+ * Construct a client on the shared handle. ZERO IO by contract: the default
266
+ * factory hands back a deferred SDK stand-in, so neither the runtime fetch
267
+ * nor the real SDK construction happens here — `ensureStarted()` owns both.
268
+ * A session that is created and never prompted therefore fetches nothing.
123
269
  * Mirrors `OmpSdkClient.spawn(args, cwd)`.
124
270
  */
125
- static async spawn(args = [], cwd) {
271
+ static async spawn(args = [], cwd, worldHome) {
126
272
  const { approvalMode, resumeThreadId } = parseSpawnArgs(args);
127
- const codex = acquire();
273
+ // Born on the native key: the route is not known until the first turn
274
+ // resolves it, and `applyLaunch` moves this client onto the provider it
275
+ // names. The handle is deferred either way, so nothing is constructed,
276
+ // fetched or even decided by this call.
277
+ const home = worldHome ?? "";
278
+ const codex = acquire(NATIVE_KEY, home, NATIVE_SDK_LAUNCH);
128
279
  try {
129
- return new CodexSdkClient(codex, cwd, approvalMode, resumeThreadId);
280
+ return new CodexSdkClient(codex, cwd, approvalMode, resumeThreadId, home);
130
281
  }
131
282
  catch (error) {
132
- release();
283
+ release(NATIVE_KEY);
133
284
  throw error;
134
285
  }
135
286
  }
@@ -273,7 +424,7 @@ export class CodexSdkClient {
273
424
  async setModel(provider, modelId) {
274
425
  this.#ensureUsable();
275
426
  void provider;
276
- this.#pendingModel = modelId;
427
+ this.#pendingModel = this.#modelOverride(modelId);
277
428
  return ok("set_model");
278
429
  }
279
430
  /** Abort the current streaming run (AbortController per run, turn-level signal). */
@@ -300,9 +451,27 @@ export class CodexSdkClient {
300
451
  this.#appliedModel = null; // a pending model still applies at the next run
301
452
  return ok("new_session");
302
453
  }
303
- /** Lazy-surface parity: a real client is born live (see OmpAgentRpc.spawned). */
304
- ensureStarted() {
305
- return Promise.resolve();
454
+ /**
455
+ * Materialize the durable SDK runtime (fetch on first call, then free) and
456
+ * the real SDK instance behind it. This is the ONLY place the production path
457
+ * may touch the network — `create()`/`spawn()` stay zero-IO.
458
+ *
459
+ * A failure is NOT memoized: the runtime bootstrap leaves no marker and heals
460
+ * on the next call, so a later prompt in the same session retries instead of
461
+ * being permanently bricked.
462
+ */
463
+ async ensureStarted() {
464
+ if (this.#startPromise === null) {
465
+ this.#startPromise = (async () => {
466
+ const warm = this.#codex.warm;
467
+ if (typeof warm === "function")
468
+ await warm.call(this.#codex);
469
+ })().catch((error) => {
470
+ this.#startPromise = null;
471
+ throw error;
472
+ });
473
+ }
474
+ return this.#startPromise;
306
475
  }
307
476
  /**
308
477
  * The current Codex thread id, exactly as the SDK thread exposes it (null
@@ -312,7 +481,12 @@ export class CodexSdkClient {
312
481
  get threadId() {
313
482
  return this.#thread.id;
314
483
  }
315
- /** Lazy-surface parity: a real client is born live (see OmpAgentRpc.spawned). */
484
+ /**
485
+ * Lazy-surface parity: a real client is born live (see OmpAgentRpc.spawned).
486
+ * Kept `true` on the deferred path on purpose: the client's whole surface is
487
+ * usable from construction, and `ensureStarted()` only warms the runtime —
488
+ * this flag was never the "has the fetch happened" bit.
489
+ */
316
490
  get spawned() {
317
491
  return true;
318
492
  }
@@ -323,7 +497,7 @@ export class CodexSdkClient {
323
497
  this.#closed = true;
324
498
  this.#sessionEpoch += 1; // a settled run must not drain the queue after close
325
499
  this.#currentAbort?.abort();
326
- release();
500
+ release(this.#appliedProvider);
327
501
  this.#fail(new Error("codex sdk client closed"));
328
502
  }
329
503
  // -------------------------------------------------------------------------
@@ -361,7 +535,7 @@ export class CodexSdkClient {
361
535
  const controller = new AbortController();
362
536
  this.#currentAbort = controller;
363
537
  try {
364
- this.#applyPendingModel();
538
+ this.#reconcileRoute();
365
539
  const streamed = await this.#thread.runStreamed(message, { signal: controller.signal });
366
540
  for await (const raw of streamed.events) {
367
541
  const record = asRecord(raw);
@@ -400,17 +574,64 @@ export class CodexSdkClient {
400
574
  }
401
575
  }
402
576
  }
403
- /** Rebuild the thread with the pending model when one is pending (next run). */
404
- #applyPendingModel() {
405
- if (this.#pendingModel === this.#appliedModel)
577
+ /**
578
+ * Put the resolved route in force for the next run: swap the handle if the
579
+ * PROVIDER moved, then rebuild the thread object against it.
580
+ *
581
+ * Cheaper than the app-server line's equivalent by construction. There, a
582
+ * provider is an argv-level property of a LONG-LIVED child, so a switch means
583
+ * killing it and re-attaching the thread to a new one (`reattach`). Here the
584
+ * SDK spawns `codex exec` (or `codex exec resume <id>`) PER RUN, so the
585
+ * provider is a property of the handle we are about to ask for a run — the
586
+ * switch is just "use the other handle from now on", and the thread keeps its
587
+ * identity because the id is a rollout on disk that both handles read.
588
+ *
589
+ * That also means a mid-turn switch lands on the next turn for free, exactly
590
+ * as the app-server line promises: the user message already dispatched keeps
591
+ * the handle it started with, and the next `#runTurn` re-reads the route.
592
+ */
593
+ #reconcileRoute() {
594
+ const key = this.#launchKey;
595
+ const providerMoved = key !== this.#appliedProvider;
596
+ if (!providerMoved && this.#pendingModel === this.#appliedModel)
406
597
  return;
598
+ if (providerMoved) {
599
+ this.#codex = acquire(key, this.#worldHome, this.#launch);
600
+ release(this.#appliedProvider);
601
+ this.#appliedProvider = key;
602
+ }
407
603
  const model = this.#pendingModel;
408
604
  const options = model === null ? { ...this.#threadOptions } : { ...this.#threadOptions, model };
605
+ // Resume by id whenever the thread has one: that is what carries the
606
+ // conversation across BOTH a model change and a provider change.
409
607
  const currentId = this.#thread.id;
410
608
  this.#thread =
411
609
  currentId !== null ? this.#codex.resumeThread(currentId, options) : this.#codex.startThread(options);
412
610
  this.#appliedModel = model;
413
611
  }
612
+ /**
613
+ * Pin the provider the NEXT run must use (2026-10-04 multi-provider rework).
614
+ *
615
+ * Called by the agent at turn open with the launch it resolved, so the client
616
+ * never has to know what a provider IS — it is handed a config and an env and
617
+ * runs on them. `providerId` is `null` for "let the native `config.toml`
618
+ * decide", which is what a session pinned to the retired single `codex` route
619
+ * resolves to.
620
+ */
621
+ applyRoute(launch, providerId, model) {
622
+ this.#launch = launch;
623
+ this.#launchKey = providerId ?? NATIVE_KEY;
624
+ this.#pendingModel = this.#modelOverride(model);
625
+ }
626
+ /**
627
+ * An empty model id means "pin no model", not "pin the empty model": the
628
+ * native `config.toml` then decides, which is what a route the adapter cannot
629
+ * resolve falls back to. Letting `model=""` through would emit
630
+ * `--config model=""` and codex would reject every turn.
631
+ */
632
+ #modelOverride(model) {
633
+ return model === "" ? null : model;
634
+ }
414
635
  /** Mirror wire events into the getMessages accumulator (best effort). */
415
636
  #mirror(wire) {
416
637
  if (wire.type === "message_end") {
@@ -0,0 +1,92 @@
1
+ /**
2
+ * codex-sdk-launch.ts — how a provider reaches the in-process Codex SDK
3
+ * (2026-10-04 multi-provider rework).
4
+ *
5
+ * This is the SDK half of `agent-codex/codex-launch.ts`. That module turns a
6
+ * provider into argv for a `codex app-server` child; this one turns the same
7
+ * provider into the structured object the SDK's `CodexOptions.config` takes —
8
+ * and underneath they are the same mechanism. The SDK's own type docs say it
9
+ * "flatten[s] it into dotted paths and serialize[s] values as TOML literals so
10
+ * they are compatible with the CLI's `--config` parsing", which is exactly the
11
+ * `--config key=value` channel the app-server line drives by hand. A provider
12
+ * therefore costs nothing on disk on this path either, and is scoped to the
13
+ * process that builds the config, which is what a per-session provider choice
14
+ * needs.
15
+ *
16
+ * The one transport difference is env. A spawned app-server child inherits the
17
+ * process environment, so `codex-launch.ts` hands back ADDITIONS and its caller
18
+ * spreads them (`env: { ...process.env, ...launch.env }`). The SDK's `env` is
19
+ * REPLACE semantics — "When provided, the SDK will not inherit variables from
20
+ * `process.env`" — so the caller must spread the same way before constructing
21
+ * `Codex`. This module still hands back additions only: composing the full map
22
+ * belongs to the caller, and keeping the addition set small is what makes the
23
+ * synthetic-key branch legible in tests.
24
+ *
25
+ * The measured constraints `codex-launch.ts` encodes carry over unchanged (see
26
+ * `docs/test-reports/2026-10-03-codex-provider-model-switch-probe.md`):
27
+ *
28
+ * - a HYPHENATED id is fine in a dotted path (`model_providers.zhipu-cn.…`),
29
+ * so native table names survive verbatim as provider ids;
30
+ * - `model_catalog_json` is resolved against the process CWD when it arrives
31
+ * as an override, so the path is always absolute;
32
+ * - `env_key` is the one auth shape that never consults the auth manager, so a
33
+ * stored API key rides the child's environment under a synthetic variable
34
+ * name. That is also what makes a keyed provider usable behind a ChatGPT
35
+ * login, where `requires_openai_auth` is silently shadowed.
36
+ *
37
+ * @module agent-codex-sdk/codex-sdk-launch
38
+ */
39
+ import { writeProviderCatalog } from "./codex-native-config.js";
40
+ /** The environment variable prefix for a key this adapter injects itself. */
41
+ const SYNTHETIC_KEY_PREFIX = "SUPERD_CODEX_KEY_";
42
+ /** The launch for "no provider injection at all" (the native config applies). */
43
+ export const NATIVE_SDK_LAUNCH = { config: {}, env: {}, providerId: "" };
44
+ /** The environment variable a stored API key is handed to the child as. */
45
+ export function syntheticKeyName(providerId) {
46
+ return `${SYNTHETIC_KEY_PREFIX}${providerId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
47
+ }
48
+ /**
49
+ * The launch that pins one provider for the life of an SDK process.
50
+ *
51
+ * @param worldHome - the world's DSH home (our per-provider catalogs live there).
52
+ * @param codexHome - the native codex home; READ ONLY (catalog template material).
53
+ * @param provider - the provider the SDK must run on.
54
+ * @returns the structured config, the environment additions, and the provider id they pin.
55
+ */
56
+ export function providerSdkLaunch(worldHome, codexHome, provider) {
57
+ // The SDK serializes these values as TOML literals itself, so unlike the argv
58
+ // line there is no hand-quoting here: a bare object is the whole override.
59
+ const table = {
60
+ name: provider.label,
61
+ base_url: provider.baseUrl,
62
+ wire_api: provider.protocol,
63
+ };
64
+ const env = {};
65
+ if (provider.auth.kind === "env") {
66
+ table["env_key"] = provider.auth.name;
67
+ }
68
+ else if (provider.auth.value !== "") {
69
+ // A key we hold: never touch auth.json, never go through the auth manager.
70
+ const name = syntheticKeyName(provider.id);
71
+ table["env_key"] = name;
72
+ env[name] = provider.auth.value;
73
+ }
74
+ else {
75
+ // The seeded shape — the runtime's own auth.json already holds this key.
76
+ table["requires_openai_auth"] = true;
77
+ }
78
+ // Top-level defaults for this process, so a thread that does not restate the
79
+ // provider still lands on the right one. `model_providers` is nested rather
80
+ // than pre-dotted: the SDK owns the flattening, and a nested table is what
81
+ // its `CodexConfigObject` type describes.
82
+ const config = {
83
+ model_providers: { [provider.id]: table },
84
+ model_provider: provider.id,
85
+ };
86
+ const catalog = writeProviderCatalog(worldHome, codexHome, provider);
87
+ // Absolute on purpose: codex resolves this override against the process cwd
88
+ // (`writeProviderCatalog` returns it under the absolute world home).
89
+ if (catalog !== undefined)
90
+ config["model_catalog_json"] = catalog;
91
+ return { config, env, providerId: provider.id };
92
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Codex home resolution + the rollout-HEAD metadata reader.
3
+ *
4
+ * The listing/replay scanner that used to live here is GONE (2026-09-16): the
5
+ * DSH session log is now the session authority (upstream `sessionPersistence`
6
+ * owns list/read/replay), so the adapter no longer derives session ids or
7
+ * transcripts from Codex's rollout store. What remains is:
8
+ *
9
+ * - home resolution (native `~/.codex`, never redirected; `CODEX_HOME` is the
10
+ * test/operator override), and
11
+ * - {@link readRolloutHead}: a bounded, memoized reader of ONE rollout's
12
+ * FIRST line (`session_meta`), used only to recover the Codex system
13
+ * prompt (`payload.base_instructions`) and the recorded cwd. It never
14
+ * parses the transcript and never throws.
15
+ *
16
+ * Single-direction valve (spec S7): this module NEVER writes under the codex
17
+ * home; the rollout stays Codex's transcript of record.
18
+ */
19
+ import { closeSync, openSync, readdirSync, readSync, statSync } from "node:fs";
20
+ import { homedir } from "node:os";
21
+ import { join, resolve } from "node:path";
22
+ /**
23
+ * Resolve the Codex CLI's app-data home — the NATIVE `~/.codex` (user ruling
24
+ * 2026-09-17, pi precedent): the installed app keeps its own home, so the
25
+ * adapter never redirects it when spawning the SDK/CLI and never seeds config
26
+ * into it. Precedence: `CODEX_HOME` (tests / operator override), then the
27
+ * native default.
28
+ *
29
+ * The world's DSH home (`<dshHome>/agents/codex-sdk`) is NOT an input here anymore
30
+ * — it anchors adapter-owned DSH state only (session-map.ts). A home without
31
+ * `config.toml` would make the CLI fall back to OpenAI's own endpoint
32
+ * (region-blocked here), but that is the operator's native home: provisioning
33
+ * it is the operator's business (test helper: scripts/setup-codex-home.mjs).
34
+ */
35
+ export function resolveCodexHome() {
36
+ if (process.env.CODEX_HOME !== undefined) {
37
+ const env = resolve(process.env.CODEX_HOME);
38
+ return env;
39
+ }
40
+ const native = join(homedir(), ".codex");
41
+ return native;
42
+ }
43
+ /**
44
+ * Take an explicit codex home as-is (prod-guarded) — for callers that already
45
+ * hold the final path (models.ts, the spike); distinct from
46
+ * {@link resolveCodexHome}, which resolves the native default.
47
+ */
48
+ export function useCodexHome(path) {
49
+ const resolved = resolve(path);
50
+ return resolved;
51
+ }
52
+ /** Bounded read budget: `session_meta` is the FIRST line, so this is generous. */
53
+ const HEAD_BYTES = 64 * 1024;
54
+ /** Memoized heads by thread id — the head line is immutable once written. */
55
+ const headCache = new Map();
56
+ /**
57
+ * Locate one thread's rollout file under `<codexHome>/sessions/<Y>/<M>/<D>/`.
58
+ * The rollout file name embeds the thread id, so presence is the match test;
59
+ * a miss is retried (a thread's rollout may not exist yet at first call).
60
+ */
61
+ function findRolloutFile(codexHome, threadId) {
62
+ const root = join(codexHome, "sessions");
63
+ try {
64
+ for (const year of readdirSync(root, { withFileTypes: true })) {
65
+ if (!year.isDirectory())
66
+ continue;
67
+ for (const month of readdirSync(join(root, year.name), { withFileTypes: true })) {
68
+ if (!month.isDirectory())
69
+ continue;
70
+ for (const day of readdirSync(join(root, year.name, month.name), { withFileTypes: true })) {
71
+ if (!day.isDirectory())
72
+ continue;
73
+ const dir = join(root, year.name, month.name, day.name);
74
+ for (const file of readdirSync(dir)) {
75
+ if (file.endsWith(".jsonl") && file.includes(threadId))
76
+ return join(dir, file);
77
+ }
78
+ }
79
+ }
80
+ }
81
+ }
82
+ catch {
83
+ return undefined;
84
+ }
85
+ return undefined;
86
+ }
87
+ /** Read (at most) the first line of a file; fail-soft. */
88
+ function readFirstLine(path) {
89
+ let fd;
90
+ try {
91
+ fd = openSync(path, "r");
92
+ }
93
+ catch {
94
+ return undefined;
95
+ }
96
+ try {
97
+ const chunk = Buffer.allocUnsafe(HEAD_BYTES);
98
+ const read = readSync(fd, chunk, 0, chunk.length, 0);
99
+ if (read <= 0)
100
+ return undefined;
101
+ const text = chunk.subarray(0, read).toString("utf8");
102
+ const end = text.indexOf("\n");
103
+ return end === -1 ? text : text.slice(0, end);
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ finally {
109
+ closeSync(fd);
110
+ }
111
+ }
112
+ /**
113
+ * Read the `session_meta` head of the rollout belonging to `threadId`:
114
+ * the Codex system prompt and the recorded cwd. Memoized per thread id (the
115
+ * head line never changes); fail-soft — an unknown thread, an unwritten
116
+ * rollout, or a malformed head all yield `undefined`.
117
+ */
118
+ /**
119
+ * Codex writes `payload.base_instructions` either as a plain string or as an
120
+ * object `{ text }` (observed 2026-09-16 on the SDK line: the object form is
121
+ * what the vendored binary emits today). Accept both; anything else is absent.
122
+ */
123
+ function baseInstructionsText(value) {
124
+ if (typeof value === "string")
125
+ return value.trim() === "" ? undefined : value;
126
+ if (value !== null && typeof value === "object") {
127
+ const text = value["text"];
128
+ if (typeof text === "string" && text.trim() !== "")
129
+ return text;
130
+ }
131
+ return undefined;
132
+ }
133
+ export function readRolloutHead(codexHome, threadId) {
134
+ const cached = headCache.get(threadId);
135
+ if (cached !== undefined)
136
+ return cached;
137
+ const file = findRolloutFile(codexHome, threadId);
138
+ if (file === undefined)
139
+ return undefined;
140
+ // Cheap existence/size gate before paying for a read.
141
+ try {
142
+ if (!statSync(file).isFile())
143
+ return undefined;
144
+ }
145
+ catch {
146
+ return undefined;
147
+ }
148
+ const line = readFirstLine(file);
149
+ if (line === undefined)
150
+ return undefined;
151
+ let record;
152
+ try {
153
+ record = JSON.parse(line);
154
+ }
155
+ catch {
156
+ return undefined;
157
+ }
158
+ if (record === null || typeof record !== "object")
159
+ return undefined;
160
+ const rec = record;
161
+ if (rec["type"] !== "session_meta")
162
+ return undefined;
163
+ const payload = rec["payload"];
164
+ if (payload === null || typeof payload !== "object")
165
+ return undefined;
166
+ const meta = payload;
167
+ const head = {
168
+ ...(typeof meta["id"] === "string" && meta["id"] !== "" ? { threadId: meta["id"] } : {}),
169
+ ...(typeof meta["cwd"] === "string" && meta["cwd"] !== "" ? { cwd: meta["cwd"] } : {}),
170
+ ...(baseInstructionsText(meta["base_instructions"]) === undefined
171
+ ? {}
172
+ : { baseInstructions: baseInstructionsText(meta["base_instructions"]) }),
173
+ };
174
+ headCache.set(threadId, head);
175
+ return head;
176
+ }
177
+ /** Test hygiene: drop memoized heads. */
178
+ export function clearRolloutHeadCache() {
179
+ headCache.clear();
180
+ }