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,65 @@
1
+ /**
2
+ * Codex runtime app home bootstrap — idempotently creates the codex home and
3
+ * seeds it with the Codex CLI's own config set (config.toml / model catalog /
4
+ * auth) copied once from the native `~/.codex`.
5
+ *
6
+ * Why this is required, not cosmetic: a Codex home without `config.toml` makes
7
+ * the CLI fall back to its built-in OpenAI provider, so every turn dies on
8
+ * `wss://api.openai.com/v1/responses` with a region block (403 "Country,
9
+ * region, or territory not supported"). The native home is where the user's
10
+ * provider wiring lives (e.g. a local proxy: `model_provider = "custom"`,
11
+ * `base_url = "http://127.0.0.1:15721/v1"`, `model = "glm-5.3-flash"`), and the
12
+ * world deliberately runs with its own CODEX_HOME — so the config has to travel.
13
+ *
14
+ * Same manners as the OMP twin: never overwrites an existing file (a
15
+ * provisioned app home wins), skips missing sources, and is fail-soft — the app
16
+ * home is derived state, never a boot gate.
17
+ */
18
+ import { copyFileSync, existsSync, mkdirSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { join } from "node:path";
21
+ import { resolveCodexHome } from "./codex-store.js";
22
+ /**
23
+ * Config files seeded into the codex home. `config.toml` names the provider and
24
+ * model, `cc-switch-model-catalog.json` is what its `model_catalog_json` key
25
+ * points at (a relative path, hence the same directory), and `auth.json` is the
26
+ * CLI's own credential record.
27
+ */
28
+ export const CODEX_HOME_CONFIG_FILES = ["config.toml", "cc-switch-model-catalog.json", "auth.json"];
29
+ /** Native codex home — the copy source (`AW_CODEX_SEED_HOME` overrides). */
30
+ export function defaultCodexSeedHome() {
31
+ const configured = process.env.AW_CODEX_SEED_HOME;
32
+ return configured !== undefined && configured !== "" ? configured : join(homedir(), ".codex");
33
+ }
34
+ /**
35
+ * Ensure the codex home exists and carries the CLI config set.
36
+ *
37
+ * @param options - target/source/files overrides.
38
+ * @returns the resolved codex home path (always, even on a failed seed).
39
+ */
40
+ export function ensureCodexAppHome(options = {}) {
41
+ const codexHome = options.codexHome ?? resolveCodexHome();
42
+ const source = options.seedHome ?? defaultCodexSeedHome();
43
+ const files = options.files ?? CODEX_HOME_CONFIG_FILES;
44
+ const seeded = [];
45
+ try {
46
+ mkdirSync(codexHome, { recursive: true });
47
+ for (const name of files) {
48
+ const dest = join(codexHome, name);
49
+ if (existsSync(dest))
50
+ continue;
51
+ const src = join(source, name);
52
+ if (!existsSync(src))
53
+ continue;
54
+ copyFileSync(src, dest);
55
+ seeded.push(name);
56
+ }
57
+ if (seeded.length > 0) {
58
+ options.warn?.(`codex-provider: app home ${codexHome} seeded from ${source} (${seeded.join(", ")})`);
59
+ }
60
+ }
61
+ catch (error) {
62
+ options.warn?.(`codex-provider: app-home bootstrap failed (fail-soft): ${String(error)}`);
63
+ }
64
+ return codexHome;
65
+ }
@@ -0,0 +1,469 @@
1
+ /**
2
+ * CodexSdkClient — the SDK-line client seam (AW-E Task 3).
3
+ *
4
+ * Public surface mirrors `OmpSdkClient` (agent-omp/src/sdk-client.ts) exactly —
5
+ * `spawn` / `on` / `onFailure` / `sendRaw` / `send` / `getState` / `getMessages`
6
+ * / `getSessionStats` / `getSubagents` / `prompt` / `followUp` / `steer` /
7
+ * `setModel` / `abort` / `newSession` / `close` / `ensureStarted` / `spawned` —
8
+ * so the Task 7 agent.ts/index.ts port compiles against this client the way it
9
+ * compiles against the omp one. Instead of one `omp --mode rpc` sidecar per
10
+ * session, ALL clients multiplex ONE module-level refcounted `Codex` instance
11
+ * (same acquire()/release() pattern as the omp sidecar; the SDK spawns its own
12
+ * per-turn codex process, so dropping the shared instance needs no stop call).
13
+ *
14
+ * Codex home: the shared instance is constructed with `{ env: { ...process.env } }`
15
+ * (SDK env is REPLACE semantics — Task 1 spike fact, so the spread is
16
+ * mandatory) and carries NO `CODEX_HOME`: the CLI resolves its own native
17
+ * `~/.codex` (user ruling 2026-09-17 — never redirect the native app home at
18
+ * spawn; tests redirect via `process.env.CODEX_HOME`). The SDK constructor
19
+ * itself is injectable via {@link setCodexFactory}.
20
+ *
21
+ * Wire events flow through `projectThreadEvent` (codex-events.ts) before
22
+ * reaching listeners, so Task 7's agent.ts switch is unchanged from omp.
23
+ */
24
+ import { Codex } from "@openai/codex-sdk";
25
+ import { projectThreadEvent } from "./codex-events.js";
26
+ let codexFactory = (options) => new Codex(options);
27
+ /** Replace the SDK constructor (tests inject a fake Codex). */
28
+ export function setCodexFactory(factory) {
29
+ codexFactory = factory;
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // Shared Codex instance (module-level singleton, refcounted — omp sidecar pattern)
33
+ // ---------------------------------------------------------------------------
34
+ let sharedCodex = null;
35
+ let refcount = 0;
36
+ function acquire() {
37
+ if (sharedCodex === null) {
38
+ sharedCodex = codexFactory({ env: { ...process.env } });
39
+ }
40
+ refcount += 1;
41
+ return sharedCodex;
42
+ }
43
+ function release() {
44
+ refcount = Math.max(0, refcount - 1);
45
+ if (refcount === 0)
46
+ sharedCodex = null;
47
+ }
48
+ /** Test/observability hook: current shared-instance refcount (0 = none alive). */
49
+ export function codexClientRefCount() {
50
+ return refcount;
51
+ }
52
+ function ok(command) {
53
+ return { type: "response", command, success: true };
54
+ }
55
+ function asRecord(value) {
56
+ if (value === null || typeof value !== "object" || Array.isArray(value))
57
+ return null;
58
+ return value;
59
+ }
60
+ /**
61
+ * Parse the omp-style spawn args. `--approval-mode <v>` is accepted and
62
+ * applied verbatim as the thread's launch `approvalPolicy` — the 3-preset
63
+ * mapping resolves the VALUE provider-side (Task 7's permission.ts) and the
64
+ * thread carries it on every construction (start, resume, setModel rebuild).
65
+ * `--resume-thread <id>` constructs the client on `codex.resumeThread(id, …)`
66
+ * — the thread-identity resume (plan Task 7), strictly distinct from the
67
+ * rejected omp-style `--resume <file>`, which keeps failing loudly exactly
68
+ * like unknown args.
69
+ */
70
+ function parseSpawnArgs(args) {
71
+ const out = {};
72
+ for (let i = 0; i < args.length; i++) {
73
+ if (args[i] === "--approval-mode" && args[i + 1] !== undefined) {
74
+ out.approvalMode = args[++i];
75
+ }
76
+ else if (args[i] === "--resume-thread" && args[i + 1] !== undefined) {
77
+ out.resumeThreadId = args[++i];
78
+ }
79
+ else {
80
+ throw new Error(`CodexSdkClient: unsupported spawn arg "${String(args[i])}" (SDK line)`);
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+ // ---------------------------------------------------------------------------
86
+ export class CodexSdkClient {
87
+ #codex;
88
+ #listeners = new Set();
89
+ #failureListeners = new Set();
90
+ #threadOptions;
91
+ #messages = [];
92
+ #queue = [];
93
+ #approvalMode;
94
+ #thread;
95
+ #closed = false;
96
+ #failure = null;
97
+ #inFlight = false;
98
+ #currentAbort = null;
99
+ #currentRun = null;
100
+ #pendingModel = null;
101
+ #appliedModel = null;
102
+ #lastUsage = null;
103
+ /** Bumped by newSession()/close() to orphan runs started on an earlier thread. */
104
+ #sessionEpoch = 0;
105
+ constructor(codex, cwd, approvalMode, resumeThreadId) {
106
+ this.#codex = codex;
107
+ this.#approvalMode = approvalMode;
108
+ // skipGitRepoCheck: adapters spawn from arbitrary test/worktree cwds (Task 1 spike precedent).
109
+ // Launch-only approval mapping (Task 7): present only when the provider
110
+ // resolved a preset, so plain spawns keep the bare ThreadOptions shape.
111
+ this.#threadOptions = {
112
+ workingDirectory: cwd,
113
+ skipGitRepoCheck: true,
114
+ ...(approvalMode === undefined ? {} : { approvalPolicy: approvalMode }),
115
+ };
116
+ // `--resume-thread <id>` re-attaches to the existing Codex thread (the
117
+ // model context lives in Codex's rollout); plain spawn starts a fresh one.
118
+ this.#thread =
119
+ resumeThreadId === undefined ? codex.startThread(this.#threadOptions) : codex.resumeThread(resumeThreadId, this.#threadOptions);
120
+ }
121
+ /**
122
+ * Start a session on the shared Codex instance and resolve once ready.
123
+ * Mirrors `OmpSdkClient.spawn(args, cwd)`.
124
+ */
125
+ static async spawn(args = [], cwd) {
126
+ const { approvalMode, resumeThreadId } = parseSpawnArgs(args);
127
+ const codex = acquire();
128
+ try {
129
+ return new CodexSdkClient(codex, cwd, approvalMode, resumeThreadId);
130
+ }
131
+ catch (error) {
132
+ release();
133
+ throw error;
134
+ }
135
+ }
136
+ /** Register a wire event listener; returns the unsubscriber. */
137
+ on(listener) {
138
+ this.#listeners.add(listener);
139
+ return () => {
140
+ this.#listeners.delete(listener);
141
+ };
142
+ }
143
+ /** Register a failure listener; returns the unsubscriber. */
144
+ onFailure(listener) {
145
+ this.#failureListeners.add(listener);
146
+ return () => {
147
+ this.#failureListeners.delete(listener);
148
+ };
149
+ }
150
+ /**
151
+ * Raw-command escape hatch. The SDK line has no stdio command channel, so
152
+ * RPC-specific records (e.g. `extension_ui_response` approval answers) are
153
+ * accepted and logged under CODEX_TRACE=1 — approval flows route through
154
+ * SDK-side approval work (Task 7 parity item).
155
+ */
156
+ sendRaw(command) {
157
+ if (this.#failure !== null || this.#closed)
158
+ return;
159
+ if (process.env.CODEX_TRACE === "1") {
160
+ process.stderr.write(`[codex-sdk] sendRaw (no-op on SDK line): ${JSON.stringify(command).slice(0, 160)}\n`);
161
+ }
162
+ }
163
+ /** Generic command sender mapping the omp command table onto client methods. */
164
+ async send(command) {
165
+ switch (command.type) {
166
+ case "prompt":
167
+ await this.prompt(String(command.message));
168
+ return ok("prompt");
169
+ case "steer":
170
+ await this.steer(String(command.message));
171
+ return ok("steer");
172
+ case "follow_up":
173
+ await this.followUp(String(command.message));
174
+ return ok("follow_up");
175
+ case "abort":
176
+ await this.abort();
177
+ return ok("abort");
178
+ case "set_model":
179
+ await this.setModel(String(command.provider), String(command.modelId));
180
+ return ok("set_model");
181
+ case "new_session":
182
+ await this.newSession();
183
+ return ok("new_session");
184
+ case "get_state":
185
+ return { type: "response", command: "get_state", success: true, data: await this.getState() };
186
+ case "get_messages":
187
+ return { type: "response", command: "get_messages", success: true, data: { messages: await this.getMessages() } };
188
+ default:
189
+ throw new Error(`CodexSdkClient.send: unsupported command "${String(command.type)}" (SDK line)`);
190
+ }
191
+ }
192
+ /** Query session state; returns the `get_state`-shaped payload. */
193
+ async getState() {
194
+ this.#ensureUsable();
195
+ return { isStreaming: this.#inFlight };
196
+ }
197
+ /**
198
+ * Best-effort conversation mirror accumulated from the wire (user echoes at
199
+ * acceptance, assistant message_end, toolResult). The authoritative
200
+ * transcript comes from the rollout scan (codex-store.ts); reasoning-only
201
+ * messages are not mirrored. Fail-soft: [] when nothing accumulated.
202
+ */
203
+ async getMessages() {
204
+ try {
205
+ return [...this.#messages];
206
+ }
207
+ catch {
208
+ return [];
209
+ }
210
+ }
211
+ /** Live token accounting from the last turn_end usage (fail-soft {}). */
212
+ async getSessionStats() {
213
+ try {
214
+ const usage = this.#lastUsage;
215
+ if (usage === null)
216
+ return {};
217
+ const tokens = {};
218
+ const map = [
219
+ ["input", "input_tokens"],
220
+ ["output", "output_tokens"],
221
+ ["cacheRead", "cached_input_tokens"],
222
+ ["cacheWrite", "cache_write_input_tokens"],
223
+ ];
224
+ for (const [wireKey, codexKey] of map) {
225
+ const value = usage[codexKey];
226
+ if (typeof value === "number")
227
+ tokens[wireKey] = value;
228
+ }
229
+ return { tokens };
230
+ }
231
+ catch {
232
+ return {};
233
+ }
234
+ }
235
+ /** Codex has no subagent registry (fail-soft constant, omp surface parity). */
236
+ async getSubagents() {
237
+ return [];
238
+ }
239
+ /** Start a turn with a user prompt (fire-and-forget acceptance). */
240
+ async prompt(message) {
241
+ this.#ensureUsable();
242
+ this.#enqueueOrRun(message);
243
+ return ok("prompt");
244
+ }
245
+ /** Queue a follow-up on a busy session (runs immediately when idle). */
246
+ async followUp(message) {
247
+ this.#ensureUsable();
248
+ this.#enqueueOrRun(message);
249
+ return ok("follow_up");
250
+ }
251
+ /**
252
+ * Steer an in-flight turn — 2026-09-10 gap table: the SDK line has no steer,
253
+ * so this is the queued-followUp approximation (plan Global Constraints:
254
+ * "无 steer(→ followUp 队列近似)"); while idle it degrades to a plain
255
+ * prompt, while in flight the message runs right after the current turn
256
+ * settles.
257
+ */
258
+ async steer(message) {
259
+ this.#ensureUsable();
260
+ if (this.#inFlight && process.env.CODEX_TRACE === "1") {
261
+ process.stderr.write("[codex-sdk] steer approximated as queued followUp (SDK gap)\n");
262
+ }
263
+ this.#enqueueOrRun(message);
264
+ return ok("steer");
265
+ }
266
+ /**
267
+ * Select the active model. SDK has no live setModel (2026-09-10 gap table):
268
+ * the id is stored and applied as the `model` ThreadOption on the NEXT run —
269
+ * via resumeThread when the thread already has an id, so conversation
270
+ * context survives the model switch. `provider` is accepted for surface
271
+ * parity; codex resolves models from its own catalog.
272
+ */
273
+ async setModel(provider, modelId) {
274
+ this.#ensureUsable();
275
+ void provider;
276
+ this.#pendingModel = modelId;
277
+ return ok("set_model");
278
+ }
279
+ /** Abort the current streaming run (AbortController per run, turn-level signal). */
280
+ async abort() {
281
+ this.#currentAbort?.abort();
282
+ const run = this.#currentRun;
283
+ if (run !== null)
284
+ await run; // settle first so state is deterministically idle
285
+ return ok("abort");
286
+ }
287
+ /** Discard the current thread and message mirror; start fresh. */
288
+ async newSession() {
289
+ this.#ensureUsable();
290
+ // Invalidate THIS run's queue-drain before aborting it: its finally must
291
+ // not start a queued message on the thread we are about to discard.
292
+ this.#sessionEpoch += 1;
293
+ this.#currentAbort?.abort();
294
+ const run = this.#currentRun;
295
+ if (run !== null)
296
+ await run;
297
+ this.#queue.length = 0;
298
+ this.#messages.length = 0;
299
+ this.#thread = this.#codex.startThread(this.#threadOptions);
300
+ this.#appliedModel = null; // a pending model still applies at the next run
301
+ return ok("new_session");
302
+ }
303
+ /** Lazy-surface parity: a real client is born live (see OmpAgentRpc.spawned). */
304
+ ensureStarted() {
305
+ return Promise.resolve();
306
+ }
307
+ /**
308
+ * The current Codex thread id, exactly as the SDK thread exposes it (null
309
+ * until the thread materializes one). The provider keeps the
310
+ * threadId ↔ Dash-session-id index from this (Task 7).
311
+ */
312
+ get threadId() {
313
+ return this.#thread.id;
314
+ }
315
+ /** Lazy-surface parity: a real client is born live (see OmpAgentRpc.spawned). */
316
+ get spawned() {
317
+ return true;
318
+ }
319
+ /** Release the shared-instance refcount and fail all future operations. */
320
+ close() {
321
+ if (this.#closed)
322
+ return;
323
+ this.#closed = true;
324
+ this.#sessionEpoch += 1; // a settled run must not drain the queue after close
325
+ this.#currentAbort?.abort();
326
+ release();
327
+ this.#fail(new Error("codex sdk client closed"));
328
+ }
329
+ // -------------------------------------------------------------------------
330
+ #ensureUsable() {
331
+ if (this.#failure !== null)
332
+ throw this.#failure;
333
+ if (this.#closed)
334
+ throw new Error("codex sdk client is closed");
335
+ }
336
+ /** Queue behind the in-flight turn, or start a run now. */
337
+ #enqueueOrRun(message) {
338
+ if (this.#inFlight) {
339
+ this.#queue.push(message);
340
+ return;
341
+ }
342
+ this.#startRun(message);
343
+ }
344
+ #startRun(message) {
345
+ this.#messages.push({ role: "user", content: [{ type: "text", text: message }] });
346
+ const run = this.#runTurn(message);
347
+ this.#currentRun = run;
348
+ }
349
+ /**
350
+ * One streamed turn: runStreamed on the thread, pump raw SDK events through
351
+ * projectThreadEvent, mirror the conversation, settle to agent_end. Mirrors
352
+ * the real SDK abort contract: an aborted turn just ENDS the stream (Task 1
353
+ * spike — no throw guarantee), so it settles like a normal end; thrown errors
354
+ * and the fatal top-level `error` event go to onFailure.
355
+ */
356
+ async #runTurn(message) {
357
+ // Session epoch: bumped by newSession()/close() so a run that started on
358
+ // the previous thread never drains the queue onto the new one.
359
+ const epoch = this.#sessionEpoch;
360
+ this.#inFlight = true;
361
+ const controller = new AbortController();
362
+ this.#currentAbort = controller;
363
+ try {
364
+ this.#applyPendingModel();
365
+ const streamed = await this.#thread.runStreamed(message, { signal: controller.signal });
366
+ for await (const raw of streamed.events) {
367
+ const record = asRecord(raw);
368
+ if (record !== null && record.type === "error") {
369
+ // Fatal stream error: SDK contract surfaces it here, not as a throw.
370
+ this.#fail(new Error(typeof record.message === "string" ? record.message : "codex stream error"));
371
+ return;
372
+ }
373
+ const wire = projectThreadEvent(raw);
374
+ if (wire !== null) {
375
+ const events = Array.isArray(wire) ? wire : [wire];
376
+ for (const event of events) {
377
+ this.#emit(event);
378
+ this.#mirror(event);
379
+ }
380
+ }
381
+ }
382
+ this.#emit({ type: "agent_end" });
383
+ }
384
+ catch (error) {
385
+ if (controller.signal.aborted) {
386
+ // Real SDK: the stream ends behind an abort — settle, stay idle.
387
+ this.#emit({ type: "agent_end" });
388
+ }
389
+ else {
390
+ this.#fail(error instanceof Error ? error : new Error(String(error)));
391
+ }
392
+ }
393
+ finally {
394
+ this.#inFlight = false;
395
+ this.#currentAbort = null;
396
+ this.#currentRun = null;
397
+ const next = this.#queue.shift();
398
+ if (next !== undefined && epoch === this.#sessionEpoch && this.#failure === null && !this.#closed) {
399
+ this.#startRun(next);
400
+ }
401
+ }
402
+ }
403
+ /** Rebuild the thread with the pending model when one is pending (next run). */
404
+ #applyPendingModel() {
405
+ if (this.#pendingModel === this.#appliedModel)
406
+ return;
407
+ const model = this.#pendingModel;
408
+ const options = model === null ? { ...this.#threadOptions } : { ...this.#threadOptions, model };
409
+ const currentId = this.#thread.id;
410
+ this.#thread =
411
+ currentId !== null ? this.#codex.resumeThread(currentId, options) : this.#codex.startThread(options);
412
+ this.#appliedModel = model;
413
+ }
414
+ /** Mirror wire events into the getMessages accumulator (best effort). */
415
+ #mirror(wire) {
416
+ if (wire.type === "message_end") {
417
+ const message = asRecord(wire.message);
418
+ if (message !== null && message.role === "assistant") {
419
+ // Reasoning-only messages are not mirrored (see getMessages doc).
420
+ const blocks = Array.isArray(message.content) ? message.content : [];
421
+ const reasoningOnly = blocks.length > 0 && blocks.every((block) => block.type === "thinking");
422
+ if (!reasoningOnly)
423
+ this.#messages.push(message);
424
+ }
425
+ return;
426
+ }
427
+ if (wire.type === "tool_execution_end") {
428
+ const result = asRecord(wire.result);
429
+ this.#messages.push({
430
+ role: "toolResult",
431
+ toolCallId: wire.toolCallId,
432
+ content: Array.isArray(result?.content) ? result.content : [],
433
+ isError: wire.isError === true,
434
+ });
435
+ return;
436
+ }
437
+ if (wire.type === "turn_end") {
438
+ const data = asRecord(wire.data);
439
+ const usage = asRecord(data?.usage);
440
+ if (usage !== null)
441
+ this.#lastUsage = usage;
442
+ }
443
+ }
444
+ #emit(event) {
445
+ if (this.#closed)
446
+ return;
447
+ for (const listener of [...this.#listeners]) {
448
+ try {
449
+ listener(event);
450
+ }
451
+ catch (error) {
452
+ process.stderr.write(`codex sdk listener error: ${String(error)}\n`);
453
+ }
454
+ }
455
+ }
456
+ #fail(error) {
457
+ if (this.#failure !== null)
458
+ return;
459
+ this.#failure = error;
460
+ for (const listener of [...this.#failureListeners]) {
461
+ try {
462
+ listener(error);
463
+ }
464
+ catch (listenerError) {
465
+ process.stderr.write(`codex sdk failure listener error: ${String(listenerError)}\n`);
466
+ }
467
+ }
468
+ }
469
+ }