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
@@ -0,0 +1,739 @@
1
+ /**
2
+ * agent-codex provider — Codex provider plugin for DeepSeek Harness
3
+ * (Super D, SDK line).
4
+ *
5
+ * Replaces the built-in agent loop with an `AgentFactory` that drives
6
+ * `@openai/codex-sdk` threads (one `CodexSdkClient` per session) and bridges
7
+ * the projected wire stream into the Dash Agent/Session contracts. Mirrors
8
+ * `@deepseek-ai/dsh-agent-loop`'s creation transaction (prepare → owned
9
+ * storage handle → setup → publish) so the session + agent publish as one
10
+ * ordered lifecycle AND the DSH session log is written through the upstream
11
+ * `sessionPersistence` service — list/read/replay/workspace grouping are then
12
+ * ordinary DSH services over that log.
13
+ *
14
+ * Session identity (2026-09-16, "mapping only"): the DSH session id is the
15
+ * authority — supplied by the harness at create (`options.sessionId`) and
16
+ * resume (`options.resumeSessionId`). Codex's thread id only materializes
17
+ * after the first turn, so the pairing is persisted in the adapter's app home
18
+ * (`<codexHome>/dsh-sessions.json`, see session-map.ts). Resume resolves that
19
+ * record and fails closed when the thread is unknown. The rollout scan, the
20
+ * adapter-owned session list and the transcript replay are gone.
21
+ */
22
+ import { existsSync, realpathSync, statSync } from "node:fs";
23
+ import { join } from "node:path";
24
+ import { Service } from "@deepseek-ai/cordis";
25
+ import { installModelSelection } from "@deepseek-ai/dsh-agent";
26
+ import { SessionLogOffset, SessionPreparation, interruptedTurnClosers, } from "@deepseek-ai/dsh-session";
27
+ import { CodexSdkClient } from "./codex-sdk-client.js";
28
+ import { CodexAgent } from "./agent.js";
29
+ import { CodexLlmAdapter, routeIds } from "./adapter.js";
30
+ import { registerCodexPreset } from "./agent-preset-codex.js";
31
+ import { readRolloutHead, resolveCodexHome } from "./codex-store.js";
32
+ import { CodexResumeError, sessionRecord, upsertSession } from "./session-map.js";
33
+ import { readProviderStore } from "./codex-providers.js";
34
+ import { readNativeSelection } from "./codex-native-config.js";
35
+ import { CodexProvidersGateway, syncFromNative } from "./codex-provider-gateway.js";
36
+ import { seedRoute } from "./codex-route.js";
37
+ import { providerSdkLaunch } from "./codex-sdk-launch.js";
38
+ import { codexApprovalMode, defaultPermissionPreset, envApprovalMode, presetFromEvents } from "./permission.js";
39
+ /** Realpath of a recorded cwd, accepted only when it names an existing directory. */
40
+ function validatedCwd(cwd) {
41
+ if (cwd === undefined)
42
+ return undefined;
43
+ try {
44
+ const canonical = realpathSync(cwd);
45
+ return statSync(canonical).isDirectory() ? canonical : undefined;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ /** Diagnostic trace (CODEX_TRACE=1 on the dsh process enables stderr tracing). */
52
+ const trace = (...parts) => {
53
+ if (process.env.CODEX_TRACE === "1")
54
+ process.stderr.write(`[codex-provider ${Date.now() % 1_000_000}] ${parts.join(" ")}\n`);
55
+ };
56
+ /**
57
+ * How often the provider store is reconciled with the user's `config.toml`
58
+ * (2026-10-04 user ruling). cc-switch writes that file behind our back, so a
59
+ * one-shot read at boot would let the panel drift from the file the user edits;
60
+ * a minute-scale tick is cheap (one small file parse) and keeps the two honest.
61
+ */
62
+ const SYNC_INTERVAL_MS = 600_000;
63
+ /** Open a read handle, pull the complete stored log, close. */
64
+ async function readStoredEvents(persistence, id, signal) {
65
+ const handle = await persistence.open(id, "read", signal === undefined ? {} : { signal });
66
+ try {
67
+ return (await handle.read(0, undefined, signal === undefined ? {} : { signal })).events;
68
+ }
69
+ finally {
70
+ await handle.close();
71
+ }
72
+ }
73
+ export class CodexProvider extends Service {
74
+ /**
75
+ * `agentDefaultModel` and `settings` are load-bearing, not decorative (OMP
76
+ * twin, same boot-order trap): the harness owns the "default for new sessions"
77
+ * selection there (`@deepseek-ai/dsh-agent-default-model`, mounted by dsh-base
78
+ * with a hard-coded `deepseek-official/deepseek-flash`), and this bridge is the
79
+ * only thing that feeds it the codex config's real default. Without the inject
80
+ * the context cannot resolve the service at apply time and the push in
81
+ * #registerDefaultModel never fires — the picker keeps advertising a model no
82
+ * adapter in this world serves.
83
+ */
84
+ static inject = ["agents", "sessions", "llm", "agentDefaultModel", "settings"];
85
+ /** Plain holder — prevents Cordis re-tracing the factory's ctx through a caller shadow. */
86
+ runtime;
87
+ /**
88
+ * The world's DSH home — captured once from the world's own `dshHomePath`
89
+ * (never ambient env inside plugins). It anchors adapter-owned DSH state
90
+ * (the session map) only; the codex app home is the CLI's native `~/.codex`
91
+ * (2026-09-17 ruling), resolved on demand via resolveCodexHome().
92
+ */
93
+ home;
94
+ /** Echo/staleness guard for the app-wide default-model push. */
95
+ defaultModelKey = "";
96
+ /**
97
+ * The live `ctx.llm` registration for the codex routes, when one exists.
98
+ * Held so a later store change can re-point it atomically: re-registering
99
+ * from scratch would race the picker and briefly lose every provider.
100
+ */
101
+ routes;
102
+ /** Epoch ms of the last reconcile with the user's `config.toml`. */
103
+ syncedAt;
104
+ constructor(ctx) {
105
+ super(ctx, "codexProvider");
106
+ this.runtime = { ctx };
107
+ // Native-home ruling (2026-09-17): the Codex CLI keeps its own native home
108
+ // (~/.codex; $CODEX_HOME overrides in tests) — the adapter never redirects
109
+ // it and never seeds config into it. this.home is only the world's DSH
110
+ // home, anchoring adapter-owned DSH state (the session map).
111
+ this.home = this.#resolveHome();
112
+ const codexHome = resolveCodexHome();
113
+ trace(`home: worldHome=${this.home} codexHome=${codexHome} configExists=${existsSync(join(codexHome, "config.toml"))}`);
114
+ ctx.effect(() => ctx.agents.setFactory(this), "codexProvider.setFactory()");
115
+ // The store is canonical and the native file is a source (2026-10-04).
116
+ // Register the routes the store already implies FIRST, so a malformed
117
+ // config.toml costs the world a reconcile, never its provider list; then
118
+ // reconcile, then keep reconciling on a timer.
119
+ this.#registerModelCatalog();
120
+ this.syncNow();
121
+ const timer = setInterval(() => this.syncNow(), SYNC_INTERVAL_MS);
122
+ timer.unref?.();
123
+ ctx.effect(() => () => clearInterval(timer), "codexProvider.syncTimer()");
124
+ this.#registerDefaultModel();
125
+ // The roster must be visible to the API gateway's root-level remote
126
+ // enumeration (dsh-host-apiproxy read it from the root service table);
127
+ // nesting it under a child fiber via ctx.plugin hides its @Remote routes
128
+ // from the gateway and every agentPresets/* call 404s. Register it on
129
+ // this plugin's own (top-level) fiber instead — its Service constructor
130
+ // ties teardown to this fiber via reflect.provide.
131
+ // 0.1.7: roster/selection/document RPC + the `agentPreset` session
132
+ // projection all live in the host AgentPresetRegistry (mounted by the
133
+ // dsh-web-app bundle's `agent-preset-registry` row; the adapter patch
134
+ // pins its `default` to the Codex preset). The adapter only registers
135
+ // its one fixed preset into the registry.
136
+ registerCodexPreset(ctx);
137
+ // The Hub settings panel's data plane (2026-10-04 multi-provider ruling on
138
+ // the SDK line): an adapter-owned Typert Remote (`codexProviders/get|save|remove`)
139
+ // on this world's context, exactly as the app-server line mounts it. The
140
+ // hub's carrier forwards `envelope.method` verbatim, so the panel reaches it
141
+ // through the existing `/hub/api?agent=codex-sdk` family — no new hub route,
142
+ // and no provider knowledge leaks out of the adapter (dev-rules §12).
143
+ //
144
+ // `resolveCodexHome` is passed as a THUNK on purpose: the native home is
145
+ // re-resolved per call (so a test's `CODEX_HOME` and an operator's override
146
+ // are both honored) and construction stays read-free — the panel's first
147
+ // `get` is what touches disk.
148
+ new CodexProvidersGateway(ctx, this.home, () => resolveCodexHome(), () => this.refreshRoutes());
149
+ }
150
+ /**
151
+ * Reconcile the canonical provider store with the user's `config.toml` and
152
+ * re-register the `ctx.llm` routes if either moved.
153
+ *
154
+ * Called once at startup and every {@link SYNC_INTERVAL_MS} (2026-10-04 user
155
+ * ruling), plus after every panel mutation through the gateway's `onChange`.
156
+ * Never throws: a provider list is not worth a boot.
157
+ */
158
+ syncNow() {
159
+ try {
160
+ const { store, added, updated } = syncFromNative(this.home, resolveCodexHome());
161
+ if (added.length > 0 || updated.length > 0) {
162
+ this.runtime.ctx.logger.info(`codex-provider: config.toml sync — added ${added.join(", ") || "none"}, refreshed ${updated.join(", ") || "none"}`);
163
+ }
164
+ this.syncedAt = Date.now();
165
+ this.#applyRoutes(store.providers);
166
+ }
167
+ catch (error) {
168
+ this.runtime.ctx.logger.warn(`codex-provider: provider sync failed: ${String(error)}`);
169
+ }
170
+ }
171
+ /** Re-register the `ctx.llm` routes from the current store (panel mutation path). */
172
+ refreshRoutes() {
173
+ try {
174
+ this.#applyRoutes(readProviderStore(this.home).providers);
175
+ }
176
+ catch (error) {
177
+ this.runtime.ctx.logger.warn(`codex-provider: route refresh failed: ${String(error)}`);
178
+ }
179
+ }
180
+ /**
181
+ * The provider id the user's own `config.toml` names, mapped into the store.
182
+ * It is the app-wide default's provider and the fallback for a session whose
183
+ * logged selection predates the multi-provider rework. `undefined` when the
184
+ * native file names nothing this world can serve.
185
+ */
186
+ nativeProviderId() {
187
+ const native = readNativeSelection(resolveCodexHome());
188
+ const wanted = native.providerId;
189
+ if (wanted === undefined)
190
+ return undefined;
191
+ const providers = readProviderStore(this.home).providers;
192
+ return providers.find((entry) => entry.id === wanted)?.id ?? undefined;
193
+ }
194
+ /** The store entry a DSH route id names. */
195
+ providerEntry(providerId) {
196
+ return readProviderStore(this.home).providers.find((entry) => entry.id === providerId);
197
+ }
198
+ /**
199
+ * The factory's session-level route fallback — always ONE coherent pair.
200
+ *
201
+ * The ambient `agentDefaultModel` selection is not authoritative here (user
202
+ * directive 2026-09-15), and neither is a global "active" provider any more:
203
+ * the session's OWN `model/selection` decides, resolved by the agent at each
204
+ * turn. These options are what a session with no selection of its own starts
205
+ * from.
206
+ *
207
+ * The one trap this method exists to close: a cached default can outlive the
208
+ * model list it named, because the panel may have removed that model since
209
+ * (upstream persists the last pick as the deployment default). Honouring a pair
210
+ * the provider no longer serves would seed a route nobody can run, so a request
211
+ * wins only when BOTH halves are still servable; otherwise the user's own
212
+ * `config.toml` pair takes over, which is authoritative for exactly the provider
213
+ * it is in force for.
214
+ */
215
+ sessionAgentOptions(requested) {
216
+ const native = readNativeSelection(resolveCodexHome());
217
+ const wanted = requested?.provider !== undefined && typeof requested.model === "string" ? requested : undefined;
218
+ const serves = wanted !== undefined
219
+ && this.providerEntry(wanted.provider)?.models.some((model) => model.id === wanted.model) === true;
220
+ if (wanted !== undefined && serves)
221
+ return { ...wanted, provider: wanted.provider, model: wanted.model };
222
+ const provider = this.nativeProviderId();
223
+ const model = native.model !== undefined && native.model !== "" ? native.model : undefined;
224
+ return {
225
+ ...(requested ?? {}),
226
+ ...(provider === undefined ? {} : { provider }),
227
+ ...(model === undefined ? {} : { model }),
228
+ };
229
+ }
230
+ /**
231
+ * The write path is mandatory now (2026-09-16): this factory owns the DSH
232
+ * session log through the upstream persistence service — a session created
233
+ * or resumed without a backend would persist nothing.
234
+ */
235
+ requirePersistence(op) {
236
+ const persistence = this.runtime.ctx.get("sessionPersistence");
237
+ if (persistence === undefined) {
238
+ throw new Error(`cannot ${op} a Codex session: session persistence is not configured`);
239
+ }
240
+ return persistence;
241
+ }
242
+ /**
243
+ * Take a fresh session's write ownership. Nothing is appended here: the
244
+ * constructor seed (which never re-emits through `session/event`) is stored
245
+ * by `appendUnstoredSuffix` at the publication commit point, so a failed or
246
+ * cancelled setup closes an unmaterialized handle and leaves no residue.
247
+ */
248
+ async createStoredSession(persistence, session, signal) {
249
+ const handle = await persistence.create(session.header, {
250
+ inheritedEventCount: session.inheritedEventCount,
251
+ ...(signal === undefined ? {} : { signal }),
252
+ });
253
+ return { handle, storedCount: 0 };
254
+ }
255
+ /**
256
+ * Runtime facts the agent stamps into the log and needs to pin a provider:
257
+ * Codex's own system prompt (rollout HEAD `base_instructions`, readable once
258
+ * the thread exists), the launch that pins a provider, and the provider's own
259
+ * context window for one of its models.
260
+ */
261
+ agentRuntime(client, id) {
262
+ const codexHome = resolveCodexHome();
263
+ return {
264
+ systemPrompt: () => {
265
+ const threadId = client.threadId;
266
+ if (threadId === null)
267
+ return undefined;
268
+ return readRolloutHead(codexHome, threadId)?.baseInstructions;
269
+ },
270
+ // The whole on-the-fly mechanism in one call: the agent asks for the
271
+ // config/env that pin a provider, and gets them without a single write to
272
+ // the user's home (the per-provider model catalog is ours).
273
+ launchFor: (providerId) => {
274
+ const entry = this.providerEntry(providerId);
275
+ return entry === undefined ? undefined : providerSdkLaunch(this.home, codexHome, entry);
276
+ },
277
+ contextWindowFor: (providerId, model) => {
278
+ const entry = this.providerEntry(providerId);
279
+ const found = entry?.models.find((candidate) => candidate.id === model);
280
+ return found?.contextWindow;
281
+ },
282
+ // RC-4: turn-time native rejections mark the map so the next resume
283
+ // fails fast (CodexResumeError NATIVE_REJECTED) instead of burning
284
+ // another turn on a dead thread.
285
+ markNotResumable: () => {
286
+ try {
287
+ upsertSession(this.home, String(id), { resumable: false });
288
+ this.runtime.ctx.logger.warn(`codex-provider: session ${String(id)} marked not resumable after a native rejection`);
289
+ }
290
+ catch (error) {
291
+ this.runtime.ctx.logger.warn(`codex-provider: failed to mark ${String(id)} not resumable: ${String(error)}`);
292
+ }
293
+ },
294
+ };
295
+ }
296
+ /** The world's DSH home: boot-provided dshHomePath first, env last. */
297
+ #resolveHome() {
298
+ const fromBoot = this.runtime.ctx.get("dshHomePath");
299
+ if (typeof fromBoot === "string" && fromBoot !== "")
300
+ return fromBoot;
301
+ if (typeof fromBoot === "function") {
302
+ const resolved = fromBoot();
303
+ if (typeof resolved === "string" && resolved !== "")
304
+ return resolved;
305
+ }
306
+ return process.env.DSH_HOME ?? join(process.cwd(), ".tests", "superd");
307
+ }
308
+ /**
309
+ * App-wide wiring the adapter owns (user 2026-09-15): the SDK has no
310
+ * default-model concept, so the bridge publishes the codex config.toml
311
+ * default into the world's `agentDefaultModel`. One-directional on purpose —
312
+ * the config is read-only for us (S7 single-direction valve), which makes the
313
+ * codex world's default sticky. Echo-guarded by the saved key.
314
+ *
315
+ * Why the write goes through `settings.replace` and not through
316
+ * `agentDefaultModel.saveSelection`: the harness service installs its section
317
+ * from an `inject(["settings"], …)` callback but then writes through
318
+ * `this.ctx.get("settings")?.replace(…)` on its UNSCOPED context, where
319
+ * `settings` is not injected — the optional chain swallows the lookup and the
320
+ * call is a silent no-op (the settings file is never touched, and the picker
321
+ * keeps showing the base default). Same defect and same fix as the OMP twin
322
+ * (`#saveDefaultSelection`): write the installed section directly through our
323
+ * own injected `settings` service, which the harness folds back via
324
+ * `installSection`, so every other consumer sees the new default.
325
+ */
326
+ #registerDefaultModel() {
327
+ this.runtime.ctx.inject(["agentDefaultModel", "settings"], (mCtx) => {
328
+ const service = mCtx.get("agentDefaultModel");
329
+ if (service === undefined)
330
+ return;
331
+ // The default is whatever the user's own codex config runs, expressed in
332
+ // the store's own route id so the harness can resolve it. A config naming
333
+ // a provider this world has no route for is not a default we can push.
334
+ const native = readNativeSelection(resolveCodexHome());
335
+ const nativeProvider = this.nativeProviderId();
336
+ const nativeModel = native.model;
337
+ if (nativeProvider === undefined || nativeModel === undefined || nativeModel === "") {
338
+ trace("default model push skipped: config.toml names no servable provider/model");
339
+ return;
340
+ }
341
+ const target = { provider: nativeProvider, model: nativeModel };
342
+ const targetKey = `${target.provider}/${target.model}`;
343
+ const current = service.currentSelection?.();
344
+ const currentKey = current === undefined ? undefined : `${current.provider}/${current.model}`;
345
+ if (currentKey === targetKey || currentKey === this.defaultModelKey)
346
+ return;
347
+ trace(`default model: ${currentKey ?? "none"} → ${targetKey}`);
348
+ const settings = mCtx.get("settings");
349
+ void Promise.resolve(settings?.replace?.("agent-default-model", target))
350
+ // Best-effort second path: harmless when upstream's no-op swallows it,
351
+ // and it keeps the in-memory selection fresh on builds where it works.
352
+ .then(() => service.saveSelection?.(target))
353
+ .then(() => {
354
+ this.defaultModelKey = targetKey;
355
+ this.runtime.ctx.logger.info(`codex-provider: default model ← codex config: ${targetKey}`);
356
+ })
357
+ .catch((error) => {
358
+ this.runtime.ctx.logger.warn(`codex-provider: default model save failed: ${String(error)}`);
359
+ });
360
+ });
361
+ }
362
+ /**
363
+ * Register the codex routes on `ctx.llm` — ONE ROUTE PER PROVIDER with
364
+ * models (2026-10-04 rework).
365
+ *
366
+ * The harness's model selector is provider-grouped, and its per-session
367
+ * choice is durable (`model/selection` + the `modelSelection` projection), so
368
+ * expressing the Codex provider list as real routes is what makes the whole
369
+ * feature DSH-native: nothing in Super D has to model "the active provider" or
370
+ * remember which route a session last used.
371
+ *
372
+ * A provider with no models yet registers no route — codex itself cannot run
373
+ * it until the user says which model to ask for, and an empty group in the
374
+ * picker would be noise.
375
+ */
376
+ #registerModelCatalog() {
377
+ this.#applyRoutes(readProviderStore(this.home).providers);
378
+ }
379
+ /** Register or atomically re-point the codex routes at the given providers. */
380
+ #applyRoutes(providers) {
381
+ const llm = this.runtime.ctx.get("llm");
382
+ if (llm === undefined)
383
+ return;
384
+ const ids = routeIds({ version: 1, providers });
385
+ if (this.routes === undefined) {
386
+ if (ids.length === 0) {
387
+ // `registerAdapter` refuses an empty initial set, and there is nothing
388
+ // to serve yet; the next sync or panel save will register the routes.
389
+ trace("route registration deferred: no provider with models yet");
390
+ return;
391
+ }
392
+ this.routes = llm.registerAdapter([...ids], new CodexLlmAdapter(this.home));
393
+ trace(`routes registered: ${ids.join(", ")}`);
394
+ return;
395
+ }
396
+ // `replace` validates the whole candidate set before swapping, so a bad id
397
+ // cannot leave the registry half-updated — but a throw here must not take
398
+ // the caller (a timer, or a panel save) down with it.
399
+ this.routes.replace([...ids]);
400
+ trace(`routes replaced: ${ids.join(", ") || "none"}`);
401
+ }
402
+ async createAgent(ownerCtx, options) {
403
+ const loopCtx = this.runtime.ctx;
404
+ const id = options.sessionId;
405
+ const meta = options.meta ?? {};
406
+ const cwd = meta.cwd;
407
+ // Fork (the apiproxy's `session.fork`) seeds a copied-turn prefix but
408
+ // Codex has no native fork: fail the fork cleanly at the factory boundary.
409
+ // `parentSession` in create metadata is set only by the fork path
410
+ // (subagents go through `ctx.subagents` instead).
411
+ if (meta.parentSession !== undefined) {
412
+ throw new Error(`cannot fork session "${meta.parentSession}" onto the Codex provider: Codex has no native session fork`);
413
+ }
414
+ // The DSH session log is this factory's write path (2026-09-16): without
415
+ // the upstream persistence service a created session would persist
416
+ // nothing, so require it up front.
417
+ const persistence = this.requirePersistence("create");
418
+ // Approval policy is pinned at Codex thread construction and the Dash
419
+ // session does not exist yet at this point (it is prepared + announced —
420
+ // which is what stamps `permission/preset` via pinInitialPermission —
421
+ // only inside setupAndPublish, AFTER this spawn), so the effective
422
+ // preset cannot come from session events: the env override
423
+ // (CODEX_APPROVAL_MODE, headless runs) wins, else the permission
424
+ // service's default, mapped onto the launch policy.
425
+ const preset = envApprovalMode() ?? defaultPermissionPreset(loopCtx);
426
+ const approvalPolicy = codexApprovalMode(preset);
427
+ const spawnCwd = validatedCwd(cwd) ?? process.cwd();
428
+ trace(`create id=${id} preset=${preset ?? "none"} approval=${approvalPolicy} spawnCwd=${spawnCwd}${envApprovalMode() === undefined ? "" : " (env override)"}`);
429
+ // Backend takes ZERO part in the UI's browser-local new-session draft
430
+ // beyond the (zero-IO) thread construction: no rollout, no session
431
+ // events — the session announces blank. The thread materializes on the
432
+ // first prompt.
433
+ // The world home is threaded through: it is where the durable SDK runtime
434
+ // is fetched on the first real prompt (sdk-runtime.ts).
435
+ const client = await CodexSdkClient.spawn(["--approval-mode", approvalPolicy], spawnCwd, this.home);
436
+ // No eager `setModel` here any more (2026-10-04): the agent resolves the
437
+ // whole route — provider included — at the top of every turn and hands it to
438
+ // the client, so the seeding below is the only thing `create` says about
439
+ // where the first turn runs.
440
+ const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
441
+ ...(options.seed === undefined ? {} : { seed: options.seed }),
442
+ ...(meta === undefined ? {} : { meta }),
443
+ ...(options.inheritedEventCount === undefined ? {} : { inheritedEventCount: options.inheritedEventCount }),
444
+ }));
445
+ let stored;
446
+ let handedOff = false;
447
+ try {
448
+ stored = await this.createStoredSession(persistence, preparation.session, options.signal);
449
+ // Identity record BEFORE publication: the DSH id → Codex thread pairing
450
+ // (threadId null until the first turn) is what resume resolves.
451
+ upsertSession(this.home, String(id), {
452
+ threadId: client.threadId,
453
+ cwd: spawnCwd,
454
+ createdAt: Date.now(),
455
+ preset: preset ?? null,
456
+ });
457
+ this.followThreadId(id, client);
458
+ handedOff = true;
459
+ return await setupAndPublish(loopCtx, ownerCtx, id, this.sessionAgentOptions(options.agentOptions), options.setup, preparation.session, client, options.parentAgent, "startup", { enterSession: true, preparation }, stored, this.agentRuntime(client, id));
460
+ }
461
+ catch (error) {
462
+ trace(`create ${id} THREW: ${String(error)}`);
463
+ client.close();
464
+ if (!handedOff)
465
+ await stored?.handle.close().catch(() => { });
466
+ throw error;
467
+ }
468
+ finally {
469
+ if (!handedOff)
470
+ preparation[Symbol.dispose]();
471
+ }
472
+ }
473
+ async resume(ownerCtx, options) {
474
+ const loopCtx = this.runtime.ctx;
475
+ const id = options.resumeSessionId;
476
+ // Identity (2026-09-16 "mapping only", reaffirmed 2026-09-18): the DSH
477
+ // session id resolves through the adapter-owned map — the thread id is
478
+ // recorded when Codex materializes it. The adapter NEVER scans the native
479
+ // ~/.codex session storage to recover pairings (2026-09-18 user ruling:
480
+ // the discovery tier violated the design — DSH log is the transcript
481
+ // authority; the native home contributes config/settings only). Every
482
+ // miss fails closed with a stable code (CodexResumeError).
483
+ const persistence = this.requirePersistence("resume");
484
+ const record = sessionRecord(this.home, String(id));
485
+ trace(`resume id=${id} record=${record === undefined ? "MISSING" : record.threadId ?? "NO-THREAD"}${record?.resumable === false ? " (marked not resumable)" : ""}`);
486
+ if (record === undefined) {
487
+ throw new CodexResumeError("SESSION_MAP_MISS", `cannot resume session "${id}": no Codex thread is recorded for this Dash session id`);
488
+ }
489
+ if (record.threadId === null) {
490
+ // C2: never auto-start a fresh thread — the session's history is in the
491
+ // (missing) native thread, not in a blank one.
492
+ throw new CodexResumeError("NATIVE_THREAD_NEVER_STARTED", `cannot resume session "${id}": its Codex thread was never started (no thread id recorded)`);
493
+ }
494
+ if (record.resumable === false) {
495
+ // A previous native resume was rejected at turn time (plan RC-4): fail
496
+ // fast with the clear refusal instead of burning another turn.
497
+ throw new CodexResumeError("NATIVE_REJECTED", `cannot resume session "${id}": its Codex thread ${record.threadId} was rejected by the native runtime in an earlier attempt`);
498
+ }
499
+ // Pre-validation (plan RC-4): the rollout must exist in the CURRENT codex
500
+ // home BEFORE spawning — covers rollouts stranded under the abandoned
501
+ // `<worldHome>/.codex` home (never migrated) and pruned rollouts alike.
502
+ // Without this gate the failure would surface only at the first turn
503
+ // (resumeThread is zero-IO).
504
+ if (readRolloutHead(resolveCodexHome(), record.threadId) === undefined) {
505
+ throw new CodexResumeError("ROLLOUT_MISSING", `cannot resume session "${id}": no rollout for Codex thread ${record.threadId} exists in the current Codex home (${resolveCodexHome()})`);
506
+ }
507
+ // Spawn cwd comes from the recorded pairing — re-realpath and re-verify so
508
+ // a moved/removed directory cannot aim a resume outside a live path.
509
+ const spawnCwd = validatedCwd(record.cwd);
510
+ if (spawnCwd === undefined) {
511
+ throw new Error(`cannot resume session "${id}": its recorded working directory no longer exists`);
512
+ }
513
+ // Approval policy for the re-attached thread: launch-only, so it is
514
+ // decided BEFORE the spawn from the recorded preset, else the persisted
515
+ // session log, else the permission service's default. CODEX_APPROVAL_MODE
516
+ // (headless runs) overrides; no preset → never.
517
+ const envMode = envApprovalMode();
518
+ const preset = envMode !== undefined
519
+ ? envMode
520
+ : record.preset ??
521
+ presetFromEvents(await readStoredEvents(persistence, id, options.signal)) ??
522
+ defaultPermissionPreset(loopCtx);
523
+ const approvalPolicy = codexApprovalMode(preset);
524
+ trace(`resume id=${id} spawnCwd=${spawnCwd} preset=${preset ?? "none"} approval=${approvalPolicy}`);
525
+ // Re-attach to the Codex thread by its id (Codex owns the live agent
526
+ // context and keeps generating it from here on; the model context is
527
+ // restored by Codex itself from its rollout).
528
+ const client = await CodexSdkClient.spawn(["--approval-mode", approvalPolicy, "--resume-thread", record.threadId], spawnCwd, this.home);
529
+ let handle;
530
+ let stored;
531
+ let preparation;
532
+ let handedOff = false;
533
+ try {
534
+ // Taking write ownership FIRST excludes a concurrent resume of the same
535
+ // id (a live agent's handle holds the claim in this process).
536
+ handle = await persistence.open(id, "write", options.signal === undefined ? {} : { signal: options.signal });
537
+ // Crash repair belongs to the agent layer: an interrupted final turn
538
+ // receives synthetic closers appended through the same handle.
539
+ const cold = await handle.read(0, undefined, options.signal === undefined ? {} : { signal: options.signal });
540
+ const closers = interruptedTurnClosers(cold.events);
541
+ if (closers.length > 0)
542
+ await handle.append(closers);
543
+ preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
544
+ seed: [...cold.events, ...closers],
545
+ meta: structuredClone(handle.header),
546
+ inheritedEventCount: handle.inheritedEventCount,
547
+ eventState: cold.eventState,
548
+ }));
549
+ stored = { handle, storedCount: cold.events.length + closers.length };
550
+ trace(`resume id=${id} seeded ${cold.events.length} events (+${closers.length} closers)`);
551
+ handle = undefined; // ownership passes to setupAndPublish
552
+ this.followThreadId(id, client);
553
+ handedOff = true;
554
+ return await setupAndPublish(loopCtx, ownerCtx, id, this.sessionAgentOptions(options.agentOptions), options.setup, preparation.session, client, options.parentAgent, "resume", { enterSession: true, preparation }, stored, this.agentRuntime(client, id));
555
+ }
556
+ catch (error) {
557
+ trace(`resume ${id} THREW: ${String(error)} | stack=${error instanceof Error ? error.stack?.slice(0, 400) : "n/a"}`);
558
+ client.close();
559
+ if (!handedOff)
560
+ await handle?.close().catch(() => { });
561
+ throw error;
562
+ }
563
+ finally {
564
+ if (!handedOff)
565
+ preparation?.[Symbol.dispose]();
566
+ }
567
+ }
568
+ /**
569
+ * Watch the client for its first materialized thread id and persist the
570
+ * Dash↔thread pairing once (the record is the resume authority; the
571
+ * create-time entry starts with `threadId: null`).
572
+ * Arrow FIELD (not a `#` method and not a true-private field): the provider
573
+ * is exposed through a Cordis tracing proxy, and hard-private members fail
574
+ * their brand check on the proxy receiver (the same reason setupAndPublish
575
+ * is module-level). A persistence failure here is logged, never thrown into
576
+ * the client's event stream.
577
+ */
578
+ followThreadId = (id, client) => {
579
+ // RE-ARMING observer (plan C6): newSession() swaps threads mid-session, so
580
+ // the pairing must be re-upserted whenever the client's thread id CHANGES,
581
+ // not just once. Unsubscribing after the first capture would leave the map
582
+ // pointing at the discarded thread, and the next resume would re-attach to
583
+ // a thread the session no longer runs on.
584
+ let lastRecorded = client.threadId;
585
+ const observe = (threadId) => {
586
+ lastRecorded = threadId;
587
+ try {
588
+ upsertSession(this.home, String(id), { threadId });
589
+ trace(`thread id recorded for ${String(id)}: ${threadId}`);
590
+ }
591
+ catch (error) {
592
+ this.runtime.ctx.logger.warn(`codex-provider: failed to persist the Codex thread id for ${String(id)}: ${String(error)}`);
593
+ }
594
+ };
595
+ if (lastRecorded !== null)
596
+ observe(lastRecorded);
597
+ client.on(() => {
598
+ const observed = client.threadId;
599
+ if (observed === null || observed === lastRecorded)
600
+ return;
601
+ observe(observed);
602
+ });
603
+ };
604
+ }
605
+ /**
606
+ * Durable flush of the session's pre-publication suffix. Constructor seed
607
+ * markers and setup-window events never re-emit through `session/event`, so
608
+ * publication must push them through the handle before live events start
609
+ * routing into it. Advances by what was stored (an event appended during the
610
+ * await stays unstored for the next flush).
611
+ */
612
+ async function appendUnstoredSuffix(stored, session) {
613
+ const suffix = session.snapshotEvents(SessionLogOffset(stored.storedCount));
614
+ if (suffix.length > 0)
615
+ await stored.handle.append(suffix);
616
+ stored.storedCount += suffix.length;
617
+ }
618
+ /**
619
+ * Shared creation/resume transaction: build the agent over the prepared
620
+ * session, run unpublished setup, flush the unstored suffix, and publish both
621
+ * in order; the owned storage handle is drained (closed) on teardown. Kept as
622
+ * a module-level function (not a private method) because Cordis exposes the
623
+ * provider through a tracing proxy, which breaks hard-private (`#`) receivers.
624
+ */
625
+ async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, session, client, parentAgent, source, opts, stored, runtimeInfo) {
626
+ let detachSession;
627
+ let detachAgent;
628
+ let agent;
629
+ // Late-bound teardown the agent triggers itself after its idle window
630
+ // expires (see CodexAgent's idle exit) — defined only after publication.
631
+ let idleExit;
632
+ try {
633
+ // Build the agent shim over the session and the live client.
634
+ agent = new CodexAgent(loopCtx, id, agentOptions, session, client, () => idleExit?.(), runtimeInfo);
635
+ // Pin THIS session to the codex route. The global default-model setting is
636
+ // a best-effort push (the settings service may flush late, and a shared
637
+ // test home can hold another line's default), so the factory must not let
638
+ // the first prompt depend on it: `selectForNextRequest` is the same
639
+ // per-session mechanism `session/selectModel` uses (appends the log-only
640
+ // `model/selection` and caches the next request's route).
641
+ try {
642
+ // The seed comes from ONE source — the factory's reconciled options —
643
+ // because that pair is already coherent. Building it from the native
644
+ // provider id and the deployment default model independently is what once
645
+ // produced a route no provider served (`zhipu-cn/deepseek-flash`); taking
646
+ // one object makes that mistake unrepresentable (`seedRoute`).
647
+ const route = seedRoute(agentOptions);
648
+ if (route !== undefined) {
649
+ const selection = { provider: route.provider, model: route.model };
650
+ // Mirrors the upstream session-controller's `selectForNextRequest`:
651
+ // the durable log-only `model/selection` plus an agent-scoped
652
+ // `ModelSelectionRef` (same shape the controller installs) that the
653
+ // next prompt's route check reads. Synchronous on purpose — the first
654
+ // prompt must not depend on the global default's late flush.
655
+ let picked = selection;
656
+ installModelSelection(agent.ctx, {
657
+ get current() { return picked; },
658
+ set current(next) { picked = next; },
659
+ assembled: undefined,
660
+ });
661
+ // `model/selection` is contributed to `SessionEventMap` by the upstream
662
+ // session-controller package (declaration merging); this adapter does
663
+ // not depend on it, so the append goes through a narrow local cast.
664
+ session.append("model/selection", selection);
665
+ trace(`session route pinned: ${route.provider}/${route.model}`);
666
+ }
667
+ }
668
+ catch (error) {
669
+ // Fail-soft: the boot-time default-model push remains the fallback.
670
+ trace(`session route pin failed: ${String(error)}`);
671
+ }
672
+ // Session identity (the preset stamp) and the Codex system prompt commit
673
+ // on the first turn via CodexAgent's #bootstrapSessionIdentity /
674
+ // #emitSystemMessage: a freshly created session announces completely
675
+ // blank, so the backend takes zero part in the UI's new-session draft.
676
+ // Composition-only setup on the unpublished agent scope.
677
+ const commit = await setup?.(agent.ctx, agent);
678
+ // Publish: flush the unstored suffix (seed + setup-window events) through
679
+ // the owned handle, then enter both session and agent, announce in order,
680
+ // and signal session-start. The commit runs immediately before
681
+ // publication.
682
+ commit?.commit();
683
+ await appendUnstoredSuffix(stored, session);
684
+ if (opts.enterSession) {
685
+ detachSession = agent.ctx.sessions.enter(session);
686
+ agent.ctx.sessions.announce(session);
687
+ }
688
+ detachAgent = loopCtx.agents.enter(agent, parentAgent);
689
+ // 0.1.6: announce(agent, source) is required, async, and emits agent/created
690
+ // itself — the manual agent/session-start emission is gone upstream.
691
+ await loopCtx.agents.announce(agent, source);
692
+ let disposed = false;
693
+ let unfollowOwner;
694
+ const dispose = async () => {
695
+ if (disposed)
696
+ return;
697
+ disposed = true;
698
+ trace(`dispose() called for agent ${id} (source=${source})`);
699
+ unfollowOwner?.();
700
+ await agent?.dispose();
701
+ detachAgent?.();
702
+ detachSession?.();
703
+ // Drain queued appends; a durability failure surfaces to the caller
704
+ // (the owner-unload and idle-exit paths log it).
705
+ await stored.handle.close();
706
+ };
707
+ idleExit = () => {
708
+ void dispose().catch((error) => {
709
+ loopCtx.logger.warn(`codex-provider: teardown of ${id} failed: ${String(error)}`);
710
+ });
711
+ };
712
+ // Follow the owner: a caller-fiber unload tears this agent down.
713
+ unfollowOwner = ownerCtx.effect(() => () => {
714
+ void dispose().catch((error) => {
715
+ loopCtx.logger.warn(`codex-provider: owner-unload teardown of ${id} failed: ${String(error)}`);
716
+ });
717
+ }, `codexProvider.lifecycle(${id})`);
718
+ return { agent, dispose };
719
+ }
720
+ catch (error) {
721
+ // Unwind the half-published transaction. A failure between `enter` and
722
+ // the announcements (e.g. a persistence listener rejecting the session)
723
+ // must not leave a live-but-dead entry the API resolver would serve.
724
+ trace(`setupAndPublish ${id} FAILED: ${String(error)}`);
725
+ detachAgent?.();
726
+ detachSession?.();
727
+ void agent?.dispose().catch(() => { });
728
+ client.close();
729
+ throw error;
730
+ }
731
+ finally {
732
+ // Release the preparation's per-id reservation on every path (mirrors
733
+ // the reference loop's unconditional dispose): the session it seeded is
734
+ // already published above, so a same-process re-resume of this id can
735
+ // prepare again instead of colliding with a leaked reservation.
736
+ opts.preparation?.[Symbol.dispose]();
737
+ }
738
+ }
739
+ export default CodexProvider;