super-dsh 0.1.0 → 0.1.2

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/agent-agy/bridge/agy_bridge.py +250 -0
  2. package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
  3. package/agent-agy/cordis.patch.yml +49 -0
  4. package/agent-agy/dist/adapter.d.ts +15 -0
  5. package/agent-agy/dist/adapter.js +42 -0
  6. package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
  7. package/agent-agy/dist/agent-preset-agy.js +169 -0
  8. package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
  9. package/agent-agy/dist/agent-preset-projection.js +33 -0
  10. package/agent-agy/dist/agent.d.ts +137 -0
  11. package/agent-agy/dist/agent.js +493 -0
  12. package/agent-agy/dist/agy-cli-client.d.ts +137 -0
  13. package/agent-agy/dist/agy-cli-client.js +301 -0
  14. package/agent-agy/dist/agy-client.d.ts +160 -0
  15. package/agent-agy/dist/agy-client.js +252 -0
  16. package/agent-agy/dist/agy-sessions.d.ts +24 -0
  17. package/agent-agy/dist/agy-sessions.js +116 -0
  18. package/agent-agy/dist/agy-store.d.ts +39 -0
  19. package/agent-agy/dist/agy-store.js +83 -0
  20. package/agent-agy/dist/inbox.d.ts +26 -0
  21. package/agent-agy/dist/inbox.js +53 -0
  22. package/agent-agy/dist/index.d.ts +80 -0
  23. package/agent-agy/dist/index.js +505 -0
  24. package/agent-agy/dist/models.d.ts +41 -0
  25. package/agent-agy/dist/models.js +74 -0
  26. package/agent-agy/dist/world-plugin.d.ts +9 -0
  27. package/agent-agy/dist/world-plugin.js +97 -0
  28. package/agent-agy/package.json +64 -0
  29. package/agent-claude/cordis.patch.yml +42 -0
  30. package/agent-claude/dist/adapter.js +62 -0
  31. package/agent-claude/dist/agent-preset-claude.js +169 -0
  32. package/agent-claude/dist/agent-preset-projection.js +30 -0
  33. package/agent-claude/dist/agent.js +1271 -0
  34. package/agent-claude/dist/claude-client.js +357 -0
  35. package/agent-claude/dist/claude-events.js +223 -0
  36. package/agent-claude/dist/claude-home.js +29 -0
  37. package/agent-claude/dist/commands.js +182 -0
  38. package/agent-claude/dist/content.js +134 -0
  39. package/agent-claude/dist/inbox.js +53 -0
  40. package/agent-claude/dist/index.js +555 -0
  41. package/agent-claude/dist/input-queue.js +64 -0
  42. package/agent-claude/dist/interaction.js +271 -0
  43. package/agent-claude/dist/models.js +158 -0
  44. package/agent-claude/dist/permission.js +120 -0
  45. package/agent-claude/dist/session-id.js +20 -0
  46. package/agent-claude/dist/session-map.js +165 -0
  47. package/agent-claude/dist/world-plugin.js +14 -0
  48. package/agent-claude/package.json +74 -0
  49. package/agent-codex/README.md +74 -0
  50. package/agent-codex/cordis.patch.yml +77 -0
  51. package/agent-codex/dist/adapter.js +55 -0
  52. package/agent-codex/dist/agent-preset-codex.js +171 -0
  53. package/agent-codex/dist/agent-preset-projection.js +37 -0
  54. package/agent-codex/dist/agent.js +1056 -0
  55. package/agent-codex/dist/app-home.js +65 -0
  56. package/agent-codex/dist/codex-client.js +469 -0
  57. package/agent-codex/dist/codex-events.js +297 -0
  58. package/agent-codex/dist/codex-store.js +190 -0
  59. package/agent-codex/dist/inbox.js +53 -0
  60. package/agent-codex/dist/index.js +605 -0
  61. package/agent-codex/dist/models.js +152 -0
  62. package/agent-codex/dist/permission.js +71 -0
  63. package/agent-codex/dist/session-map.js +170 -0
  64. package/agent-codex/dist/spike.js +46 -0
  65. package/agent-codex/dist/world-plugin.js +96 -0
  66. package/agent-codex/package.json +78 -0
  67. package/agent-hermes/README.md +144 -0
  68. package/agent-hermes/cordis.patch.yml +83 -0
  69. package/agent-hermes/dist/adapter.js +83 -0
  70. package/agent-hermes/dist/agent-preset-hermes.js +171 -0
  71. package/agent-hermes/dist/agent-preset-projection.js +34 -0
  72. package/agent-hermes/dist/agent.js +1068 -0
  73. package/agent-hermes/dist/hermes-client.js +953 -0
  74. package/agent-hermes/dist/hermes-events.js +289 -0
  75. package/agent-hermes/dist/hermes-store.js +158 -0
  76. package/agent-hermes/dist/inbox.js +53 -0
  77. package/agent-hermes/dist/index.js +541 -0
  78. package/agent-hermes/dist/models.js +324 -0
  79. package/agent-hermes/dist/permission.js +60 -0
  80. package/agent-hermes/dist/world-plugin.js +101 -0
  81. package/agent-hermes/package.json +76 -0
  82. package/agent-hub/cordis.patch.yml +12 -0
  83. package/agent-hub/dist/agent-roster.d.ts +34 -0
  84. package/agent-hub/dist/agent-roster.js +27 -0
  85. package/agent-hub/dist/carrier.d.ts +43 -0
  86. package/agent-hub/dist/carrier.js +241 -0
  87. package/agent-hub/dist/client-shim.d.ts +28 -0
  88. package/agent-hub/dist/client-shim.js +251 -0
  89. package/agent-hub/dist/envelope.d.ts +54 -0
  90. package/agent-hub/dist/envelope.js +92 -0
  91. package/agent-hub/dist/gateway.d.ts +28 -0
  92. package/agent-hub/dist/gateway.js +18 -0
  93. package/agent-hub/dist/index-pass.d.ts +22 -0
  94. package/agent-hub/dist/index-pass.js +47 -0
  95. package/agent-hub/dist/index.d.ts +29 -0
  96. package/agent-hub/dist/index.js +29 -0
  97. package/agent-hub/dist/labels.d.ts +45 -0
  98. package/agent-hub/dist/labels.js +83 -0
  99. package/agent-hub/dist/ownership.d.ts +37 -0
  100. package/agent-hub/dist/ownership.js +54 -0
  101. package/agent-hub/dist/roster.d.ts +28 -0
  102. package/agent-hub/dist/roster.js +38 -0
  103. package/agent-hub/dist/rpc.d.ts +38 -0
  104. package/agent-hub/dist/rpc.js +52 -0
  105. package/agent-hub/dist/spawn-world.d.ts +46 -0
  106. package/agent-hub/dist/spawn-world.js +72 -0
  107. package/agent-hub/dist/targets.d.ts +33 -0
  108. package/agent-hub/dist/targets.js +30 -0
  109. package/agent-hub/dist/world-entry.d.ts +31 -0
  110. package/agent-hub/dist/world-entry.js +51 -0
  111. package/agent-hub/dist/world-host.d.ts +69 -0
  112. package/agent-hub/dist/world-host.js +44 -0
  113. package/agent-hub/dist/world-join.d.ts +16 -0
  114. package/agent-hub/dist/world-join.js +122 -0
  115. package/agent-hub/dist/world-mount.d.ts +52 -0
  116. package/agent-hub/dist/world-mount.js +52 -0
  117. package/agent-hub/dist/world-mux.d.ts +72 -0
  118. package/agent-hub/dist/world-mux.js +224 -0
  119. package/agent-hub/dist/world-provision.d.ts +20 -0
  120. package/agent-hub/dist/world-provision.js +122 -0
  121. package/agent-hub/dist/world-web-server.d.ts +162 -0
  122. package/agent-hub/dist/world-web-server.js +248 -0
  123. package/agent-hub/lib/client/index.js +242 -0
  124. package/agent-hub/package.json +65 -0
  125. package/agent-omp/README.md +77 -0
  126. package/agent-omp/cordis.patch.yml +92 -0
  127. package/agent-omp/dist/adapter.js +36 -0
  128. package/agent-omp/dist/agent-preset-omp.js +171 -0
  129. package/agent-omp/dist/agent-preset-projection.js +36 -0
  130. package/agent-omp/dist/agent.js +1145 -0
  131. package/agent-omp/dist/app-home.js +86 -0
  132. package/agent-omp/dist/discovery.js +116 -0
  133. package/agent-omp/dist/inbox.js +53 -0
  134. package/agent-omp/dist/index.js +574 -0
  135. package/agent-omp/dist/knobs.js +50 -0
  136. package/agent-omp/dist/lazy-rpc.js +161 -0
  137. package/agent-omp/dist/mobile/gesture.js +148 -0
  138. package/agent-omp/dist/mobile/zoom-guard.js +294 -0
  139. package/agent-omp/dist/mobile-boot.js +38 -0
  140. package/agent-omp/dist/models.js +299 -0
  141. package/agent-omp/dist/omp-cli.js +147 -0
  142. package/agent-omp/dist/omp-disk-discovery.js +178 -0
  143. package/agent-omp/dist/omp-store.js +518 -0
  144. package/agent-omp/dist/pairing.js +45 -0
  145. package/agent-omp/dist/permission.js +96 -0
  146. package/agent-omp/dist/protocol.js +13 -0
  147. package/agent-omp/dist/rpc-types.js +1 -0
  148. package/agent-omp/dist/sdk-client.js +378 -0
  149. package/agent-omp/dist/sidecar-client.js +195 -0
  150. package/agent-omp/dist/store/adapter.js +47 -0
  151. package/agent-omp/dist/store/db.js +210 -0
  152. package/agent-omp/dist/store/index.js +59 -0
  153. package/agent-omp/dist/store/reconcile.js +51 -0
  154. package/agent-omp/dist/store/schema.js +40 -0
  155. package/agent-omp/dist/world-plugin.js +96 -0
  156. package/agent-omp/package.json +97 -0
  157. package/agent-omp/sidecar/main.ts +481 -0
  158. package/agent-pi/README.md +67 -0
  159. package/agent-pi/cordis.patch.yml +80 -0
  160. package/agent-pi/dist/adapter.js +53 -0
  161. package/agent-pi/dist/agent-preset-pi.js +169 -0
  162. package/agent-pi/dist/agent-preset-projection.js +29 -0
  163. package/agent-pi/dist/agent.js +949 -0
  164. package/agent-pi/dist/inbox.js +53 -0
  165. package/agent-pi/dist/index.js +595 -0
  166. package/agent-pi/dist/knobs.js +24 -0
  167. package/agent-pi/dist/models.js +200 -0
  168. package/agent-pi/dist/permission.js +66 -0
  169. package/agent-pi/dist/pi-client.js +224 -0
  170. package/agent-pi/dist/pi-events.js +155 -0
  171. package/agent-pi/dist/pi-home.js +55 -0
  172. package/agent-pi/dist/safe-json.js +25 -0
  173. package/agent-pi/dist/session-map.js +119 -0
  174. package/agent-pi/dist/world-plugin.js +97 -0
  175. package/agent-pi/package.json +77 -0
  176. package/cordis.patch.yml +22 -19
  177. package/package.json +88 -25
  178. package/client.js +0 -4
@@ -0,0 +1,605 @@
1
+ /**
2
+ * agent-codex provider — Codex provider plugin for DeepSeek Harness
3
+ * (Agent Worlds AW-E, 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 { emitAgentEvent, installModelSelection } from "@deepseek-ai/dsh-agent";
26
+ import { SessionLogOffset, SessionPreparation, interruptedTurnClosers, } from "@deepseek-ai/dsh-session";
27
+ import { CodexSdkClient } from "./codex-client.js";
28
+ import { CodexAgent } from "./agent.js";
29
+ import { CodexLlmAdapter, CODEX_PROVIDER_ID } from "./adapter.js";
30
+ import { SingleCodexPresetRoster } from "./agent-preset-codex.js";
31
+ import { codexAgentPresetProjection } from "./agent-preset-projection.js";
32
+ import { readRolloutHead, resolveCodexHome } from "./codex-store.js";
33
+ import { CodexResumeError, sessionRecord, upsertSession } from "./session-map.js";
34
+ import { readCodexModelCatalog, CODEX_DEFAULT_MODEL_PLACEHOLDER } from "./models.js";
35
+ import { codexApprovalMode, defaultPermissionPreset, envApprovalMode, presetFromEvents } from "./permission.js";
36
+ /** Realpath of a recorded cwd, accepted only when it names an existing directory. */
37
+ function validatedCwd(cwd) {
38
+ if (cwd === undefined)
39
+ return undefined;
40
+ try {
41
+ const canonical = realpathSync(cwd);
42
+ return statSync(canonical).isDirectory() ? canonical : undefined;
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
48
+ /** Diagnostic trace (CODEX_TRACE=1 on the dsh process enables stderr tracing). */
49
+ const trace = (...parts) => {
50
+ if (process.env.CODEX_TRACE === "1")
51
+ process.stderr.write(`[codex-provider ${Date.now() % 1_000_000}] ${parts.join(" ")}\n`);
52
+ };
53
+ /** Open a read handle, pull the complete stored log, close. */
54
+ async function readStoredEvents(persistence, id, signal) {
55
+ const handle = await persistence.open(id, "read", signal === undefined ? {} : { signal });
56
+ try {
57
+ return (await handle.read(0, undefined, signal === undefined ? {} : { signal })).events;
58
+ }
59
+ finally {
60
+ await handle.close();
61
+ }
62
+ }
63
+ export class CodexProvider extends Service {
64
+ /**
65
+ * `agentDefaultModel` and `settings` are load-bearing, not decorative (OMP
66
+ * twin, same boot-order trap): the harness owns the "default for new sessions"
67
+ * selection there (`@deepseek-ai/dsh-agent-default-model`, mounted by dsh-base
68
+ * with a hard-coded `deepseek-official/deepseek-flash`), and this bridge is the
69
+ * only thing that feeds it the codex config's real default. Without the inject
70
+ * the context cannot resolve the service at apply time and the push in
71
+ * #registerDefaultModel never fires — the picker keeps advertising a model no
72
+ * adapter in this world serves.
73
+ */
74
+ static inject = ["agents", "sessions", "llm", "agentDefaultModel", "settings"];
75
+ /** Plain holder — prevents Cordis re-tracing the factory's ctx through a caller shadow. */
76
+ runtime;
77
+ /**
78
+ * The world's DSH home — captured once from the world's own `dshHomePath`
79
+ * (never ambient env inside plugins). It anchors adapter-owned DSH state
80
+ * (the session map) only; the codex app home is the CLI's native `~/.codex`
81
+ * (2026-09-17 ruling), resolved on demand via resolveCodexHome().
82
+ */
83
+ home;
84
+ /** Echo/staleness guard for the app-wide default-model push. */
85
+ defaultModelKey = "";
86
+ constructor(ctx) {
87
+ super(ctx, "codexProvider");
88
+ this.runtime = { ctx };
89
+ // Native-home ruling (2026-09-17): the Codex CLI keeps its own native home
90
+ // (~/.codex; $CODEX_HOME overrides in tests) — the adapter never redirects
91
+ // it and never seeds config into it. this.home is only the world's DSH
92
+ // home, anchoring adapter-owned DSH state (the session map).
93
+ this.home = this.#resolveHome();
94
+ const codexHome = resolveCodexHome();
95
+ trace(`home: worldHome=${this.home} codexHome=${codexHome} configExists=${existsSync(join(codexHome, "config.toml"))}`);
96
+ ctx.effect(() => ctx.agents.setFactory(this), "codexProvider.setFactory()");
97
+ this.#registerModelCatalog();
98
+ this.#registerDefaultModel();
99
+ // The roster must be visible to the API gateway's root-level remote
100
+ // enumeration (dsh-host-apiproxy read it from the root service table);
101
+ // nesting it under a child fiber via ctx.plugin hides its @Remote routes
102
+ // from the gateway and every agentPresets/* call 404s. Register it on
103
+ // this plugin's own (top-level) fiber instead — its Service constructor
104
+ // ties teardown to this fiber via reflect.provide.
105
+ new SingleCodexPresetRoster(ctx);
106
+ // Drive the `agentPreset` session projection ourselves: the upstream
107
+ // registrant lives in the (disabled) dsh-agent-presets package, but the
108
+ // Web UI gates the preset chip and header label on
109
+ // `projectionValues.agentPreset`. Deferred via inject so the registry's
110
+ // service contract is honored (registers only once it exists).
111
+ ctx.inject(["sessionProjections"], (scoped) => {
112
+ scoped.sessionProjections.register(codexAgentPresetProjection);
113
+ });
114
+ }
115
+ /**
116
+ * The factory owns its sessions' route (user directive 2026-09-15): the
117
+ * ambient `agentDefaultModel` selection is NOT authoritative here — a
118
+ * session created by this factory always runs on the codex route, with the
119
+ * codex config's real default model unless the caller asked for a served
120
+ * codex model. Without an explicit choice the model rides the thread
121
+ * options only when the catalog is readable (the placeholder is omitted so
122
+ * codex's own config default applies).
123
+ */
124
+ sessionAgentOptions(requested) {
125
+ const catalog = readCodexModelCatalog(resolveCodexHome());
126
+ const explicit = requested?.provider === CODEX_PROVIDER_ID && typeof requested.model === "string" && requested.model !== CODEX_DEFAULT_MODEL_PLACEHOLDER
127
+ ? requested.model
128
+ : undefined;
129
+ const model = explicit ?? (catalog.defaultModel === CODEX_DEFAULT_MODEL_PLACEHOLDER ? undefined : catalog.defaultModel);
130
+ return { ...(requested ?? {}), provider: CODEX_PROVIDER_ID, ...(model === undefined ? {} : { model }) };
131
+ }
132
+ /**
133
+ * The write path is mandatory now (2026-09-16): this factory owns the DSH
134
+ * session log through the upstream persistence service — a session created
135
+ * or resumed without a backend would persist nothing.
136
+ */
137
+ requirePersistence(op) {
138
+ const persistence = this.runtime.ctx.get("sessionPersistence");
139
+ if (persistence === undefined) {
140
+ throw new Error(`cannot ${op} a Codex session: session persistence is not configured`);
141
+ }
142
+ return persistence;
143
+ }
144
+ /**
145
+ * Take a fresh session's write ownership. Nothing is appended here: the
146
+ * constructor seed (which never re-emits through `session/event`) is stored
147
+ * by `appendUnstoredSuffix` at the publication commit point, so a failed or
148
+ * cancelled setup closes an unmaterialized handle and leaves no residue.
149
+ */
150
+ async createStoredSession(persistence, session, signal) {
151
+ const handle = await persistence.create(session.header, {
152
+ inheritedEventCount: session.inheritedEventCount,
153
+ ...(signal === undefined ? {} : { signal }),
154
+ });
155
+ return { handle, storedCount: 0 };
156
+ }
157
+ /**
158
+ * Runtime facts the agent stamps into the log: Codex's own system prompt
159
+ * (rollout HEAD `base_instructions`, readable once the thread exists) and
160
+ * the effective route metadata (selected codex model, else the catalog
161
+ * default, plus the catalog's context window).
162
+ */
163
+ agentRuntime(client, id) {
164
+ const codexHome = resolveCodexHome();
165
+ return {
166
+ systemPrompt: () => {
167
+ const threadId = client.threadId;
168
+ if (threadId === null)
169
+ return undefined;
170
+ return readRolloutHead(codexHome, threadId)?.baseInstructions;
171
+ },
172
+ routeContext: (preferredModel) => {
173
+ const catalog = readCodexModelCatalog(codexHome);
174
+ const model = preferredModel !== undefined && preferredModel !== "" && preferredModel !== CODEX_DEFAULT_MODEL_PLACEHOLDER
175
+ ? preferredModel
176
+ : catalog.defaultModel === CODEX_DEFAULT_MODEL_PLACEHOLDER
177
+ ? undefined
178
+ : catalog.defaultModel;
179
+ if (model === undefined)
180
+ return undefined;
181
+ const entry = catalog.models.find((candidate) => candidate.id === model);
182
+ return {
183
+ provider: CODEX_PROVIDER_ID,
184
+ model,
185
+ ...(entry?.contextWindow !== undefined && entry.contextWindow > 0 ? { contextWindow: entry.contextWindow } : {}),
186
+ };
187
+ },
188
+ // RC-4: turn-time native rejections mark the map so the next resume
189
+ // fails fast (CodexResumeError NATIVE_REJECTED) instead of burning
190
+ // another turn on a dead thread.
191
+ markNotResumable: () => {
192
+ try {
193
+ upsertSession(this.home, String(id), { resumable: false });
194
+ this.runtime.ctx.logger.warn(`codex-provider: session ${String(id)} marked not resumable after a native rejection`);
195
+ }
196
+ catch (error) {
197
+ this.runtime.ctx.logger.warn(`codex-provider: failed to mark ${String(id)} not resumable: ${String(error)}`);
198
+ }
199
+ },
200
+ };
201
+ }
202
+ /** The world's DSH home: boot-provided dshHomePath first, env last. */
203
+ #resolveHome() {
204
+ const fromBoot = this.runtime.ctx.get("dshHomePath");
205
+ if (typeof fromBoot === "string" && fromBoot !== "")
206
+ return fromBoot;
207
+ if (typeof fromBoot === "function") {
208
+ const resolved = fromBoot();
209
+ if (typeof resolved === "string" && resolved !== "")
210
+ return resolved;
211
+ }
212
+ return process.env.DSH_HOME ?? join(process.cwd(), ".tests", "aw");
213
+ }
214
+ /**
215
+ * App-wide wiring the adapter owns (user 2026-09-15): the SDK has no
216
+ * default-model concept, so the bridge publishes the codex config.toml
217
+ * default into the world's `agentDefaultModel`. One-directional on purpose —
218
+ * the config is read-only for us (S7 single-direction valve), which makes the
219
+ * codex world's default sticky. Echo-guarded by the saved key.
220
+ *
221
+ * Why the write goes through `settings.replace` and not through
222
+ * `agentDefaultModel.saveSelection`: the harness service installs its section
223
+ * from an `inject(["settings"], …)` callback but then writes through
224
+ * `this.ctx.get("settings")?.replace(…)` on its UNSCOPED context, where
225
+ * `settings` is not injected — the optional chain swallows the lookup and the
226
+ * call is a silent no-op (the settings file is never touched, and the picker
227
+ * keeps showing the base default). Same defect and same fix as the OMP twin
228
+ * (`#saveDefaultSelection`): write the installed section directly through our
229
+ * own injected `settings` service, which the harness folds back via
230
+ * `installSection`, so every other consumer sees the new default.
231
+ */
232
+ #registerDefaultModel() {
233
+ this.runtime.ctx.inject(["agentDefaultModel", "settings"], (mCtx) => {
234
+ const service = mCtx.get("agentDefaultModel");
235
+ if (service === undefined)
236
+ return;
237
+ const catalog = readCodexModelCatalog(resolveCodexHome());
238
+ if (catalog.defaultModel === CODEX_DEFAULT_MODEL_PLACEHOLDER) {
239
+ trace("default model push skipped: catalog unreadable (placeholder)");
240
+ return;
241
+ }
242
+ const target = { provider: CODEX_PROVIDER_ID, model: catalog.defaultModel };
243
+ const targetKey = `${target.provider}/${target.model}`;
244
+ const current = service.currentSelection?.();
245
+ const currentKey = current === undefined ? undefined : `${current.provider}/${current.model}`;
246
+ if (currentKey === targetKey || currentKey === this.defaultModelKey)
247
+ return;
248
+ trace(`default model: ${currentKey ?? "none"} → ${targetKey}`);
249
+ const settings = mCtx.get("settings");
250
+ void Promise.resolve(settings?.replace?.("agent-default-model", target))
251
+ // Best-effort second path: harmless when upstream's no-op swallows it,
252
+ // and it keeps the in-memory selection fresh on builds where it works.
253
+ .then(() => service.saveSelection?.(target))
254
+ .then(() => {
255
+ this.defaultModelKey = targetKey;
256
+ this.runtime.ctx.logger.info(`codex-provider: default model ← codex config: ${targetKey}`);
257
+ })
258
+ .catch((error) => {
259
+ this.runtime.ctx.logger.warn(`codex-provider: default model save failed: ${String(error)}`);
260
+ });
261
+ });
262
+ }
263
+ #registerModelCatalog() {
264
+ const llm = this.runtime.ctx.get("llm");
265
+ if (llm === undefined)
266
+ return;
267
+ // The catalog is fail-soft (a `codex-default` placeholder when the home
268
+ // is unreadable), so the route always registers and the selector is
269
+ // never empty.
270
+ llm.registerAdapter([CODEX_PROVIDER_ID], new CodexLlmAdapter(resolveCodexHome()));
271
+ }
272
+ async createAgent(ownerCtx, options) {
273
+ const loopCtx = this.runtime.ctx;
274
+ const id = options.sessionId;
275
+ const meta = options.meta ?? {};
276
+ const cwd = meta.cwd;
277
+ // Fork (the apiproxy's `session.fork`) seeds a copied-turn prefix but
278
+ // Codex has no native fork: fail the fork cleanly at the factory boundary.
279
+ // `parentSession` in create metadata is set only by the fork path
280
+ // (subagents go through `ctx.subagents` instead).
281
+ if (meta.parentSession !== undefined) {
282
+ throw new Error(`cannot fork session "${meta.parentSession}" onto the Codex provider: Codex has no native session fork`);
283
+ }
284
+ // The DSH session log is this factory's write path (2026-09-16): without
285
+ // the upstream persistence service a created session would persist
286
+ // nothing, so require it up front.
287
+ const persistence = this.requirePersistence("create");
288
+ // Approval policy is pinned at Codex thread construction and the Dash
289
+ // session does not exist yet at this point (it is prepared + announced —
290
+ // which is what stamps `permission/preset` via pinInitialPermission —
291
+ // only inside setupAndPublish, AFTER this spawn), so the effective
292
+ // preset cannot come from session events: the env override
293
+ // (CODEX_APPROVAL_MODE, headless runs) wins, else the permission
294
+ // service's default, mapped onto the launch policy.
295
+ const preset = envApprovalMode() ?? defaultPermissionPreset(loopCtx);
296
+ const approvalPolicy = codexApprovalMode(preset);
297
+ const spawnCwd = validatedCwd(cwd) ?? process.cwd();
298
+ trace(`create id=${id} preset=${preset ?? "none"} approval=${approvalPolicy} spawnCwd=${spawnCwd}${envApprovalMode() === undefined ? "" : " (env override)"}`);
299
+ // Backend takes ZERO part in the UI's browser-local new-session draft
300
+ // beyond the (zero-IO) thread construction: no rollout, no session
301
+ // events — the session announces blank. The thread materializes on the
302
+ // first prompt.
303
+ const client = await CodexSdkClient.spawn(["--approval-mode", approvalPolicy], spawnCwd);
304
+ // Deterministic per-session route (user wiring directive 2026-09-15): the
305
+ // factory owns the model — the catalog's real default rides the first
306
+ // turn; the placeholder (unreadable catalog) is OMITTED so the codex
307
+ // config default applies.
308
+ const createCatalog = readCodexModelCatalog(resolveCodexHome());
309
+ if (createCatalog.defaultModel !== CODEX_DEFAULT_MODEL_PLACEHOLDER) {
310
+ void client.setModel(CODEX_PROVIDER_ID, createCatalog.defaultModel).catch(() => { });
311
+ }
312
+ const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
313
+ ...(options.seed === undefined ? {} : { seed: options.seed }),
314
+ ...(meta === undefined ? {} : { meta }),
315
+ ...(options.inheritedEventCount === undefined ? {} : { inheritedEventCount: options.inheritedEventCount }),
316
+ }));
317
+ let stored;
318
+ let handedOff = false;
319
+ try {
320
+ stored = await this.createStoredSession(persistence, preparation.session, options.signal);
321
+ // Identity record BEFORE publication: the DSH id → Codex thread pairing
322
+ // (threadId null until the first turn) is what resume resolves.
323
+ upsertSession(this.home, String(id), {
324
+ threadId: client.threadId,
325
+ cwd: spawnCwd,
326
+ createdAt: Date.now(),
327
+ preset: preset ?? null,
328
+ });
329
+ this.followThreadId(id, client);
330
+ handedOff = true;
331
+ 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));
332
+ }
333
+ catch (error) {
334
+ trace(`create ${id} THREW: ${String(error)}`);
335
+ client.close();
336
+ if (!handedOff)
337
+ await stored?.handle.close().catch(() => { });
338
+ throw error;
339
+ }
340
+ finally {
341
+ if (!handedOff)
342
+ preparation[Symbol.dispose]();
343
+ }
344
+ }
345
+ async resume(ownerCtx, options) {
346
+ const loopCtx = this.runtime.ctx;
347
+ const id = options.resumeSessionId;
348
+ // Identity (2026-09-16 "mapping only", reaffirmed 2026-09-18): the DSH
349
+ // session id resolves through the adapter-owned map — the thread id is
350
+ // recorded when Codex materializes it. The adapter NEVER scans the native
351
+ // ~/.codex session storage to recover pairings (2026-09-18 user ruling:
352
+ // the discovery tier violated the design — DSH log is the transcript
353
+ // authority; the native home contributes config/settings only). Every
354
+ // miss fails closed with a stable code (CodexResumeError).
355
+ const persistence = this.requirePersistence("resume");
356
+ const record = sessionRecord(this.home, String(id));
357
+ trace(`resume id=${id} record=${record === undefined ? "MISSING" : record.threadId ?? "NO-THREAD"}${record?.resumable === false ? " (marked not resumable)" : ""}`);
358
+ if (record === undefined) {
359
+ throw new CodexResumeError("SESSION_MAP_MISS", `cannot resume session "${id}": no Codex thread is recorded for this Dash session id`);
360
+ }
361
+ if (record.threadId === null) {
362
+ // C2: never auto-start a fresh thread — the session's history is in the
363
+ // (missing) native thread, not in a blank one.
364
+ throw new CodexResumeError("NATIVE_THREAD_NEVER_STARTED", `cannot resume session "${id}": its Codex thread was never started (no thread id recorded)`);
365
+ }
366
+ if (record.resumable === false) {
367
+ // A previous native resume was rejected at turn time (plan RC-4): fail
368
+ // fast with the clear refusal instead of burning another turn.
369
+ throw new CodexResumeError("NATIVE_REJECTED", `cannot resume session "${id}": its Codex thread ${record.threadId} was rejected by the native runtime in an earlier attempt`);
370
+ }
371
+ // Pre-validation (plan RC-4): the rollout must exist in the CURRENT codex
372
+ // home BEFORE spawning — covers rollouts stranded under the abandoned
373
+ // `<worldHome>/.codex` home (never migrated) and pruned rollouts alike.
374
+ // Without this gate the failure would surface only at the first turn
375
+ // (resumeThread is zero-IO).
376
+ if (readRolloutHead(resolveCodexHome(), record.threadId) === undefined) {
377
+ throw new CodexResumeError("ROLLOUT_MISSING", `cannot resume session "${id}": no rollout for Codex thread ${record.threadId} exists in the current Codex home (${resolveCodexHome()})`);
378
+ }
379
+ // Spawn cwd comes from the recorded pairing — re-realpath and re-verify so
380
+ // a moved/removed directory cannot aim a resume outside a live path.
381
+ const spawnCwd = validatedCwd(record.cwd);
382
+ if (spawnCwd === undefined) {
383
+ throw new Error(`cannot resume session "${id}": its recorded working directory no longer exists`);
384
+ }
385
+ // Approval policy for the re-attached thread: launch-only, so it is
386
+ // decided BEFORE the spawn from the recorded preset, else the persisted
387
+ // session log, else the permission service's default. CODEX_APPROVAL_MODE
388
+ // (headless runs) overrides; no preset → never.
389
+ const envMode = envApprovalMode();
390
+ const preset = envMode !== undefined
391
+ ? envMode
392
+ : record.preset ??
393
+ presetFromEvents(await readStoredEvents(persistence, id, options.signal)) ??
394
+ defaultPermissionPreset(loopCtx);
395
+ const approvalPolicy = codexApprovalMode(preset);
396
+ trace(`resume id=${id} spawnCwd=${spawnCwd} preset=${preset ?? "none"} approval=${approvalPolicy}`);
397
+ // Re-attach to the Codex thread by its id (Codex owns the live agent
398
+ // context and keeps generating it from here on; the model context is
399
+ // restored by Codex itself from its rollout).
400
+ const client = await CodexSdkClient.spawn(["--approval-mode", approvalPolicy, "--resume-thread", record.threadId], spawnCwd);
401
+ let handle;
402
+ let stored;
403
+ let preparation;
404
+ let handedOff = false;
405
+ try {
406
+ // Taking write ownership FIRST excludes a concurrent resume of the same
407
+ // id (a live agent's handle holds the claim in this process).
408
+ handle = await persistence.open(id, "write", options.signal === undefined ? {} : { signal: options.signal });
409
+ // Crash repair belongs to the agent layer: an interrupted final turn
410
+ // receives synthetic closers appended through the same handle.
411
+ const cold = await handle.read(0, undefined, options.signal === undefined ? {} : { signal: options.signal });
412
+ const closers = interruptedTurnClosers(cold.events);
413
+ if (closers.length > 0)
414
+ await handle.append(closers);
415
+ preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
416
+ seed: [...cold.events, ...closers],
417
+ meta: structuredClone(handle.header),
418
+ inheritedEventCount: handle.inheritedEventCount,
419
+ eventState: cold.eventState,
420
+ }));
421
+ stored = { handle, storedCount: cold.events.length + closers.length };
422
+ trace(`resume id=${id} seeded ${cold.events.length} events (+${closers.length} closers)`);
423
+ handle = undefined; // ownership passes to setupAndPublish
424
+ this.followThreadId(id, client);
425
+ handedOff = true;
426
+ 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));
427
+ }
428
+ catch (error) {
429
+ trace(`resume ${id} THREW: ${String(error)} | stack=${error instanceof Error ? error.stack?.slice(0, 400) : "n/a"}`);
430
+ client.close();
431
+ if (!handedOff)
432
+ await handle?.close().catch(() => { });
433
+ throw error;
434
+ }
435
+ finally {
436
+ if (!handedOff)
437
+ preparation?.[Symbol.dispose]();
438
+ }
439
+ }
440
+ /**
441
+ * Watch the client for its first materialized thread id and persist the
442
+ * Dash↔thread pairing once (the record is the resume authority; the
443
+ * create-time entry starts with `threadId: null`).
444
+ * Arrow FIELD (not a `#` method and not a true-private field): the provider
445
+ * is exposed through a Cordis tracing proxy, and hard-private members fail
446
+ * their brand check on the proxy receiver (the same reason setupAndPublish
447
+ * is module-level). A persistence failure here is logged, never thrown into
448
+ * the client's event stream.
449
+ */
450
+ followThreadId = (id, client) => {
451
+ // RE-ARMING observer (plan C6): newSession() swaps threads mid-session, so
452
+ // the pairing must be re-upserted whenever the client's thread id CHANGES,
453
+ // not just once. Unsubscribing after the first capture would leave the map
454
+ // pointing at the discarded thread, and the next resume would re-attach to
455
+ // a thread the session no longer runs on.
456
+ let lastRecorded = client.threadId;
457
+ const observe = (threadId) => {
458
+ lastRecorded = threadId;
459
+ try {
460
+ upsertSession(this.home, String(id), { threadId });
461
+ trace(`thread id recorded for ${String(id)}: ${threadId}`);
462
+ }
463
+ catch (error) {
464
+ this.runtime.ctx.logger.warn(`codex-provider: failed to persist the Codex thread id for ${String(id)}: ${String(error)}`);
465
+ }
466
+ };
467
+ if (lastRecorded !== null)
468
+ observe(lastRecorded);
469
+ client.on(() => {
470
+ const observed = client.threadId;
471
+ if (observed === null || observed === lastRecorded)
472
+ return;
473
+ observe(observed);
474
+ });
475
+ };
476
+ }
477
+ /**
478
+ * Durable flush of the session's pre-publication suffix. Constructor seed
479
+ * markers and setup-window events never re-emit through `session/event`, so
480
+ * publication must push them through the handle before live events start
481
+ * routing into it. Advances by what was stored (an event appended during the
482
+ * await stays unstored for the next flush).
483
+ */
484
+ async function appendUnstoredSuffix(stored, session) {
485
+ const suffix = session.snapshotEvents(SessionLogOffset(stored.storedCount));
486
+ if (suffix.length > 0)
487
+ await stored.handle.append(suffix);
488
+ stored.storedCount += suffix.length;
489
+ }
490
+ /**
491
+ * Shared creation/resume transaction: build the agent over the prepared
492
+ * session, run unpublished setup, flush the unstored suffix, and publish both
493
+ * in order; the owned storage handle is drained (closed) on teardown. Kept as
494
+ * a module-level function (not a private method) because Cordis exposes the
495
+ * provider through a tracing proxy, which breaks hard-private (`#`) receivers.
496
+ */
497
+ async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, session, client, parentAgent, source, opts, stored, runtimeInfo) {
498
+ let detachSession;
499
+ let detachAgent;
500
+ let agent;
501
+ // Late-bound teardown the agent triggers itself after its idle window
502
+ // expires (see CodexAgent's idle exit) — defined only after publication.
503
+ let idleExit;
504
+ try {
505
+ // Build the agent shim over the session and the live client.
506
+ agent = new CodexAgent(loopCtx, id, agentOptions, session, client, () => idleExit?.(), runtimeInfo);
507
+ // Pin THIS session to the codex route. The global default-model setting is
508
+ // a best-effort push (the settings service may flush late, and a shared
509
+ // test home can hold another line's default), so the factory must not let
510
+ // the first prompt depend on it: `selectForNextRequest` is the same
511
+ // per-session mechanism `session/selectModel` uses (appends the log-only
512
+ // `model/selection` and caches the next request's route).
513
+ try {
514
+ const route = runtimeInfo?.routeContext(agentOptions.model);
515
+ if (route !== undefined) {
516
+ const selection = { provider: route.provider, model: route.model };
517
+ // Mirrors the upstream session-controller's `selectForNextRequest`:
518
+ // the durable log-only `model/selection` plus an agent-scoped
519
+ // `ModelSelectionRef` (same shape the controller installs) that the
520
+ // next prompt's route check reads. Synchronous on purpose — the first
521
+ // prompt must not depend on the global default's late flush.
522
+ let picked = selection;
523
+ installModelSelection(agent.ctx, {
524
+ get current() { return picked; },
525
+ set current(next) { picked = next; },
526
+ assembled: undefined,
527
+ });
528
+ // `model/selection` is contributed to `SessionEventMap` by the upstream
529
+ // session-controller package (declaration merging); this adapter does
530
+ // not depend on it, so the append goes through a narrow local cast.
531
+ session.append("model/selection", selection);
532
+ trace(`session route pinned: ${route.provider}/${route.model}`);
533
+ }
534
+ }
535
+ catch (error) {
536
+ // Fail-soft: the boot-time default-model push remains the fallback.
537
+ trace(`session route pin failed: ${String(error)}`);
538
+ }
539
+ // Session identity (the preset stamp) and the Codex system prompt commit
540
+ // on the first turn via CodexAgent's #bootstrapSessionIdentity /
541
+ // #emitSystemMessage: a freshly created session announces completely
542
+ // blank, so the backend takes zero part in the UI's new-session draft.
543
+ // Composition-only setup on the unpublished agent scope.
544
+ const commit = await setup?.(agent.ctx, agent);
545
+ // Publish: flush the unstored suffix (seed + setup-window events) through
546
+ // the owned handle, then enter both session and agent, announce in order,
547
+ // and signal session-start. The commit runs immediately before
548
+ // publication.
549
+ commit?.commit();
550
+ await appendUnstoredSuffix(stored, session);
551
+ if (opts.enterSession) {
552
+ detachSession = agent.ctx.sessions.enter(session);
553
+ agent.ctx.sessions.announce(session);
554
+ }
555
+ detachAgent = loopCtx.agents.enter(agent, parentAgent);
556
+ loopCtx.agents.announce(agent);
557
+ emitAgentEvent(loopCtx, agent, "agent/session-start", { source });
558
+ let disposed = false;
559
+ let unfollowOwner;
560
+ const dispose = async () => {
561
+ if (disposed)
562
+ return;
563
+ disposed = true;
564
+ trace(`dispose() called for agent ${id} (source=${source})`);
565
+ unfollowOwner?.();
566
+ await agent?.dispose();
567
+ detachAgent?.();
568
+ detachSession?.();
569
+ // Drain queued appends; a durability failure surfaces to the caller
570
+ // (the owner-unload and idle-exit paths log it).
571
+ await stored.handle.close();
572
+ };
573
+ idleExit = () => {
574
+ void dispose().catch((error) => {
575
+ loopCtx.logger.warn(`codex-provider: teardown of ${id} failed: ${String(error)}`);
576
+ });
577
+ };
578
+ // Follow the owner: a caller-fiber unload tears this agent down.
579
+ unfollowOwner = ownerCtx.effect(() => () => {
580
+ void dispose().catch((error) => {
581
+ loopCtx.logger.warn(`codex-provider: owner-unload teardown of ${id} failed: ${String(error)}`);
582
+ });
583
+ }, `codexProvider.lifecycle(${id})`);
584
+ return { agent, dispose };
585
+ }
586
+ catch (error) {
587
+ // Unwind the half-published transaction. A failure between `enter` and
588
+ // the announcements (e.g. a persistence listener rejecting the session)
589
+ // must not leave a live-but-dead entry the API resolver would serve.
590
+ trace(`setupAndPublish ${id} FAILED: ${String(error)}`);
591
+ detachAgent?.();
592
+ detachSession?.();
593
+ void agent?.dispose().catch(() => { });
594
+ client.close();
595
+ throw error;
596
+ }
597
+ finally {
598
+ // Release the preparation's per-id reservation on every path (mirrors
599
+ // the reference loop's unconditional dispose): the session it seeded is
600
+ // already published above, so a same-process re-resume of this id can
601
+ // prepare again instead of colliding with a leaked reservation.
602
+ opts.preparation?.[Symbol.dispose]();
603
+ }
604
+ }
605
+ export default CodexProvider;