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
@@ -3,53 +3,89 @@
3
3
  *
4
4
  * The browser model selector never touches the Codex agent directly: it is an
5
5
  * RPC round-trip through the apiproxy, which reads `ctx.llm.listProviders /
6
- * listModels / resolveModel` to build the catalog. This adapter answers those
7
- * queries from the Codex home's real catalog (`models.ts`: config.toml +
8
- * model_catalog_json) under the single provider route `codex` — never the
9
- * native deepseek-official / pi-ai routes, which the profile disables.
6
+ * listModels / resolveModelInfo` to build the catalog. This adapter answers
7
+ * those queries from the ADAPTER's canonical provider store
8
+ * (`codex-providers.ts`), registering ONE ROUTE PER CONFIGURED PROVIDER.
10
9
  *
11
- * `stream` is never dispatched: Codex owns generation through its SDK thread
12
- * (`CodexAgent`), so the adapter only serves catalog/metadata queries. It
13
- * throws rather than fabricating a wire route the harness would misuse.
10
+ * That one-route-per-provider shape is the 2026-10-04 rework, and it is what
11
+ * makes the whole feature DSH-native: the harness's own model selector already
12
+ * renders a provider-grouped catalog, already keeps the choice per session
13
+ * (`session/selectModel` → the durable `model/selection` event + the
14
+ * `modelSelection` projection), and already reports which route a session last
15
+ * used. Registering the Codex providers as real routes means Super D gets all
16
+ * of that by construction instead of re-implementing it — there is no "active
17
+ * provider" left for the UI to hold.
18
+ *
19
+ * `stream` is never dispatched: Codex owns generation through its app-server
20
+ * thread (`CodexAgent`), so the adapter only serves catalog/metadata queries.
21
+ * It throws rather than fabricating a wire route the harness would misuse.
22
+ *
23
+ * @module agent-codex/adapter
14
24
  */
15
25
  import { LlmAdapter, LlmError, } from "@deepseek-ai/dsh-llm";
16
- import { readCodexModelCatalog } from "./models.js";
17
- /** The one provider route every Codex model is served under. */
26
+ import { readProviderStore } from "./codex-providers.js";
27
+ /**
28
+ * The single route every Codex model was served under before the multi-provider
29
+ * rework. No longer registered (a session pinned to it falls back to whatever
30
+ * the native `config.toml` runs), but kept as the one named constant so the
31
+ * legacy value is greppable in session logs.
32
+ */
18
33
  export const CODEX_PROVIDER_ID = "codex";
34
+ /** Whether a provider may be registered as a model route. */
35
+ export function routableProvider(entry) {
36
+ return entry.models.length > 0;
37
+ }
38
+ /** The route ids a store currently contributes to `ctx.llm`. */
39
+ export function routeIds(store) {
40
+ return store.providers.filter(routableProvider).map((entry) => entry.id);
41
+ }
42
+ /** The provider entry a route id names, or `undefined`. */
43
+ export function providerFor(worldHome, providerId) {
44
+ return readProviderStore(worldHome).providers.find((entry) => entry.id === providerId);
45
+ }
19
46
  export class CodexLlmAdapter extends LlmAdapter {
20
- home;
47
+ worldHome;
21
48
  /**
22
- * `home` = the codex app home — the CLI's native `~/.codex` (2026-09-17
23
- * home ruling), resolved by the provider via resolveCodexHome() so catalog
24
- * reads never depend on construction-time context.
49
+ * @param worldHome - the world's DSH home, holding the canonical provider
50
+ * store in `<worldHome>/codex-providers.json`. Read on every call, so a
51
+ * panel edit is visible to the next catalog query without re-registration.
25
52
  */
26
- constructor(home) {
53
+ constructor(worldHome) {
27
54
  super();
28
- this.home = home;
55
+ this.worldHome = worldHome;
29
56
  }
30
- providerInfo(_provider) {
31
- return { id: CODEX_PROVIDER_ID, name: readCodexModelCatalog(this.home).provider ?? "Codex" };
57
+ providerInfo(provider) {
58
+ // The display name is the entry's label — for a provider folded in from the
59
+ // user's own config that is its endpoint URL, which is the only name a
60
+ // hand-written codex config supplies.
61
+ return { id: provider, name: providerFor(this.worldHome, provider)?.label ?? provider };
32
62
  }
33
- async listModels(_provider) {
34
- return readCodexModelCatalog(this.home).models.map((model) => ({
35
- provider: CODEX_PROVIDER_ID,
63
+ async listModels(provider) {
64
+ const entry = providerFor(this.worldHome, provider);
65
+ if (entry === undefined)
66
+ return [];
67
+ return entry.models.map((model) => ({
68
+ provider,
36
69
  id: model.id,
37
- name: model.label,
70
+ name: model.displayName ?? model.id,
38
71
  }));
39
72
  }
40
- async resolveModel(_provider, model) {
41
- const found = readCodexModelCatalog(this.home).models.find((candidate) => candidate.id === model);
73
+ async resolveModel(provider, model, _signal) {
74
+ const entry = providerFor(this.worldHome, provider);
75
+ const found = entry?.models.find((candidate) => candidate.id === model);
42
76
  if (found === undefined) {
43
- throw new LlmError(`Codex provider "${CODEX_PROVIDER_ID}" does not serve model "${model}"`, "MODEL_NOT_FOUND");
77
+ throw new LlmError(`Codex provider "${provider}" does not serve model "${model}"`, "MODEL_NOT_FOUND");
44
78
  }
45
79
  return {
46
- provider: CODEX_PROVIDER_ID,
80
+ provider,
47
81
  id: found.id,
48
- name: found.label,
49
- ...(found.contextWindow !== undefined && found.contextWindow > 0 ? { context: { contextWindow: found.contextWindow } } : {}),
82
+ name: found.displayName ?? found.id,
83
+ ...(found.contextWindow !== undefined && found.contextWindow > 0
84
+ ? { context: { contextWindow: found.contextWindow } }
85
+ : {}),
50
86
  };
51
87
  }
52
88
  stream(_options) {
53
- throw new LlmError("the Codex adapter does not stream: Codex owns generation through its SDK agent", "UNSUPPORTED_STREAM");
89
+ throw new LlmError("the Codex adapter does not stream: Codex owns generation through its app-server thread", "UNSUPPORTED_STREAM");
54
90
  }
55
91
  }
@@ -1,8 +1,13 @@
1
1
  import { agentEvents } from "@deepseek-ai/dsh-agent";
2
2
  import { Inbox } from "./inbox.js";
3
+ import { canonicalHeader, headerEquals } from "@deepseek-ai/dsh-session";
3
4
  import { LlmAttemptId, ToolCallId, QUOTA_EXCEEDED_CODE, createAssistantMessage, createSystemMessage, createToolResultMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
4
5
  import { createScope } from "@deepseek-ai/dsh-scope";
6
+ import { makeApprovalHandler, parseApprovalTimeout } from "./app-server-approvals.js";
7
+ import { presetFromEvents, turnPolicyFromEvents } from "./permission.js";
5
8
  import { withSystemHead } from "../../superd-hub/dist/index.js";
9
+ import { NATIVE_LAUNCH } from "./codex-launch.js";
10
+ import { resolveRoute } from "./codex-route.js";
6
11
  /**
7
12
  * v2 live assistant-stream publication for the Codex bridge — the same frame
8
13
  * protocol the reference loop's AssistantStreamAttempt speaks (start marker,
@@ -193,11 +198,15 @@ function userMessageText(message) {
193
198
  /**
194
199
  * Dash-host plugin narration from the approval service: the policy-change
195
200
  * notice `agent.inject` carries when `/permission` switches a live session's
196
- * approval policy. Codex has no runtime approval control either (the policy
197
- * is pinned at launch via the preset mapping; no SDK surface changes it
198
- * mid-session), so forwarding the text would steer it into an in-flight turn
199
- * (interrupting tool work) and echo it into history as a fake user prompt.
200
- * Detect and drop it.
201
+ * approval policy.
202
+ *
203
+ * App-server line (2026-10-03): the switch itself is now REAL — the fold in
204
+ * `#syncTurnPolicy` reads the accompanying `approval/policy` + `sandbox/mode`
205
+ * events and overrides both fields on the next `turn/start`. The narration TEXT
206
+ * is still dropped on purpose: it is DSH's own sandbox-policy sentence, written
207
+ * for the native tool loop, and Codex enforces its own policy inside the rust
208
+ * runtime. Forwarding it would steer it into the in-flight turn (interrupting
209
+ * tool work) and echo into history as a fake user prompt.
201
210
  */
202
211
  function isApprovalNarration(message) {
203
212
  // 0.1.7: the shared `plugin` source kind is gone (MessageSourceMap is
@@ -235,6 +244,12 @@ function parseIdleExit(raw) {
235
244
  const value = Number(raw);
236
245
  return Number.isFinite(value) && value >= 0 ? value : 600_000;
237
246
  }
247
+ /**
248
+ * Approval answer timeout: fail closed (decline) after this long so a headless
249
+ * or unattended run can never hang Codex on a card nobody answers. `0`
250
+ * disables the timeout. Default: 5 minutes.
251
+ */
252
+ const CODEX_APPROVAL_TIMEOUT_MS = parseApprovalTimeout(process.env.CODEX_APPROVAL_TIMEOUT_MS);
238
253
  export class CodexAgent {
239
254
  id;
240
255
  options;
@@ -281,8 +296,26 @@ export class CodexAgent {
281
296
  * failure/abort paths so the turn's visible text is never dropped.
282
297
  */
283
298
  #deferredAssistant = null;
284
- /** The last selection already synced to the client via `set_model`, as `provider/model`. */
285
- #lastSyncedModel = null;
299
+ /**
300
+ * The route the OPEN turn runs, captured when the turn opened.
301
+ *
302
+ * Codex's wire messages carry no provider attribution, so the assistant
303
+ * message's `source` has to be stamped from here. `options` is the wrong
304
+ * source for it: options are frozen at create/resume, so after a mid-session
305
+ * switch they name the provider the session STARTED on. Measured on the 4999
306
+ * line: turn 2 ran on `deepseek/deepseek-flash` while its assistant message was
307
+ * attributed to `zhipu-cn/glm-5.3-flash` — a user-visible lie, since the UI and
308
+ * the model-switch notice both read that attribution.
309
+ */
310
+ #turnRoute = undefined;
311
+ /**
312
+ * The route the LIVE child was last told to run, as `provider/model`. Purely
313
+ * a work-saver: the client is idempotent about `setModel` and `reattach`, and
314
+ * this only keeps a steady session from re-issuing either every turn.
315
+ */
316
+ #routeKey = null;
317
+ /** Whether THIS agent instance has already logged a `request/header` snapshot. */
318
+ #requestHeaderLogged = false;
286
319
  /** Fires when the agent has been idle past CODEX_IDLE_EXIT_MS (tears down the client). */
287
320
  #onIdleExit;
288
321
  #idleExitTimer = undefined;
@@ -310,37 +343,43 @@ export class CodexAgent {
310
343
  this.#lastTurn = session.snapshotEvents().findLast((event) => event.type === "turn/start")?.data.turn ?? 0;
311
344
  client.on((event) => this.#handleEvent(event));
312
345
  client.onFailure((error) => this.#fail(error));
346
+ // App-server line (2026-10-03): Codex now ASKS before running a command or
347
+ // applying a patch (`item/commandExecution|fileChange/requestApproval` plus
348
+ // the legacy execCommand/applyPatch pair) and blocks the tool until the
349
+ // client answers. Bridge those requests onto the Dash approval seam, whose
350
+ // Web UI answerer renders the ApprovalPanel. Every failure path answers
351
+ // DECLINE (fail-closed) so a request is never left hanging; the service is
352
+ // resolved lazily because the approval row lives in a sibling fiber.
353
+ client.setServerRequestHandler(makeApprovalHandler({
354
+ getApproval: () => this.ctx.get("approval"),
355
+ agent: this,
356
+ timeoutMs: CODEX_APPROVAL_TIMEOUT_MS,
357
+ asCallId: (raw) => ToolCallId(raw),
358
+ trace: (message) => trace(message),
359
+ }));
313
360
  // Session-identity events (the agent-preset stamp) are committed on the
314
361
  // FIRST turn by #bootstrapSessionIdentity — only once the client actually
315
362
  // exists — so a freshly created session announces completely blank,
316
363
  // exactly like the native agent-loop factory's: the backend takes zero
317
364
  // part in the UI's browser-local new-session draft.
318
- // `/permission` cannot work here: Codex pins the approval policy at
319
- // launch and no SDK surface changes it mid-session. Registering the SAME
320
- // name on this agent's own scope (a command-injected child of `agent.ctx`)
321
- // SHADOWS the global permission command for this agent only — the switch
322
- // then fails cleanly instead of appending approval/policy events Codex
323
- // cannot honor. (The `inject` narration drop below stays as the backstop.)
324
- this.ctx.inject(["commands"], (cmdCtx) => cmdCtx.commands.register({
325
- name: "permission",
326
- description: "Unavailable for Codex sessions (approval policy is fixed at Codex launch)",
327
- input: { hint: "<preset>" },
328
- handler: () => ({
329
- kind: "error",
330
- text: "Approval policy is fixed at Codex launch; runtime switching is not supported for Codex sessions.",
331
- }),
332
- }));
333
- // `/compact` parity: the SDK line exposes no compaction surface (V1 gap
334
- // table: no messages surgery/compact — fail-soft default). Shadowing the
335
- // loop-owned global name keeps the (disabled) native pipeline from
336
- // receiving it; the command node renders the clean refusal natively.
365
+ //
366
+ // `/permission` is NOT shadowed on the app-server line: the app-server
367
+ // accepts `approvalPolicy` + `sandboxPolicy` on every `turn/start`, so the
368
+ // global command's `permission/preset` + `sandbox/mode` + `approval/policy`
369
+ // triple is folded per turn by #syncTurnPolicy and takes effect from the
370
+ // NEXT turn on. (The SDK line had to shadow it with a refusal.)
371
+ // `/compact` parity: the app-server DOES expose `thread/compact/start` +
372
+ // `thread/compacted`, but the DSH compaction event vocabulary is not wired
373
+ // to it yet (registered backlog item). Shadowing the loop-owned global name
374
+ // keeps the (disabled) native pipeline from receiving it; the command node
375
+ // renders the clean refusal natively.
337
376
  this.ctx.inject(["commands"], (cmdCtx) => cmdCtx.commands.register({
338
377
  name: "compact",
339
- description: "Unavailable for Codex sessions (the SDK line has no compaction surface)",
378
+ description: "Unavailable for Codex sessions (thread compaction is not wired yet)",
340
379
  input: { hint: "[instructions]" },
341
380
  handler: () => ({
342
381
  kind: "error",
343
- text: "Compaction is not available for Codex sessions: the SDK line has no compaction surface.",
382
+ text: "Compaction is not available for Codex sessions yet: the app-server's thread/compact/start is not wired to the Dash compaction events.",
344
383
  }),
345
384
  }));
346
385
  }
@@ -360,18 +399,16 @@ export class CodexAgent {
360
399
  steer(message) {
361
400
  // Mirrors dsh-agent-loop's `steer = send(input, "next-step", true)`: an
362
401
  // idle driver starts a turn; a running driver consumes it at the next
363
- // step (on the SDK line: queued followUp after the in-flight turn).
402
+ // step (app-server line: `turn/steer` into the live turn).
364
403
  this.#deliver(message, "next-step", true);
365
404
  }
366
405
  inject(message) {
367
- // Dash-host approval-policy narrations have no Codex runtime equivalent
368
- // (the policy is pinned at launch; no SDK surface changes it mid-session).
369
- // Forwarding the text would steer it into an in-flight turn — interrupting
370
- // tool work — and echo it into history as a fake user prompt. Drop it; the
371
- // accompanying `approval/policy` session event stays recorded so the UI's
372
- // permission state remains truthful.
406
+ // Drop the approval-policy narration text (see isApprovalNarration). The
407
+ // real switch rides the accompanying `approval/policy` + `sandbox/mode`
408
+ // session events, which #syncTurnPolicy folds into the next turn/start;
409
+ // recording them keeps the UI's permission state truthful.
373
410
  if (isApprovalNarration(message)) {
374
- trace("inject: dropped approval-policy narration (Codex lacks runtime approval control)");
411
+ trace("inject: dropped approval-policy narration (the switch rides the session events, not the text)");
375
412
  return;
376
413
  }
377
414
  // Mirrors dsh-agent-loop's `inject = send(input, "next-step", false)`:
@@ -430,7 +467,7 @@ export class CodexAgent {
430
467
  *
431
468
  * Busy is the LOCAL turn state (`#turnOpen`), not the `#streaming` flag:
432
469
  * the client needs a moment to emit `agent_start` after a prompt, and in
433
- * that gap a bare `prompt` is still safe on the SDK line, but keeping the
470
+ * that gap a bare `prompt` is still safe on the app-server line, but keeping the
434
471
  * omp queuing semantics makes the echo mapping identical.
435
472
  */
436
473
  #deliver(message, target, wakeup) {
@@ -471,10 +508,15 @@ export class CodexAgent {
471
508
  * reservation.
472
509
  */
473
510
  async #startTurn(message) {
511
+ let route;
474
512
  try {
475
- await this.#client.ensureStarted();
476
- await this.#bootstrapSessionIdentity();
477
- await this.#syncModelSelection();
513
+ // The route is resolved and put in force BEFORE the thread is touched: a
514
+ // provider switch is a different child process, and `thread/resume` cannot
515
+ // move a thread a live app-server already holds (`#applyRoute`).
516
+ route = this.#resolveRoute();
517
+ await this.#applyRoute(route);
518
+ this.#turnRoute = route;
519
+ this.#syncTurnPolicy();
478
520
  }
479
521
  catch (error) {
480
522
  trace(`#startTurn cold start failed: ${String(error)}`);
@@ -489,7 +531,9 @@ export class CodexAgent {
489
531
  if (this.#disposed)
490
532
  return;
491
533
  this.session.append("turn/start", { turn: this.#dashTurn });
492
- this.#emitRequestContext();
534
+ if (route !== undefined)
535
+ this.#commitRequestHeader(route);
536
+ this.#emitRequestContext(route);
493
537
  this.session.append("user/message", message, { surfaceOp: "append" });
494
538
  void this.#client.prompt(userMessageText(message)).catch((error) => this.#fail(error));
495
539
  }
@@ -529,28 +573,34 @@ export class CodexAgent {
529
573
  * Turn-enclosed (no step required) and deduped by route key, so it lands at
530
574
  * the start of a turn and again only when the effective route changes.
531
575
  */
532
- #emitRequestContext() {
533
- if (this.#runtimeInfo === undefined || !this.#turnOpen)
534
- return;
535
- const route = this.#runtimeInfo.routeContext(this.options.model);
536
- if (route === undefined)
576
+ /**
577
+ * Stamp route metadata for the next request (`request/context`): the effective
578
+ * provider/model plus the provider's own context window when it advertises one.
579
+ * Turn-enclosed (no step required) and deduped by route key, so it lands at
580
+ * the start of a turn and again only when the effective route changes.
581
+ *
582
+ * @param route - the route this turn runs, already resolved by `#resolveRoute`.
583
+ */
584
+ #emitRequestContext(route) {
585
+ if (route === undefined || !this.#turnOpen)
537
586
  return;
538
587
  const key = `${route.provider}/${route.model}`;
539
588
  if (key === this.#lastRouteKey)
540
589
  return;
541
590
  this.#lastRouteKey = key;
591
+ const contextWindow = this.#runtimeInfo?.contextWindowFor?.(route.provider, route.model);
542
592
  this.session.append("request/context", {
543
593
  provider: route.provider,
544
594
  model: route.model,
545
- ...(route.contextWindow === undefined ? {} : { contextWindow: route.contextWindow }),
595
+ ...(contextWindow === undefined ? {} : { contextWindow }),
546
596
  });
547
- trace(`request/context ${key}${route.contextWindow === undefined ? "" : ` ctx=${route.contextWindow}`}`);
597
+ trace(`request/context ${key}${contextWindow === undefined ? "" : ` ctx=${contextWindow}`}`);
548
598
  }
549
599
  /**
550
600
  * One-shot first-turn identity commit: the agent-preset stamp (idempotent).
551
- * The SDK line exposes no system-prompt surface, so — unlike the omp
552
- * bridge — no `system/message` node is emitted here; a Codex session's
553
- * system prompt is Codex's own and never crosses the wire. The guard —
601
+ * The system prompt is not sent BY us: Codex owns its base instructions and
602
+ * the adapter recovers them from the rollout HEAD (see #emitSystemMessage).
603
+ * The guard —
554
604
  * skip when the seed already carries a `system/message` — stays for shape
555
605
  * parity with resumed logs that might carry one.
556
606
  */
@@ -580,8 +630,26 @@ export class CodexAgent {
580
630
  if (this.#streaming || transport === "followUp")
581
631
  this.#flushRemote();
582
632
  }
633
+ /**
634
+ * Retarget the model the NEXT turn requests, without disturbing a live one.
635
+ *
636
+ * A delivery can arrive while a turn is running (steering, or a queued
637
+ * follow-up the client will auto-generate), and the app-server fixes a turn's
638
+ * route at `turn/start`. So the model — which the app-server does accept per
639
+ * turn — is re-pinned here, while a PROVIDER move is deliberately left to the
640
+ * next turn's `#startTurn`, which is the only place a child can be replaced.
641
+ */
642
+ async #pinNextModel() {
643
+ const route = this.#resolveRoute();
644
+ if (route === undefined)
645
+ return;
646
+ await this.#client.setModel(route.provider, route.model).catch((error) => {
647
+ trace(`set_model ${route.provider}/${route.model} failed: ${String(error)}`);
648
+ });
649
+ }
583
650
  #flushRemote() {
584
- void this.#syncModelSelection().finally(() => {
651
+ this.#syncTurnPolicy();
652
+ void this.#pinNextModel().finally(() => {
585
653
  for (const entry of this.#remoteQueue) {
586
654
  if (entry.sent)
587
655
  continue;
@@ -594,33 +662,130 @@ export class CodexAgent {
594
662
  });
595
663
  }
596
664
  /**
597
- * Push the effective model selection into the client before a turn is
598
- * dispatched. The SDK has no live setModel, so the client stores the id
599
- * and applies it as the next run's ThreadOption (via resumeThread, so the
600
- * conversation context survives). The omp waterfall pair
601
- * (system-prompt assemble → agent/request) is a V1 cut: the base is the
602
- * session's logged `request/header` falling back to the global
603
- * `agentDefaultModel` selection, else the agent options — the apiproxy's
604
- * installed selection IS the global one this process serves.
665
+ * The route this session must run, resolved the way the harness itself
666
+ * resolves it.
667
+ *
668
+ * `sessionController.selectionFor(agent).current` is `pending ?? logged ??
669
+ * default`, and `pending` is the durable `modelSelection` projection's view of
670
+ * the latest `model/selection` event that no later `request/header` has
671
+ * consumed. Reading the PROJECTION (rather than re-scanning the log) is what
672
+ * makes a mid-turn switch work for free: the harness's own `session.selectModel`
673
+ * appends that event and retires it once this agent logs the header that used
674
+ * it, so "switch now, run next turn" is the projection's semantics, not ours.
675
+ *
676
+ * @returns the route, or `undefined` when nothing in the log or the services
677
+ * names a complete provider/model pair.
678
+ * @returns the route, or `undefined` when nothing in the log or the services
679
+ * names a complete provider/model pair (see `codex-route.ts` for the order).
605
680
  */
606
- async #syncModelSelection() {
607
- const persisted = this.session.requestHeader()?.config;
681
+ #resolveRoute() {
682
+ const projections = this.#loopCtx.get("sessionProjections");
608
683
  const service = this.ctx.get("agentDefaultModel");
609
- const selection = service?.currentSelection();
610
- const target = persisted !== undefined && persisted.provider !== "" && persisted.model !== ""
611
- ? { provider: persisted.provider, model: persisted.model }
612
- : selection !== undefined && selection.provider !== "" && selection.model !== ""
613
- ? selection
614
- : { provider: this.options.provider ?? "", model: this.options.model ?? "" };
615
- if (target.provider === "" || target.model === "")
616
- return;
617
- const key = `${target.provider}/${target.model}`;
618
- if (key === this.#lastSyncedModel)
619
- return;
620
- await this.#client.setModel(target.provider, target.model).catch((error) => {
621
- trace(`set_model ${key} failed: ${String(error)}`);
684
+ return resolveRoute({
685
+ pending: projections?.stateOf?.(this.session, "modelSelection")?.pending,
686
+ logged: this.session.requestHeader()?.config,
687
+ preferred: service?.currentSelection(),
688
+ fallback: { provider: this.options.provider, model: this.options.model },
689
+ });
690
+ }
691
+ /**
692
+ * Put the resolved route in force on the native side, switching the CHILD if
693
+ * the provider moved.
694
+ *
695
+ * Three cases, in increasing cost:
696
+ *
697
+ * 1. nothing attached yet — pin the launch and let the first prompt spawn
698
+ * (this is the lazy-spawn gate: choosing a model must not cost a process);
699
+ * 2. same provider, different model — `setModel` rides the next `turn/start`,
700
+ * which the app-server documents as "this turn and subsequent turns";
701
+ * 3. different provider — the child has to be replaced, because a provider is
702
+ * an argv-level property of the process and `thread/resume` cannot move a
703
+ * thread a live app-server already holds (see `app-server-client.ts`
704
+ * `reattach`). This is THE turn boundary the feature is built around.
705
+ *
706
+ * @param route - the resolved route, or `undefined` when none is known (the
707
+ * native config then decides, which is also a legacy `codex` selection's fate).
708
+ */
709
+ async #applyRoute(route) {
710
+ const launch = route === undefined ? undefined : this.#runtimeInfo?.launchFor(route.provider);
711
+ // An unknown provider id (a session pinned before the multi-provider rework)
712
+ // maps to "no injection": the user's own config.toml is the honest answer.
713
+ const launchSpec = launch ?? NATIVE_LAUNCH;
714
+ const providerId = launch === undefined ? null : route?.provider ?? null;
715
+ const client = this.#client;
716
+ const key = `${providerId ?? "native"}/${route?.model ?? ""}`;
717
+ if (!client.spawned && client.threadId === null) {
718
+ client.applyLaunch(launchSpec, providerId);
719
+ await client.ensureStarted();
720
+ }
721
+ else if (client.threadId === null) {
722
+ // A live child with no thread yet: still just a launch swap.
723
+ client.applyLaunch(launchSpec, providerId);
724
+ await client.ensureStarted();
725
+ }
726
+ else if (client.appliedProvider !== providerId) {
727
+ await client.reattach(launchSpec, providerId, route?.model);
728
+ }
729
+ else if (route !== undefined) {
730
+ await client.setModel(route.provider, route.model).catch((error) => {
731
+ trace(`set_model ${key} failed: ${String(error)}`);
732
+ });
733
+ }
734
+ this.#routeKey = key;
735
+ }
736
+ /**
737
+ * Log the request header the NEXT request runs under, exactly as the reference
738
+ * loop does.
739
+ *
740
+ * This single append is what wires Super D into the harness's model-selection
741
+ * state machine rather than beside it: the `modelSelection` projection takes
742
+ * `lastUsed` from it, `sessionController.consumeSelection` retires a pending
743
+ * switch when it matches, and `installModelSelection` compares it to decide
744
+ * whether a model-change notice is owed. Without it a resumed session can
745
+ * never say which provider produced its transcript.
746
+ *
747
+ * @param route - the route this turn runs.
748
+ */
749
+ #commitRequestHeader(route) {
750
+ const header = canonicalHeader({
751
+ config: {
752
+ provider: route.provider,
753
+ model: route.model,
754
+ ...(route.reasoningEffort === undefined
755
+ ? {}
756
+ : { reasoningEffort: route.reasoningEffort }),
757
+ },
622
758
  });
623
- this.#lastSyncedModel = key;
759
+ const baseline = this.session.requestHeader();
760
+ if (!this.#requestHeaderLogged) {
761
+ this.session.append("request/header", { header, reason: baseline === undefined ? "initial" : "resume" });
762
+ this.#requestHeaderLogged = true;
763
+ return;
764
+ }
765
+ if (baseline === undefined || !headerEquals(baseline, header)) {
766
+ this.session.append("request/header", { header, reason: "change" });
767
+ }
768
+ }
769
+ /**
770
+ * Fold the session's LIVE permission state into the codex per-turn overrides
771
+ * and hand them to the client. The app-server accepts `approvalPolicy` and
772
+ * `sandboxPolicy` on every `turn/start` ("for this turn and subsequent
773
+ * turns"), so a `/permission` switch is honored from the NEXT turn on — the
774
+ * capability whose absence forced the SDK line to shadow that command with a
775
+ * refusal. Never throws: any fold failure leaves the previous override in
776
+ * place (the thread's launch posture), which is the safe direction.
777
+ */
778
+ #syncTurnPolicy() {
779
+ try {
780
+ const events = this.session.snapshotEvents();
781
+ const cwd = this.#runtimeInfo?.cwd?.() ?? "";
782
+ const policy = turnPolicyFromEvents(events, presetFromEvents(events), cwd);
783
+ this.#client.setTurnPolicy(policy);
784
+ trace(`turn policy: approval=${policy.approvalPolicy} sandbox=${policy.sandbox.type}`);
785
+ }
786
+ catch (error) {
787
+ trace(`turn policy fold failed: ${String(error)}`);
788
+ }
624
789
  }
625
790
  /**
626
791
  * Reserve a turn WITHOUT appending anything: the prompt path reserves
@@ -643,7 +808,15 @@ export class CodexAgent {
643
808
  this.#reserveTurn();
644
809
  this.#localUserPending = localUser;
645
810
  this.session.append("turn/start", { turn: this.#dashTurn });
646
- this.#emitRequestContext();
811
+ // This path opens a turn for a message the CLIENT queued and is now
812
+ // generating, so the child is already running: resolve the route for the
813
+ // log's benefit only. It cannot have moved since the turn it belongs to
814
+ // started, because a provider switch needs a turn boundary.
815
+ const route = this.#resolveRoute();
816
+ this.#turnRoute = route;
817
+ if (route !== undefined)
818
+ this.#commitRequestHeader(route);
819
+ this.#emitRequestContext(route);
647
820
  this.session.append("user/message", message, { surfaceOp: "append" });
648
821
  }
649
822
  #closeTurn(reason) {
@@ -994,10 +1167,11 @@ export class CodexAgent {
994
1167
  const assistant = createAssistantMessage({
995
1168
  content,
996
1169
  source: {
997
- // Rollout/wire messages may carry no attribution: fall back to this
998
- // agent's options (the omp bridge did the same).
999
- provider: String(message.provider ?? this.options.provider ?? ""),
1000
- model: String(message.model ?? this.options.model ?? ""),
1170
+ // Codex's wire messages carry no attribution at all, so the fallback is
1171
+ // the route the OPEN turn runs — never `options`, which is frozen at
1172
+ // create/resume and would name the provider the session started on.
1173
+ provider: String(message.provider ?? this.#turnRoute?.provider ?? this.options.provider ?? ""),
1174
+ model: String(message.model ?? this.#turnRoute?.model ?? this.options.model ?? ""),
1001
1175
  },
1002
1176
  });
1003
1177
  const bridge = this.#streamBridge;