@khorsheed/dsh-local-agent-codex 0.1.0-rc.6

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.
package/lib/index.js ADDED
@@ -0,0 +1,3550 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { LIVE_FLUSH_INTERVAL_MS, LiveFlush, LiveStreamPublisher, ModelDirectoryCache, assertResumeCwdUnchanged, assertResumeScopeUnchanged, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, extendModelDirectory, modelDirectoryContextKey, persistChildSession, probeCliVersion, resolveChildCwd, resolveRoundModel, settingsFace, subagentDelegationLabel, vol } from "@khorsheed/dsh-local-agent";
3
+ import { MEMBER_BRIDGE_SOCKET_ENV, MEMBER_BRIDGE_TOKEN_ENV, endpointHost } from "@khorsheed/dsh-local-agent/types";
4
+ import { randomUUID } from "node:crypto";
5
+ import { createAssistantMessage, createToolResultMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
6
+ import { SessionId } from "@deepseek-ai/dsh-session";
7
+ import { NO_START_CAPABILITIES, settleRunResult, subprocessRunHandle } from "@deepseek-ai/dsh-subagent";
8
+ import { StringDecoder } from "node:string_decoder";
9
+ import { open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
10
+ import { dirname, join, resolve } from "node:path";
11
+ //#region lib/types/records.js
12
+ /**
13
+ * Codex session records adapter: scans the scoped home's `sessions/`
14
+ * rollout files (one JSONL per session under `sessions/YYYY/MM/DD/`).
15
+ * The LISTING reads only the head of each file — the first `session_meta`
16
+ * line carries the id, cwd, and started-at timestamp it needs. The READ-BACK
17
+ * path ({@link codexRolloutRoundFacts}) instead locates one run's file (by
18
+ * thread id, working directory, or a time window) and reads that round's
19
+ * usage, model, and codex build out of it — the facts the exec `--json` wire
20
+ * either omits entirely (the model) or drops on a non-completed round (the
21
+ * usage). Files are treated as append-only with torn-tail tolerance, and
22
+ * contents are always untrusted input.
23
+ * @module @khorsheed/dsh-local-agent-codex/records
24
+ */
25
+ /** Rollout files live under this directory inside the scoped home. */
26
+ const SESSIONS_ROOT = "sessions";
27
+ /** Bound on the head prefix read from one rollout file. */
28
+ const HEAD_BYTES = 65536;
29
+ /** Bound on the tail read from one rollout file for the usage scan. */
30
+ const TAIL_BYTES = 65536;
31
+ /** Bound on the full-file fallback read when the tail scan finds no token_count. */
32
+ const FULL_BYTES = 8388608;
33
+ /**
34
+ * How far back a candidate file's start may precede the run's start and
35
+ * still count as "this run" (clock skew + pre-spawn session creation slack).
36
+ */
37
+ const ROLLOUT_WINDOW_SLACK_MS = 3e5;
38
+ /** Extra future slack for the window end (file timestamps may skew ahead). */
39
+ const ROLLOUT_WINDOW_FUTURE_MS = 6e4;
40
+ /**
41
+ * Whether the scoped home holds usable Codex credentials: the `auth.json`
42
+ * file exists. Absent means the device-code flow has not completed.
43
+ * @param homeDir - the `codex` harness's scoped home.
44
+ * @returns true when credentials are present.
45
+ */
46
+ async function codexAuthenticated(homeDir) {
47
+ try {
48
+ await readFile(join(homeDir, "auth.json"));
49
+ return true;
50
+ } catch (error) {
51
+ if (error.code === "ENOENT") return false;
52
+ throw error;
53
+ }
54
+ }
55
+ /**
56
+ * Parse the first line of a rollout file for the listing fields. Unknown or
57
+ * malformed values yield an undefined record rather than failing the list.
58
+ * @param text - the raw rollout file content (only the head is read).
59
+ * @returns the record, or undefined when the head is not a session_meta line.
60
+ */
61
+ function rolloutRecord(text) {
62
+ const line = text.split("\n")[0];
63
+ if (line === void 0) return void 0;
64
+ let event;
65
+ try {
66
+ event = JSON.parse(line);
67
+ } catch {
68
+ return;
69
+ }
70
+ if (event.type !== "session_meta" || typeof event.payload !== "object" || event.payload === null) return;
71
+ const meta = event.payload;
72
+ const sessionId = typeof meta.id === "string" && meta.id !== "" ? meta.id : typeof meta.session_id === "string" && meta.session_id !== "" ? meta.session_id : void 0;
73
+ if (sessionId === void 0) return void 0;
74
+ if (typeof meta.cwd !== "string") return void 0;
75
+ const record = {
76
+ id: sessionId,
77
+ workDir: meta.cwd
78
+ };
79
+ const startedAt = typeof meta.timestamp === "string" ? Date.parse(meta.timestamp) : void 0;
80
+ if (startedAt !== void 0 && Number.isFinite(startedAt)) record.startedAt = startedAt;
81
+ return record;
82
+ }
83
+ /**
84
+ * Read the scoped home's rollout files. The listing walks
85
+ * `sessions/YYYY/MM/DD/` and reads each `rollout-*.jsonl` head; a torn or
86
+ * malformed file is skipped rather than failing the whole list.
87
+ * @param homeDir - the `codex` harness's scoped home.
88
+ * @returns the parsed records, empty when no sessions exist yet.
89
+ */
90
+ async function listCodexSessions(homeDir) {
91
+ const records = [];
92
+ let years;
93
+ try {
94
+ years = await readdir(join(homeDir, SESSIONS_ROOT));
95
+ } catch (error) {
96
+ if (error.code === "ENOENT") return [];
97
+ throw error;
98
+ }
99
+ for (const year of years) {
100
+ if (!/^\d{4}$/.test(year)) continue;
101
+ let months;
102
+ try {
103
+ months = await readdir(join(homeDir, SESSIONS_ROOT, year));
104
+ } catch {
105
+ continue;
106
+ }
107
+ for (const month of months) {
108
+ if (!/^\d{2}$/.test(month)) continue;
109
+ let days;
110
+ try {
111
+ days = await readdir(join(homeDir, SESSIONS_ROOT, year, month));
112
+ } catch {
113
+ continue;
114
+ }
115
+ for (const day of days) {
116
+ if (!/^\d{2}$/.test(day)) continue;
117
+ let files;
118
+ try {
119
+ files = await readdir(join(homeDir, SESSIONS_ROOT, year, month, day));
120
+ } catch {
121
+ continue;
122
+ }
123
+ for (const name of files) {
124
+ if (!name.startsWith("rollout-") || !name.endsWith(".jsonl")) continue;
125
+ try {
126
+ const handle = await open(join(homeDir, SESSIONS_ROOT, year, month, day, name), "r");
127
+ let text;
128
+ try {
129
+ const buffer = Buffer.alloc(HEAD_BYTES);
130
+ const { bytesRead } = await handle.read(buffer, 0, HEAD_BYTES, 0);
131
+ text = buffer.subarray(0, bytesRead).toString("utf8");
132
+ } finally {
133
+ await handle.close();
134
+ }
135
+ const record = rolloutRecord(text);
136
+ if (record !== void 0) records.push(record);
137
+ } catch {
138
+ continue;
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ return records;
145
+ }
146
+ /**
147
+ * Map codex's token-count payload onto the shared usage contract — the
148
+ * SINGLE caliber for both the exec `turn.completed` usage and the rollout
149
+ * `token_count` recovery, so the two sources can never drift. Codex's
150
+ * `input_tokens` is the TOTAL input including cache hits (OpenAI-style
151
+ * accounting, confirmed against `total_tokens` in the rollout token_count),
152
+ * and `cached_input_tokens` is the cache-read subset — so the uncached
153
+ * bucket subtracts the cached portion to avoid double counting. There is no
154
+ * cache-write concept, so that bucket is omitted (the shared four-bucket
155
+ * contract: uncached input, output, cache read, cache write — codex fills
156
+ * the first three).
157
+ * @param usage - the raw codex usage object (`turn.completed`'s `usage` or a
158
+ * rollout `token_count` `info.last_token_usage`/`total_token_usage`).
159
+ * @returns the shared usage record.
160
+ */
161
+ function usageFromCodex(usage) {
162
+ const raw = usage;
163
+ const input = Number(raw.input_tokens);
164
+ const cached = Number(raw.cached_input_tokens);
165
+ const output = Number(raw.output_tokens);
166
+ const usageRecord = {
167
+ inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached) : Number.isFinite(input) ? input : 0,
168
+ outputTokens: Number.isFinite(output) ? output : 0
169
+ };
170
+ if (Number.isFinite(cached) && cached > 0) usageRecord.cacheReadTokens = cached;
171
+ return usageRecord;
172
+ }
173
+ /**
174
+ * Scan rollout-file text for the LAST `token_count` event and map its usage
175
+ * onto the shared contract. A token_count entry rides an
176
+ * `{"type":"event_msg","payload":{"type":"token_count","info":{…}}}` line
177
+ * (verified against codex-cli 0.144.0 rollout files); the per-turn
178
+ * `info.last_token_usage` is the same caliber as `turn.completed`'s usage,
179
+ * with the session-wide `total_token_usage` as fallback. Torn or malformed
180
+ * lines are skipped; an absent event yields undefined.
181
+ * @param text - raw rollout file content (the tail scan reads a bounded suffix).
182
+ * @returns the mapped usage of the last token_count event, or undefined.
183
+ */
184
+ function codexRolloutTokenUsage(text) {
185
+ let info;
186
+ for (const raw of text.split("\n")) {
187
+ const line = raw.trim();
188
+ if (line === "") continue;
189
+ let event;
190
+ try {
191
+ event = JSON.parse(line);
192
+ } catch {
193
+ continue;
194
+ }
195
+ if (event.type !== "event_msg" || typeof event.payload !== "object" || event.payload === null) continue;
196
+ const payload = event.payload;
197
+ if (payload.type !== "token_count") continue;
198
+ if (typeof payload.info === "object" && payload.info !== null) info = payload.info;
199
+ }
200
+ if (info === void 0) return void 0;
201
+ const entry = info;
202
+ const usage = entry.last_token_usage ?? entry.total_token_usage;
203
+ return usage === void 0 ? void 0 : usageFromCodex(usage);
204
+ }
205
+ /** Parse the ISO timestamp embedded in a `rollout-<ISO>-<uuid>.jsonl` name. */
206
+ function rolloutFilenameTimestamp(name) {
207
+ const match = /^rollout-(\d{4}-\d{2}-\d{2}T\d{2})-(\d{2})-(\d{2})/.exec(name);
208
+ if (match === null) return void 0;
209
+ const parsed = Date.parse(`${match[1]}:${match[2]}:${match[3]}`);
210
+ return Number.isFinite(parsed) ? parsed : void 0;
211
+ }
212
+ /**
213
+ * The codex build that wrote one rollout file, from its `session_meta` head
214
+ * (`payload.cli_version`, present since codex 0.14x). This is the AUTHORITY
215
+ * for the version a delegation round actually ran: it is codex's own record of
216
+ * itself, written by the process that served the round, so it cannot drift
217
+ * from the binary the way a later `--version` probe can.
218
+ * @param text - the raw rollout head.
219
+ * @returns the version, or undefined when the head names none.
220
+ */
221
+ function rolloutCliVersion(text) {
222
+ const line = text.split("\n")[0];
223
+ if (line === void 0) return void 0;
224
+ let event;
225
+ try {
226
+ event = JSON.parse(line);
227
+ } catch {
228
+ return;
229
+ }
230
+ if (event.type !== "session_meta" || typeof event.payload !== "object" || event.payload === null) return;
231
+ const version = event.payload.cli_version;
232
+ return typeof version === "string" && version !== "" ? version : void 0;
233
+ }
234
+ /**
235
+ * How far before the run's spawn moment a `turn_context` timestamp may sit and
236
+ * still count as THIS run's turn (sub-second ordering between the host's
237
+ * spawn clock and the CLI's own stamps).
238
+ */
239
+ const ROLLOUT_CONTEXT_SLACK_MS = 2e3;
240
+ /**
241
+ * Locate ONE run's rollout file. Walks the scoped home's `sessions/YYYY/MM/DD/`
242
+ * tree reading each file's head (the same bounded walk as
243
+ * {@link listCodexSessions}), then prefers the file whose session id equals
244
+ * `locator.threadId`; otherwise the file whose start falls in the window
245
+ * around `locator.windowStart`, newest first.
246
+ *
247
+ * **Concurrency**: the window alone cannot separate two runs started seconds
248
+ * apart, so when the locator names a `cwd` the window set is restricted to
249
+ * files whose head records that same directory. A window that holds candidates
250
+ * but none in the run's directory resolves to NOTHING rather than to a
251
+ * neighbour's file — unless the window holds exactly one candidate, where
252
+ * there is no other run to confuse it with and the mismatch is path skew (a
253
+ * symlinked temp root) rather than ambiguity. A misattributed model is a
254
+ * worse answer than an absent one: the evaluation fails a run loud on
255
+ * declared ≠ observed.
256
+ * @param homeDir - the `codex` harness's scoped home.
257
+ * @param locator - the thread id, working directory, and/or start-time window.
258
+ * @returns the located candidate, or undefined when nothing locates.
259
+ */
260
+ async function locateCodexRollout(homeDir, locator) {
261
+ const candidates = [];
262
+ let years;
263
+ try {
264
+ years = await readdir(join(homeDir, SESSIONS_ROOT));
265
+ } catch (error) {
266
+ if (error.code === "ENOENT") return void 0;
267
+ throw error;
268
+ }
269
+ for (const year of years) {
270
+ if (!/^\d{4}$/.test(year)) continue;
271
+ let months;
272
+ try {
273
+ months = await readdir(join(homeDir, SESSIONS_ROOT, year));
274
+ } catch {
275
+ continue;
276
+ }
277
+ for (const month of months) {
278
+ if (!/^\d{2}$/.test(month)) continue;
279
+ let days;
280
+ try {
281
+ days = await readdir(join(homeDir, SESSIONS_ROOT, year, month));
282
+ } catch {
283
+ continue;
284
+ }
285
+ for (const day of days) {
286
+ if (!/^\d{2}$/.test(day)) continue;
287
+ let files;
288
+ try {
289
+ files = await readdir(join(homeDir, SESSIONS_ROOT, year, month, day));
290
+ } catch {
291
+ continue;
292
+ }
293
+ for (const name of files) {
294
+ if (!name.startsWith("rollout-") || !name.endsWith(".jsonl")) continue;
295
+ const path = join(homeDir, SESSIONS_ROOT, year, month, day, name);
296
+ try {
297
+ const handle = await open(path, "r");
298
+ let text;
299
+ try {
300
+ const buffer = Buffer.alloc(HEAD_BYTES);
301
+ const { bytesRead } = await handle.read(buffer, 0, HEAD_BYTES, 0);
302
+ text = buffer.subarray(0, bytesRead).toString("utf8");
303
+ } finally {
304
+ await handle.close();
305
+ }
306
+ const record = rolloutRecord(text);
307
+ candidates.push({
308
+ path,
309
+ id: record?.id,
310
+ startedAt: record?.startedAt ?? rolloutFilenameTimestamp(name),
311
+ workDir: record?.workDir,
312
+ cliVersion: rolloutCliVersion(text)
313
+ });
314
+ } catch {
315
+ continue;
316
+ }
317
+ }
318
+ }
319
+ }
320
+ }
321
+ if (candidates.length === 0) return void 0;
322
+ const threadMatch = locator.threadId === void 0 ? void 0 : candidates.find((candidate) => candidate.id === locator.threadId);
323
+ if (threadMatch !== void 0) return threadMatch;
324
+ if (locator.windowStart === void 0) return void 0;
325
+ const windowStart = locator.windowStart - ROLLOUT_WINDOW_SLACK_MS;
326
+ const windowEnd = Date.now() + ROLLOUT_WINDOW_FUTURE_MS;
327
+ const inWindow = candidates.filter((candidate) => candidate.startedAt !== void 0 && candidate.startedAt >= windowStart && candidate.startedAt <= windowEnd).sort((left, right) => (right.startedAt ?? 0) - (left.startedAt ?? 0));
328
+ if (locator.cwd === void 0) return inWindow[0];
329
+ const wanted = resolve(locator.cwd);
330
+ const sameCwd = inWindow.filter((candidate) => candidate.workDir !== void 0 && resolve(candidate.workDir) === wanted);
331
+ if (sameCwd.length > 0) return sameCwd[0];
332
+ return inWindow.length === 1 ? inWindow[0] : void 0;
333
+ }
334
+ /**
335
+ * Scan rollout text for this run's two read-back facts: the LAST `token_count`
336
+ * usage, and the model of the LAST `turn_context` whose top-level `timestamp`
337
+ * falls inside the run's window (`windowStart` onward) — the turn codex
338
+ * actually started for this round, so a resumed thread's earlier rounds are
339
+ * never misread and a round killed mid-turn still observes the model it
340
+ * started with. A torn line is skipped.
341
+ */
342
+ function scanRolloutText(text, windowStart) {
343
+ const usage = codexRolloutTokenUsage(text);
344
+ const scan = usage === void 0 ? {} : { usage };
345
+ for (const raw of text.split("\n")) {
346
+ const line = raw.trim();
347
+ if (line === "") continue;
348
+ let event;
349
+ try {
350
+ event = JSON.parse(line);
351
+ } catch {
352
+ continue;
353
+ }
354
+ if (event.type !== "turn_context" || typeof event.payload !== "object" || event.payload === null) continue;
355
+ if (windowStart !== void 0) {
356
+ const stamped = typeof event.timestamp === "string" ? Date.parse(event.timestamp) : void 0;
357
+ if (stamped === void 0 || !Number.isFinite(stamped) || stamped < windowStart) continue;
358
+ }
359
+ const candidate = event.payload.model;
360
+ if (typeof candidate === "string" && candidate !== "") scan.model = candidate;
361
+ }
362
+ return scan;
363
+ }
364
+ /**
365
+ * Read one located rollout file for the round's usage and model.
366
+ *
367
+ * The tail is read first because it is where `token_count` lives (codex writes
368
+ * one per turn boundary, so the last is at the end). `turn_context` is the
369
+ * opposite: codex writes it when the turn STARTS, so a round that then
370
+ * produces more than {@link TAIL_BYTES} of events pushes it out of the tail
371
+ * entirely — which is exactly how a real evaluation run reported a null model
372
+ * against rollout files that named it plainly, while a short smoke round (a
373
+ * whole file smaller than the tail window) read back fine. So whenever the
374
+ * tail leaves either fact missing, a bounded full read from offset 0 fills it
375
+ * in.
376
+ */
377
+ async function readRolloutFacts(path, windowStart) {
378
+ const handle = await open(path, "r");
379
+ try {
380
+ const { size } = await handle.stat();
381
+ if (size === 0) return {};
382
+ const tailLength = Math.min(TAIL_BYTES, size);
383
+ const tail = Buffer.alloc(tailLength);
384
+ const tailRead = await handle.read(tail, 0, tailLength, size - tailLength);
385
+ const scan = scanRolloutText(tail.subarray(0, tailRead.bytesRead).toString("utf8"), windowStart);
386
+ if (scan.usage !== void 0 && scan.model !== void 0 || size <= TAIL_BYTES) return scan;
387
+ const full = Buffer.alloc(Math.min(FULL_BYTES, size));
388
+ const fullRead = await handle.read(full, 0, full.length, 0);
389
+ const wide = scanRolloutText(full.subarray(0, fullRead.bytesRead).toString("utf8"), windowStart);
390
+ const usage = scan.usage ?? wide.usage;
391
+ const model = scan.model ?? wide.model;
392
+ return {
393
+ ...usage === void 0 ? {} : { usage },
394
+ ...model === void 0 ? {} : { model }
395
+ };
396
+ } finally {
397
+ await handle.close();
398
+ }
399
+ }
400
+ /**
401
+ * Read ONE run's facts back from its rollout file: the token usage a
402
+ * non-completed round never streamed, the model the exec `--json` wire does
403
+ * not carry at all (codex 0.144.0), and the codex build that served it. One
404
+ * locate and one file open serve all three.
405
+ *
406
+ * Every field is absent on a miss — no rollout file, an unreadable home, a
407
+ * hard kill before any turn boundary, a window with no turn_context. Absence
408
+ * is recorded, never guessed.
409
+ * @param homeDir - the `codex` harness's scoped home.
410
+ * @param locator - the thread id, working directory, and/or start-time window.
411
+ * @returns the round's read-back facts.
412
+ */
413
+ async function codexRolloutRoundFacts(homeDir, locator) {
414
+ let located;
415
+ try {
416
+ located = await locateCodexRollout(homeDir, locator);
417
+ } catch {
418
+ return {};
419
+ }
420
+ if (located === void 0) return {};
421
+ const windowStart = locator.windowStart === void 0 ? void 0 : locator.windowStart - ROLLOUT_CONTEXT_SLACK_MS;
422
+ let scan = {};
423
+ try {
424
+ scan = await readRolloutFacts(located.path, windowStart);
425
+ } catch {}
426
+ return {
427
+ ...scan.usage === void 0 ? {} : { usage: scan.usage },
428
+ ...scan.model === void 0 ? {} : { model: scan.model },
429
+ ...located.cliVersion === void 0 ? {} : { cliVersion: located.cliVersion }
430
+ };
431
+ }
432
+ //#endregion
433
+ //#region lib/types/live-driver.js
434
+ /**
435
+ * The codex provider's live driver: one resident `codex app-server --stdio`
436
+ * process per member (child session), driven over the vendor's experimental
437
+ * JSON-RPC app-server wire (protocol-probed against codex-cli 0.144.0's
438
+ * `app-server generate-json-schema`; the harness `subagent-codex` package is
439
+ * the reference implementation). A delegation round is a `turn/start` on the
440
+ * member's resident thread instead of a fresh `codex exec` process, which buys
441
+ * what the process boundary could not: runtime-level graceful interrupt
442
+ * (`turn/interrupt`; the process survives and the thread stays continuable)
443
+ * and push-mode mirroring (`item/completed` + delta notifications instead of
444
+ * folding a polled stdout stream).
445
+ *
446
+ * Lifecycle mirrors the dsh live driver's discipline (M1): lazy spawn with an
447
+ * initialize handshake, one in-flight spawn per member, idle-timeout reclaim
448
+ * (stdin EOF → grace → SIGTERM ladder — the app-server wire has no shutdown
449
+ * method), crash re-spawn with `thread/resume` of the recorded thread id, a
450
+ * channel breaker with cooldown, and `disposeAll` on plugin unload. The wire
451
+ * adapter is self-contained (no new dependency): line framing with a
452
+ * StringDecoder for split UTF-8 tails, request correlation, and unattended
453
+ * auto-answers for the server→client approval requests (the proposal's
454
+ * default permission policy: answer exactly as the unattended exec mode does).
455
+ * @module @khorsheed/dsh-local-agent-codex/live-driver
456
+ */
457
+ /** Default idle lifetime of an unused resident runtime before reclaim. */
458
+ const DEFAULT_LIVE_IDLE_MS = 18e5;
459
+ /** Default timeout for a wire request acknowledgement. */
460
+ const DEFAULT_LIVE_REQUEST_TIMEOUT_MS = 3e4;
461
+ /** Default timeout for the app-server handshake (a cold boot can be slow). */
462
+ const DEFAULT_LIVE_INITIALIZE_TIMEOUT_MS = 6e4;
463
+ /** Bounded wait for an interrupted turn to converge while a run disposes. */
464
+ const DEFAULT_LIVE_DISPOSE_CONVERGE_MS = 5e3;
465
+ /** How long a tripped channel breaker stays on the exec fallback. */
466
+ const DEFAULT_LIVE_CHANNEL_RETRY_MS = 3e5;
467
+ /** Grace between stdin EOF and SIGTERM when reclaiming an app-server. */
468
+ const RECLAIM_EOF_GRACE_MS = 1e3;
469
+ /** Default minimum interval between one streaming item's snapshot messages. */
470
+ const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = LIVE_FLUSH_INTERVAL_MS;
471
+ /**
472
+ * The app-server channel could not come up (spawn failure or handshake
473
+ * timeout/mismatch). The provider catches exactly this and falls back to the
474
+ * exec one-shot; any other error is a real round failure.
475
+ */
476
+ var LiveChannelUnavailableError = class extends Error {
477
+ constructor(message) {
478
+ super(message);
479
+ this.name = "LiveChannelUnavailableError";
480
+ }
481
+ };
482
+ const DEFAULT_TIMEOUTS = {
483
+ initializeMs: DEFAULT_LIVE_INITIALIZE_TIMEOUT_MS,
484
+ requestMs: DEFAULT_LIVE_REQUEST_TIMEOUT_MS,
485
+ convergeMs: DEFAULT_LIVE_DISPOSE_CONVERGE_MS,
486
+ channelRetryMs: DEFAULT_LIVE_CHANNEL_RETRY_MS
487
+ };
488
+ function thrown$1(value) {
489
+ return value instanceof Error ? value : new Error(String(value));
490
+ }
491
+ /**
492
+ * Auth-shaped failure signatures in codex's error output: the endpoint
493
+ * rejected the credential. Mirrors the exec path's detector
494
+ * (CODEX_AUTH_FAILURE in codex-cli-provider.ts); the live path matches
495
+ * against turn-failure messages, which is where a mid-run 401 surfaces when
496
+ * the process does not exit.
497
+ */
498
+ const CODEX_LIVE_AUTH_FAILURE = /401 unauthorized|unauthorized|authentication failed|not logged in|access token/i;
499
+ function delay(ms) {
500
+ return new Promise((resolve) => setTimeout(resolve, ms));
501
+ }
502
+ /**
503
+ * Map one app-server `item/completed` ThreadItem onto the shared transcript
504
+ * line shape — the live transport's entry into the SAME fold the exec NDJSON
505
+ * parser owns (`foldCodexStreamLine`); both produce `CodexTranscriptLine`s
506
+ * and append through `appendCodexTranscriptLine`, so the two drivers' final
507
+ * child transcripts cannot drift.
508
+ */
509
+ function codexAppServerItemToLine(item) {
510
+ const itemId = typeof item["id"] === "string" ? item["id"] : void 0;
511
+ switch (item["type"]) {
512
+ case "reasoning": {
513
+ const parts = [item["summary"], item["content"]].filter(Array.isArray).flat().filter((entry) => typeof entry === "string" && entry.trim() !== "");
514
+ return parts.length === 0 ? void 0 : {
515
+ kind: "think",
516
+ text: parts.join("\n"),
517
+ ...itemId === void 0 ? {} : { itemId }
518
+ };
519
+ }
520
+ case "agentMessage": return typeof item["text"] === "string" ? {
521
+ kind: "text",
522
+ text: item["text"],
523
+ ...itemId === void 0 ? {} : { itemId }
524
+ } : void 0;
525
+ case "commandExecution": {
526
+ const command = typeof item["command"] === "string" ? item.command : void 0;
527
+ const output = typeof item["aggregatedOutput"] === "string" && item["aggregatedOutput"].trim() !== "" ? item["aggregatedOutput"] : void 0;
528
+ if (command === void 0 && output === void 0) return void 0;
529
+ return {
530
+ kind: "tool",
531
+ id: itemId ?? `codex-live-${randomUUID()}`,
532
+ name: "Bash",
533
+ ...command === void 0 ? {} : { args: command },
534
+ ...output === void 0 ? {} : { result: output }
535
+ };
536
+ }
537
+ case "webSearch": return {
538
+ kind: "tool",
539
+ id: itemId ?? `codex-live-${randomUUID()}`,
540
+ name: "WebSearch"
541
+ };
542
+ case "fileChange": {
543
+ const changes = Array.isArray(item["changes"]) ? item["changes"] : [];
544
+ const args = changes.map((change) => `${typeof change["kind"] === "string" ? change["kind"] : "update"}: ${typeof change["path"] === "string" ? change["path"] : "?"}`).join("\n");
545
+ const diff = changes.map((change) => change["diff"]).filter((entry) => typeof entry === "string" && entry.trim() !== "").join("\n");
546
+ return {
547
+ kind: "tool",
548
+ id: itemId ?? `codex-live-${randomUUID()}`,
549
+ name: "ApplyPatch",
550
+ ...args === "" ? {} : { args },
551
+ ...diff === "" ? {} : { result: diff }
552
+ };
553
+ }
554
+ case "dynamicToolCall": {
555
+ const tool = typeof item["tool"] === "string" ? item.tool : "dynamic";
556
+ const args = item["arguments"] === void 0 || item["arguments"] === null ? void 0 : typeof item["arguments"] === "string" ? item["arguments"] : JSON.stringify(item["arguments"]);
557
+ return {
558
+ kind: "tool",
559
+ id: itemId ?? `codex-live-${randomUUID()}`,
560
+ name: tool,
561
+ ...args === void 0 || args === "" ? {} : { args }
562
+ };
563
+ }
564
+ case "collabAgentToolCall": {
565
+ const tool = typeof item["tool"] === "string" ? item.tool : "collab";
566
+ return {
567
+ kind: "tool",
568
+ id: itemId ?? `codex-live-${randomUUID()}`,
569
+ name: `collab/${tool}`
570
+ };
571
+ }
572
+ case "mcpToolCall": {
573
+ const server = typeof item["server"] === "string" ? item.server : void 0;
574
+ const tool = typeof item["tool"] === "string" ? item.tool : void 0;
575
+ return {
576
+ kind: "tool",
577
+ id: itemId ?? `codex-live-${randomUUID()}`,
578
+ name: server !== void 0 && tool !== void 0 ? `${server}/${tool}` : tool ?? "mcp"
579
+ };
580
+ }
581
+ case "plan": return typeof item["text"] === "string" && item["text"].trim() !== "" ? {
582
+ kind: "think",
583
+ text: item["text"]
584
+ } : void 0;
585
+ default: return;
586
+ }
587
+ }
588
+ /** Map the app-server's per-turn usage breakdown onto the shared usage contract. */
589
+ function usageFromAppServer(breakdown) {
590
+ const input = Number(breakdown["inputTokens"]);
591
+ const cached = Number(breakdown["cachedInputTokens"]);
592
+ const output = Number(breakdown["outputTokens"]);
593
+ const usage = {
594
+ inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached) : Number.isFinite(input) ? input : 0,
595
+ outputTokens: Number.isFinite(output) ? output : 0
596
+ };
597
+ if (Number.isFinite(cached) && cached > 0) usage.cacheReadTokens = cached;
598
+ return usage;
599
+ }
600
+ /**
601
+ * The app-server wire peer: newline-delimited JSON-RPC over the child's
602
+ * stdio. Self-contained on purpose (no new package dependency): request
603
+ * correlation, StringDecoder framing, and server→client request answering
604
+ * (the approval surface, auto-declined per the unattended policy).
605
+ */
606
+ var CodexWirePeer = class {
607
+ output;
608
+ warn;
609
+ onServerRequest;
610
+ onNotification;
611
+ requestMs;
612
+ buffer = "";
613
+ decoder = new StringDecoder("utf8");
614
+ nextId = 0;
615
+ pending = /* @__PURE__ */ new Map();
616
+ closed = false;
617
+ constructor(input, output, warn, onServerRequest, onNotification, requestMs) {
618
+ this.output = output;
619
+ this.warn = warn;
620
+ this.onServerRequest = onServerRequest;
621
+ this.onNotification = onNotification;
622
+ this.requestMs = requestMs;
623
+ input?.on("data", (chunk) => {
624
+ this.feed(this.decoder.write(chunk));
625
+ });
626
+ }
627
+ write(message) {
628
+ this.output?.write(JSON.stringify(message) + "\n");
629
+ }
630
+ request(method, params, timeoutMs) {
631
+ if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("subagent-codex live: the wire is closed"));
632
+ this.nextId += 1;
633
+ const id = `req_${this.nextId}`;
634
+ return new Promise((resolve, reject) => {
635
+ const timer = setTimeout(() => {
636
+ this.pending.delete(id);
637
+ reject(/* @__PURE__ */ new Error(`subagent-codex live: ${method} timed out`));
638
+ timer.unref();
639
+ }, timeoutMs ?? this.requestMs);
640
+ timer.unref();
641
+ this.pending.set(id, {
642
+ resolve: (result) => resolve(result),
643
+ reject,
644
+ timer
645
+ });
646
+ this.write({
647
+ jsonrpc: "2.0",
648
+ id,
649
+ method,
650
+ params
651
+ });
652
+ });
653
+ }
654
+ notify(method, params) {
655
+ this.write(params === void 0 ? {
656
+ jsonrpc: "2.0",
657
+ method
658
+ } : {
659
+ jsonrpc: "2.0",
660
+ method,
661
+ params
662
+ });
663
+ }
664
+ /** Detach and reject every outstanding request. */
665
+ close() {
666
+ if (this.closed) return;
667
+ this.closed = true;
668
+ for (const request of this.pending.values()) {
669
+ clearTimeout(request.timer);
670
+ request.reject(/* @__PURE__ */ new Error("subagent-codex live: the wire closed"));
671
+ }
672
+ this.pending.clear();
673
+ }
674
+ feed(text) {
675
+ this.buffer += text;
676
+ let index = this.buffer.indexOf("\n");
677
+ while (index >= 0) {
678
+ const line = this.buffer.slice(0, index);
679
+ this.buffer = this.buffer.slice(index + 1);
680
+ index = this.buffer.indexOf("\n");
681
+ if (line.trim() !== "") this.dispatchLine(line);
682
+ }
683
+ }
684
+ /** Handle one wire line; a malformed or hostile line never escapes this frame. */
685
+ dispatchLine(line) {
686
+ let message;
687
+ try {
688
+ message = JSON.parse(line);
689
+ } catch {
690
+ this.warn("subagent-codex live: ignored a malformed wire line");
691
+ return;
692
+ }
693
+ try {
694
+ const hasId = typeof message.id === "string" || typeof message.id === "number";
695
+ if (hasId && typeof message.method === "string") {
696
+ const id = message.id;
697
+ try {
698
+ const result = this.onServerRequest(message.method, message.params ?? {});
699
+ this.write({
700
+ jsonrpc: "2.0",
701
+ id,
702
+ result
703
+ });
704
+ } catch (error) {
705
+ this.write({
706
+ jsonrpc: "2.0",
707
+ id,
708
+ error: {
709
+ code: -32603,
710
+ message: thrown$1(error).message
711
+ }
712
+ });
713
+ }
714
+ return;
715
+ }
716
+ if (hasId && ("result" in message || "error" in message)) {
717
+ const request = this.pending.get(String(message.id));
718
+ if (request === void 0) return;
719
+ this.pending.delete(String(message.id));
720
+ clearTimeout(request.timer);
721
+ const wireError = message.error;
722
+ if (wireError !== void 0 && wireError !== null) request.reject(/* @__PURE__ */ new Error(`subagent-codex live: ${wireError.message ?? "wire error"}`));
723
+ else request.resolve(message.result);
724
+ return;
725
+ }
726
+ if (typeof message.method === "string") this.onNotification(message.method, message.params ?? {});
727
+ } catch (error) {
728
+ this.warn(`subagent-codex live: wire line handler failed: ${thrown$1(error).message}`);
729
+ }
730
+ }
731
+ };
732
+ /** One resident app-server process: the wire peer, the loaded thread's id, and
733
+ * the reclaim ladder (stdin EOF → grace → SIGTERM → grace → SIGKILL).
734
+ */
735
+ var CodexLiveRuntime = class {
736
+ child;
737
+ dead = false;
738
+ reclaimed = false;
739
+ peer;
740
+ /** The member's loaded thread; assigned by the first round's thread/start or thread/resume. */
741
+ threadId;
742
+ /**
743
+ * The model this process bound at spawn (the `-c model=…` override), or
744
+ * undefined when it spawned with no model flag at all. A round whose start
745
+ * model differs retires the runtime instead of silently running the old one.
746
+ */
747
+ configurationKey;
748
+ boundModel;
749
+ /** Turn ids of settled/interrupted rounds — their late notifications are tagged out. */
750
+ retiredTurnIds = /* @__PURE__ */ new Set();
751
+ /** The active round's notification sink; installed per round, cleared at settle. */
752
+ onWireNotification;
753
+ onDead;
754
+ stderrTail = "";
755
+ stderrDecoder = new StringDecoder("utf8");
756
+ constructor(child, onServerRequest, warn, requestMs) {
757
+ this.child = child;
758
+ this.peer = new CodexWirePeer(child.stdout, child.stdin, warn, onServerRequest, (method, params) => {
759
+ this.onWireNotification?.(method, params);
760
+ }, requestMs);
761
+ child.stderr?.on("data", (chunk) => {
762
+ this.stderrTail = (this.stderrTail + this.stderrDecoder.write(chunk)).slice(-4096);
763
+ });
764
+ child.done.then(() => {
765
+ this.markDead();
766
+ }, () => {
767
+ this.markDead();
768
+ });
769
+ }
770
+ get diagnostics() {
771
+ return this.stderrTail.trim();
772
+ }
773
+ markDead() {
774
+ if (this.dead) return;
775
+ this.dead = true;
776
+ this.peer.close();
777
+ this.onDead?.();
778
+ }
779
+ /** Graceful teardown: close the wire, EOF stdin, grace, then the terminate ladder. */
780
+ async reclaim() {
781
+ if (this.reclaimed) return;
782
+ this.reclaimed = true;
783
+ this.peer.close();
784
+ if (!this.dead) {
785
+ this.child.stdin?.end();
786
+ await Promise.race([this.child.done.catch(() => {}), delay(RECLAIM_EOF_GRACE_MS)]);
787
+ }
788
+ if (!this.dead) {
789
+ this.child.terminate();
790
+ await this.child.waitForExit();
791
+ }
792
+ await this.child.done.catch(() => {});
793
+ this.markDead();
794
+ }
795
+ };
796
+ /** Unattended approval answer: prefer cancel, else decline (harness-proven tables). */
797
+ function unattendedDecision(params) {
798
+ const available = params["availableDecisions"];
799
+ if (Array.isArray(available)) {
800
+ if (available.includes("cancel")) return "cancel";
801
+ if (available.includes("decline")) return "decline";
802
+ }
803
+ return "decline";
804
+ }
805
+ /**
806
+ * The codex live driver owns every resident app-server runtime of this
807
+ * provider generation. Created only when `live: true`; disposed with the
808
+ * plugin.
809
+ */
810
+ var CodexLiveDriver = class {
811
+ ctx;
812
+ config;
813
+ timeouts;
814
+ runtimes = /* @__PURE__ */ new Map();
815
+ /** In-flight spawns by member: concurrent rounds share one, disposeAll waits them out. */
816
+ ensuring = /* @__PURE__ */ new Map();
817
+ idleTimers = /* @__PURE__ */ new Map();
818
+ /** Per-member round serialization (the resume lock covers resume-vs-resume only). */
819
+ roundChains = /* @__PURE__ */ new Map();
820
+ channelBrokenAt;
821
+ disposed = false;
822
+ /**
823
+ * Set by drain() (a settings-driven generation handoff): new rounds are
824
+ * refused so the provider falls back to exec, while in-flight rounds finish
825
+ * on their runtime undisturbed. Unlike `disposed`, the driver still serves
826
+ * what it already accepted.
827
+ */
828
+ draining = false;
829
+ disposeController = new AbortController();
830
+ constructor(ctx, config, timeouts = DEFAULT_TIMEOUTS) {
831
+ this.ctx = ctx;
832
+ this.config = config;
833
+ this.timeouts = timeouts;
834
+ }
835
+ /** Whether the channel is in its post-failure cooldown (rounds fall back to exec). */
836
+ get disabled() {
837
+ return this.channelBrokenAt !== void 0 && Date.now() - this.channelBrokenAt < (this.timeouts.channelRetryMs ?? 3e5);
838
+ }
839
+ /** Count of resident runtimes currently registered (zombie accounting in tests). */
840
+ get liveCount() {
841
+ return this.runtimes.size;
842
+ }
843
+ clearIdleTimer(key) {
844
+ const timer = this.idleTimers.get(key);
845
+ if (timer !== void 0) clearTimeout(timer);
846
+ this.idleTimers.delete(key);
847
+ }
848
+ armIdleTimer(key) {
849
+ this.clearIdleTimer(key);
850
+ const idleMs = this.config.liveIdleMs ?? 18e5;
851
+ const timer = setTimeout(() => {
852
+ this.reclaim(key);
853
+ }, idleMs);
854
+ timer.unref();
855
+ this.idleTimers.set(key, timer);
856
+ }
857
+ async reclaim(key) {
858
+ const runtime = this.runtimes.get(key);
859
+ this.clearIdleTimer(key);
860
+ if (runtime === void 0) return;
861
+ this.runtimes.delete(key);
862
+ await runtime.reclaim();
863
+ }
864
+ /** Reclaim every runtime (plugin unload); no process survives the profile. */
865
+ async disposeAll() {
866
+ this.disposed = true;
867
+ this.disposeController.abort();
868
+ await Promise.all([...this.ensuring.values()].map((pending) => pending.catch(() => void 0)));
869
+ for (const key of [...this.runtimes.keys()]) await this.reclaim(key);
870
+ }
871
+ /**
872
+ * True while the member's runtime exists or is being spawned. The settings
873
+ * controller gates a new generation on this: a fresh driver must not serve
874
+ * a member whose retiring generation still hosts the (same) codex thread.
875
+ */
876
+ hasRuntime(key) {
877
+ return this.runtimes.has(key) || this.ensuring.has(key);
878
+ }
879
+ /** @deprecated Compatibility no-op: live output is always incremental. */
880
+ setLiveMirrorGranularity(_granularity) {}
881
+ /**
882
+ * Drain for a settings-driven generation handoff: refuse new rounds (the
883
+ * provider's catch falls back to exec), let every in-flight round finish on
884
+ * its runtime, then reclaim. Resolves when no runtime or spawn remains.
885
+ * Unlike disposeAll, in-flight work is never interrupted.
886
+ */
887
+ async drain() {
888
+ this.draining = true;
889
+ const keys = /* @__PURE__ */ new Set([
890
+ ...this.runtimes.keys(),
891
+ ...this.ensuring.keys(),
892
+ ...this.roundChains.keys()
893
+ ]);
894
+ await Promise.all([...keys].map(async (key) => {
895
+ await (this.roundChains.get(key) ?? Promise.resolve()).catch(() => void 0);
896
+ await (this.ensuring.get(key) ?? Promise.resolve()).catch(() => void 0);
897
+ await this.reclaim(key);
898
+ }));
899
+ }
900
+ /**
901
+ * The model the member's live runtime bound at spawn: the identifier, or
902
+ * undefined when the runtime spawned with no model flag. Null when the
903
+ * member has no live runtime in this generation — the broker's switch check
904
+ * distinguishes "no runtime to retire" from "runtime bound to no model".
905
+ */
906
+ boundModelOf(key) {
907
+ const runtime = this.runtimes.get(key);
908
+ if (runtime === void 0 || runtime.dead) return null;
909
+ return runtime.boundModel;
910
+ }
911
+ /**
912
+ * Retire the member's resident runtime so the NEXT round respawns (the CLI
913
+ * session itself carries over via thread/resume). The composer's model
914
+ * switch calls this after storing the new override; the caller guarantees
915
+ * no round is in flight for the member.
916
+ */
917
+ async retireRuntime(key) {
918
+ await this.reclaim(key);
919
+ }
920
+ /** The member's runtime, spawning it (once per member at a time) when absent or dead. */
921
+ async ensureRuntime(spec, signal) {
922
+ const key = String(spec.childSession.id);
923
+ const startModel = spec.startModel?.trim();
924
+ const existing = this.runtimes.get(key);
925
+ if (existing !== void 0 && !existing.dead) {
926
+ if (spec.configuration !== void 0 && existing.configurationKey !== JSON.stringify(spec.configuration) || spec.configuration === void 0 && existing.configurationKey !== void 0 || startModel !== void 0 && startModel !== "" && existing.boundModel !== startModel) await this.reclaim(key);
927
+ else {
928
+ this.clearIdleTimer(key);
929
+ return existing;
930
+ }
931
+ } else if (existing !== void 0) this.runtimes.delete(key);
932
+ const pending = this.ensuring.get(key);
933
+ if (pending !== void 0) return pending;
934
+ const spawn = this.spawnRuntime(spec, signal).finally(() => {
935
+ this.ensuring.delete(key);
936
+ });
937
+ this.ensuring.set(key, spawn);
938
+ return spawn;
939
+ }
940
+ /**
941
+ * Spawn the member's resident app-server and prove the wire with a
942
+ * handshake. Failure trips the channel breaker; a cancellation mid-spawn
943
+ * reclaims the half-started process without touching the breaker.
944
+ */
945
+ async spawnRuntime(spec, signal) {
946
+ const key = String(spec.childSession.id);
947
+ if (this.disposed) throw new LiveChannelUnavailableError("the live driver is disposed");
948
+ const member = registerCodexMemberRun(this.ctx, String(spec.childSession.id), spec.parentSessionId);
949
+ const startModel = spec.startModel?.trim();
950
+ const model = spec.configuration !== void 0 ? spec.configuration.model : startModel !== void 0 && startModel !== "" ? startModel : this.config.model?.(key)?.trim();
951
+ const spawnSpec = {
952
+ argv: [
953
+ "codex",
954
+ "app-server",
955
+ ...member === void 0 ? [] : ["-c", member.configOverride],
956
+ ...model === void 0 || model === "" ? [] : ["-c", `model=${JSON.stringify(model)}`],
957
+ ...spec.configuration?.effort === void 0 ? [] : ["-c", `model_reasoning_effort=${JSON.stringify(spec.configuration.effort)}`],
958
+ "--stdio"
959
+ ],
960
+ cwd: spec.cwd,
961
+ stdio: {
962
+ stdin: "pipe",
963
+ stdout: "pipe",
964
+ stderr: "pipe"
965
+ },
966
+ graceMs: DEFAULT_DISPOSE_GRACE_MS,
967
+ env: delegationEnv({ CODEX_HOME: spec.homeDir })
968
+ };
969
+ let child;
970
+ try {
971
+ child = this.ctx.subprocess.spawn(spawnSpec);
972
+ } catch (error) {
973
+ member?.release();
974
+ this.markChannelBroken();
975
+ throw new LiveChannelUnavailableError(`the app-server failed to spawn: ${thrown$1(error).message}`);
976
+ }
977
+ const runtime = new CodexLiveRuntime(child, (method, params) => this.answerServerRequest(method, params), (message) => {
978
+ this.ctx.logger.warn(message);
979
+ }, this.timeouts.requestMs);
980
+ runtime.boundModel = model === void 0 || model === "" ? void 0 : model;
981
+ runtime.configurationKey = spec.configuration === void 0 ? void 0 : JSON.stringify(spec.configuration);
982
+ runtime.onDead = () => {
983
+ if (this.runtimes.get(key) === runtime) this.runtimes.delete(key);
984
+ this.clearIdleTimer(key);
985
+ member?.release();
986
+ };
987
+ const aborted = new Promise((_, reject) => {
988
+ const cancel = () => {
989
+ reject(/* @__PURE__ */ new Error("subagent-codex: run cancelled locally"));
990
+ };
991
+ const unload = () => {
992
+ reject(new LiveChannelUnavailableError("the live driver was disposed during spawn"));
993
+ };
994
+ if (signal.aborted) {
995
+ cancel();
996
+ return;
997
+ }
998
+ if (this.disposeController.signal.aborted) {
999
+ unload();
1000
+ return;
1001
+ }
1002
+ signal.addEventListener("abort", cancel, { once: true });
1003
+ this.disposeController.signal.addEventListener("abort", unload, { once: true });
1004
+ });
1005
+ aborted.catch(() => {});
1006
+ try {
1007
+ await Promise.race([runtime.peer.request("initialize", {
1008
+ clientInfo: {
1009
+ name: "dsh-local-agent-codex",
1010
+ title: "dsh local-agent codex live driver",
1011
+ version: "0.1.0"
1012
+ },
1013
+ capabilities: {
1014
+ experimentalApi: false,
1015
+ requestAttestation: false
1016
+ }
1017
+ }, this.timeouts.initializeMs), aborted]);
1018
+ runtime.peer.notify("initialized");
1019
+ } catch (error) {
1020
+ await runtime.reclaim();
1021
+ if (signal.aborted) throw thrown$1(error);
1022
+ this.markChannelBroken();
1023
+ throw new LiveChannelUnavailableError(`the app-server handshake failed: ${thrown$1(error).message}`);
1024
+ }
1025
+ if (this.disposed) {
1026
+ await runtime.reclaim();
1027
+ throw new LiveChannelUnavailableError("the live driver was disposed during spawn");
1028
+ }
1029
+ this.channelBrokenAt = void 0;
1030
+ this.runtimes.set(key, runtime);
1031
+ return runtime;
1032
+ }
1033
+ markChannelBroken() {
1034
+ this.channelBrokenAt = Date.now();
1035
+ }
1036
+ /**
1037
+ * Report an auth-shaped round failure to the family registry's auth-failure
1038
+ * mark (the exec path's post-exit 401 detection ported to a process that
1039
+ * never exits: the match runs against the settled error instead). Degrades
1040
+ * silently on a core predating reportAuthFailure.
1041
+ */
1042
+ reportAuthIfShaped(error) {
1043
+ if (!CODEX_LIVE_AUTH_FAILURE.test(error.message)) return;
1044
+ const registry = this.ctx.localAgent;
1045
+ if (typeof registry.reportAuthFailure === "function") registry.reportAuthFailure("codex", error.message.split("\n").find((line) => line.trim() !== "") ?? error.message);
1046
+ }
1047
+ /**
1048
+ * Auto-answer the app-server's approval/elicitation requests, matching the
1049
+ * unattended exec behavior (and the harness reference implementation's
1050
+ * tables): the live driver never grants interactive approval. This is the
1051
+ * proposal's default permission policy; the relay hook for a human
1052
+ * approval UI is a later increment.
1053
+ */
1054
+ answerServerRequest(method, params) {
1055
+ switch (method) {
1056
+ case "item/commandExecution/requestApproval":
1057
+ case "item/fileChange/requestApproval": return { decision: unattendedDecision(params) };
1058
+ case "item/permissions/requestApproval": return {
1059
+ permissions: {},
1060
+ scope: "turn"
1061
+ };
1062
+ case "item/tool/requestUserInput": return { answers: {} };
1063
+ case "mcpServer/elicitation/request": return {
1064
+ action: "decline",
1065
+ content: null,
1066
+ _meta: null
1067
+ };
1068
+ default: throw new Error(`subagent-codex live: unsupported app-server request ${method}`);
1069
+ }
1070
+ }
1071
+ /**
1072
+ * Drive one delegation round on the member's resident app-server. Mirrors
1073
+ * the exec run's settlement contract exactly so the facade, the tool, and
1074
+ * the projections cannot tell the difference — except `cancel` is a runtime
1075
+ * interrupt and the process survives. Cancellation is honored in every
1076
+ * window (the M1 acceptance lesson): the abort listener registers before
1077
+ * any await, the spawn/handshake races it, and a pre-accept cancel reclaims
1078
+ * the fresh runtime instead of letting the turn run unwatched.
1079
+ */
1080
+ /** Create/resume the native thread without starting a model turn. */
1081
+ async prepare(spec, signal) {
1082
+ signal.throwIfAborted();
1083
+ if (this.draining || this.disabled) throw new LiveChannelUnavailableError("the live driver is unavailable for preparation");
1084
+ try {
1085
+ const runtime = await this.ensureRuntime(spec, signal);
1086
+ await this.ensureThread(runtime, spec);
1087
+ signal.throwIfAborted();
1088
+ this.armIdleTimer(String(spec.childSession.id));
1089
+ } catch (error) {
1090
+ await this.reclaim(String(spec.childSession.id));
1091
+ throw error;
1092
+ }
1093
+ }
1094
+ async ensureThread(rt, spec) {
1095
+ if (rt.threadId === void 0) {
1096
+ const permissionParams = {
1097
+ approvalPolicy: "never",
1098
+ sandbox: this.config.sandbox ?? "workspace-write"
1099
+ };
1100
+ if (spec.resume === void 0) {
1101
+ const response = await rt.peer.request("thread/start", {
1102
+ cwd: spec.cwd,
1103
+ ephemeral: false,
1104
+ ...permissionParams
1105
+ });
1106
+ if (typeof response?.thread?.id !== "string") throw new Error("subagent-codex live: thread/start returned no thread id");
1107
+ rt.threadId = response.thread.id;
1108
+ } else {
1109
+ const response = await rt.peer.request("thread/resume", {
1110
+ threadId: spec.resume.cliSessionId,
1111
+ cwd: spec.cwd,
1112
+ ...permissionParams
1113
+ });
1114
+ if (typeof response?.thread?.id !== "string") throw new Error("subagent-codex live: thread/resume returned no thread");
1115
+ rt.threadId = response.thread.id;
1116
+ }
1117
+ }
1118
+ if (spec.resume === void 0 && rt.threadId !== void 0) spec.onThreadId?.(rt.threadId);
1119
+ }
1120
+ async startRound(request, spec) {
1121
+ if (this.draining) throw new LiveChannelUnavailableError("the live driver is draining (a settings change retired this generation)");
1122
+ const key = String(spec.childSession.id);
1123
+ const round = (this.roundChains.get(key) ?? Promise.resolve()).catch(() => {}).then(() => this.startRoundLocked(request, spec));
1124
+ this.roundChains.set(key, round.then((handle) => handle.result.catch(() => ({})), () => ({})));
1125
+ return round;
1126
+ }
1127
+ /** The serialized round body. */
1128
+ async startRoundLocked(request, spec) {
1129
+ const task = textTask(request.prompt);
1130
+ if (request.signal.aborted) throw new Error("subagent-codex: request was aborted before the run started");
1131
+ if (this.draining) throw new LiveChannelUnavailableError("the live driver is draining (a settings change retired this generation)");
1132
+ if (this.disposed) throw new Error("subagent-codex: the live driver is disposed");
1133
+ const turn = spec.resume?.turn ?? 1;
1134
+ const childSession = spec.childSession;
1135
+ const localAgent = this.ctx.get("localAgent");
1136
+ /** The round's start moment, anchoring the settle read-back's rollout time window. */
1137
+ const startedAtMs = Date.now();
1138
+ const runAbort = new AbortController();
1139
+ let roundSettled = false;
1140
+ let runtime;
1141
+ /** The in-flight turn's server-assigned id; interrupt needs it. */
1142
+ let activeTurnId;
1143
+ /** Pre-ack turn id observed via turn/started (notifications can precede the response). */
1144
+ let pendingTurnId;
1145
+ let turnOpened = false;
1146
+ let lastText = "";
1147
+ /** Final-answer selection per the harness wire: final_answer phase beats unphased. */
1148
+ let lastFinalAnswer;
1149
+ let lastUnphasedAnswer;
1150
+ /** The round's folded transcript (the exec fold's line shape). */
1151
+ const lines = [];
1152
+ let mirrored = 0;
1153
+ let usage;
1154
+ /** Whether a folded line already carries the round's usage (no backfill event exists). */
1155
+ let usageCarried = false;
1156
+ /**
1157
+ * The step ledger: `reservedSteps` holds every step a stream reservation
1158
+ * consumed (in increasing order — permanently, whether the stream later
1159
+ * completes or stays an orphan snapshot), and a sequential line folds at
1160
+ * its index shifted past every reservation before it. A tool completed
1161
+ * mid-stream therefore folds ABOVE the stream's step and the projection
1162
+ * renders the round's chronological order.
1163
+ */
1164
+ const reservedSteps = [];
1165
+ /**
1166
+ * The streaming items seen this round by item id:
1167
+ * deltas accumulate into throttled snapshot assistant/messages appended
1168
+ * at the item's reserved (turn, step) — the host folds repeated settles
1169
+ * at one coordinate into one live-updating chat node, which is the only
1170
+ * streaming channel left after 0.1.5 retired the durable per-chunk event.
1171
+ * The item's completion folds at the same step and finalizes it; an entry
1172
+ * whose completion never arrives is force-finalized at settle.
1173
+ */
1174
+ const streams = /* @__PURE__ */ new Map();
1175
+ /** The item deltas currently accumulate into (items stream sequentially). */
1176
+ let activeStream;
1177
+ /** Items/completions that arrived before the turn id was known. */
1178
+ const earlyNotifications = [];
1179
+ let persistQueue = Promise.resolve();
1180
+ const persist = () => {
1181
+ persistQueue = persistQueue.then(() => persistChildSession(this.ctx, childSession)).catch((error) => {
1182
+ this.ctx.logger.warn(`subagent-codex: live persistence failed: ${thrown$1(error).message}`);
1183
+ });
1184
+ };
1185
+ const requestCancel = () => {
1186
+ if (roundSettled || runAbort.signal.aborted) return;
1187
+ runAbort.abort(/* @__PURE__ */ new Error("subagent-codex: run cancelled locally"));
1188
+ const turnId = activeTurnId ?? pendingTurnId;
1189
+ if (turnId !== void 0 && runtime?.threadId !== void 0 && !runtime.dead) runtime.peer.request("turn/interrupt", {
1190
+ threadId: runtime.threadId,
1191
+ turnId
1192
+ }).catch(() => {});
1193
+ };
1194
+ const onAbort = () => {
1195
+ requestCancel();
1196
+ };
1197
+ request.signal.addEventListener("abort", onAbort, { once: true });
1198
+ const abortBranch = new Promise((_, reject) => {
1199
+ runAbort.signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("subagent-codex: run cancelled locally")), { once: true });
1200
+ });
1201
+ const collectOutput = () => {
1202
+ const partial = activeStream === void 0 ? "" : streams.get(activeStream)?.text ?? "";
1203
+ const selected = lastFinalAnswer ?? lastUnphasedAnswer ?? (lastText !== "" ? lastText : partial === "" ? void 0 : partial);
1204
+ return selected === void 0 || selected.trim() === "" ? [] : [{
1205
+ type: "text",
1206
+ text: selected
1207
+ }];
1208
+ };
1209
+ /**
1210
+ * Append one snapshot of a streaming item at its reserved (turn, step).
1211
+ * Throttled per item by interval and growth unless `force`; the forced
1212
+ * final snapshot carries `interrupted` (a cancelled turn reads 已停止
1213
+ * legitimately) and, when `withUsage` and no folded line carried it, the
1214
+ * round's usage.
1215
+ */
1216
+ const streamPublisher = localAgent?.liveStreams === void 0 ? void 0 : new LiveStreamPublisher(localAgent.liveStreams, childSession, turn, persist, (error) => this.ctx.logger.warn(`live checkpoint failed: ${String(error)}`));
1217
+ const liveFlush = new LiveFlush((error) => this.ctx.logger.warn(`live mirror flush failed: ${String(error)}`), this.config.snapshotMinIntervalMs ?? DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
1218
+ const pendingStreamArrival = /* @__PURE__ */ new Map();
1219
+ const appendStreamSnapshot = (stream, force, interrupted, withUsage = false, final = false) => {
1220
+ if (stream.text.trim() === "") return;
1221
+ const now = Date.now();
1222
+ if (!force) {
1223
+ if (!pendingStreamArrival.has(stream.step)) pendingStreamArrival.set(stream.step, now);
1224
+ liveFlush.schedule(stream.step, () => appendStreamSnapshot(stream, true, interrupted, withUsage));
1225
+ return;
1226
+ }
1227
+ liveFlush.cancel(stream.step);
1228
+ const receivedAt = pendingStreamArrival.get(stream.step) ?? now;
1229
+ pendingStreamArrival.delete(stream.step);
1230
+ if (streamPublisher !== void 0 && !final) {
1231
+ if (!stream.opened) {
1232
+ childSession.append("step/start", {
1233
+ turn,
1234
+ step: stream.step
1235
+ });
1236
+ stream.opened = true;
1237
+ }
1238
+ streamPublisher.update(stream.step, stream.kind, stream.text, { receivedAt });
1239
+ stream.lastSnapshotAt = now;
1240
+ stream.lastSnapshotLen = stream.text.length;
1241
+ return;
1242
+ }
1243
+ if (!stream.opened) {
1244
+ childSession.append("step/start", {
1245
+ turn,
1246
+ step: stream.step
1247
+ });
1248
+ stream.opened = true;
1249
+ }
1250
+ const attachUsage = withUsage && !usageCarried && usage !== void 0;
1251
+ childSession.append("assistant/message", {
1252
+ turn,
1253
+ step: stream.step,
1254
+ message: codexAssistantEvent([stream.kind === "think" ? {
1255
+ type: "reasoning",
1256
+ text: stream.text
1257
+ } : {
1258
+ type: "text",
1259
+ text: stream.text
1260
+ }]),
1261
+ stream: [],
1262
+ ...withUsage && !usageCarried && usage !== void 0 ? { usage } : {},
1263
+ ...interrupted ? { interrupted: true } : {}
1264
+ }, { surfaceOp: "append" });
1265
+ if (attachUsage) usageCarried = true;
1266
+ stream.lastSnapshotAt = now;
1267
+ stream.lastSnapshotLen = stream.text.length;
1268
+ persist();
1269
+ };
1270
+ /**
1271
+ * Reserve the step one new streaming item will occupy — past every
1272
+ * completed line (folded or held back) and every earlier reservation. A
1273
+ * different item's deltas force one final snapshot of the current stream
1274
+ * (its completion still folds at its own reserved step when it lands).
1275
+ */
1276
+ const reserveStream = (itemId, kind) => {
1277
+ if (activeStream !== void 0 && activeStream !== itemId) {
1278
+ const previous = streams.get(activeStream);
1279
+ if (previous !== void 0 && previous.text.length !== previous.lastSnapshotLen) appendStreamSnapshot(previous, true, false);
1280
+ activeStream = void 0;
1281
+ }
1282
+ let stream = streams.get(itemId);
1283
+ if (stream === void 0) {
1284
+ const step = lines.length + reservedSteps.length + 1;
1285
+ reservedSteps.push(step);
1286
+ stream = {
1287
+ itemId,
1288
+ kind,
1289
+ step,
1290
+ text: "",
1291
+ lastSnapshotAt: 0,
1292
+ lastSnapshotLen: 0,
1293
+ opened: false
1294
+ };
1295
+ streams.set(itemId, stream);
1296
+ }
1297
+ activeStream = itemId;
1298
+ };
1299
+ /** The sequential fold step for `lines[index]`: its position shifted past every reservation before it. */
1300
+ const foldStep = (index) => {
1301
+ let step = index + 1;
1302
+ for (const reserved of reservedSteps) if (reserved <= step) step += 1;
1303
+ else break;
1304
+ return step;
1305
+ };
1306
+ /** Mirror folded lines [mirrored, upto); the last line is held back until completion. */
1307
+ const mirrorUpTo = (upto, withUsage) => {
1308
+ let usageIndex = -1;
1309
+ if (withUsage) {
1310
+ for (let index = 0; index < lines.length; index += 1) if (lines[index]?.kind !== "tool") usageIndex = index;
1311
+ }
1312
+ for (let index = mirrored; index < upto; index += 1) {
1313
+ const line = lines[index];
1314
+ if (line === void 0) continue;
1315
+ const lineUsage = withUsage && index === usageIndex ? usage : void 0;
1316
+ if (lineUsage !== void 0) usageCarried = true;
1317
+ const itemId = line.kind === "tool" ? void 0 : line.itemId;
1318
+ const stream = itemId === void 0 ? void 0 : streams.get(itemId);
1319
+ if (itemId !== void 0 && stream !== void 0) {
1320
+ liveFlush.cancel(stream.step);
1321
+ streams.delete(itemId);
1322
+ if (activeStream === itemId) activeStream = void 0;
1323
+ if (!stream.opened) childSession.append("step/start", {
1324
+ turn,
1325
+ step: stream.step
1326
+ });
1327
+ const blocks = stream.kind === "think" ? [{
1328
+ type: "reasoning",
1329
+ text: line.kind === "tool" ? "" : line.text
1330
+ }] : [{
1331
+ type: "text",
1332
+ text: line.kind === "tool" ? "" : line.text
1333
+ }];
1334
+ childSession.append("assistant/message", {
1335
+ turn,
1336
+ step: stream.step,
1337
+ message: codexAssistantEvent(blocks),
1338
+ stream: [],
1339
+ ...lineUsage === void 0 ? {} : { usage: lineUsage }
1340
+ }, { surfaceOp: "append" });
1341
+ streamPublisher?.finish(stream.step);
1342
+ childSession.append("step/end", {
1343
+ turn,
1344
+ step: stream.step
1345
+ });
1346
+ } else appendCodexTranscriptLine(childSession, turn, foldStep(index), line, lineUsage);
1347
+ mirrored = index + 1;
1348
+ localAgent?.reportRunProgress(childSession.id, {
1349
+ kind: "delta",
1350
+ text: codexLineText(line)
1351
+ });
1352
+ }
1353
+ if (upto > 0) persist();
1354
+ };
1355
+ let resolveCompletion;
1356
+ const completion = new Promise((resolve) => {
1357
+ resolveCompletion = resolve;
1358
+ });
1359
+ const openTurn = () => {
1360
+ childSession.append("turn/start", { turn });
1361
+ childSession.append("user/message", createUserMessage({
1362
+ content: [{
1363
+ type: "text",
1364
+ text: task
1365
+ }],
1366
+ source: { kind: "user" }
1367
+ }), { surfaceOp: "append" });
1368
+ turnOpened = true;
1369
+ persist();
1370
+ localAgent?.reportRunProgress(childSession.id, {
1371
+ kind: "delta",
1372
+ text: task
1373
+ });
1374
+ for (const early of earlyNotifications.splice(0)) dispatchTurnNotification(early.method, early.params);
1375
+ };
1376
+ /** Process one turn-scoped notification for the round's own turn id. */
1377
+ const dispatchTurnNotification = (method, params) => {
1378
+ if (method === "item/completed") {
1379
+ const item = params["item"];
1380
+ const line = codexAppServerItemToLine(item);
1381
+ if (item["type"] === "agentMessage" && typeof item["text"] === "string") {
1382
+ const phase = item["phase"];
1383
+ if (phase === "final_answer") lastFinalAnswer = item["text"];
1384
+ else if (phase === null || phase === void 0) lastUnphasedAnswer = item["text"];
1385
+ lastText = item["text"];
1386
+ }
1387
+ if (line === void 0) return;
1388
+ lines.push(line);
1389
+ mirrorUpTo(lines.length - 1, false);
1390
+ return;
1391
+ }
1392
+ if (method === "item/agentMessage/delta" || method === "item/reasoning/textDelta") {
1393
+ if (typeof params["delta"] !== "string") return;
1394
+ const text = params["delta"];
1395
+ if (text === "") return;
1396
+ const reasoning = method === "item/reasoning/textDelta";
1397
+ const itemId = typeof params["itemId"] === "string" ? params["itemId"] : `codex-stream-${reasoning ? "think" : "text"}-${turn}`;
1398
+ reserveStream(itemId, reasoning ? "think" : "text");
1399
+ const stream = activeStream === void 0 ? void 0 : streams.get(activeStream);
1400
+ if (stream !== void 0) {
1401
+ stream.text += text;
1402
+ appendStreamSnapshot(stream, false, false);
1403
+ }
1404
+ localAgent?.reportRunProgress(childSession.id, {
1405
+ kind: "delta",
1406
+ text
1407
+ });
1408
+ return;
1409
+ }
1410
+ if (method === "thread/tokenUsage/updated") {
1411
+ const last = params["tokenUsage"]?.["last"];
1412
+ if (last !== void 0) usage = usageFromAppServer(last);
1413
+ return;
1414
+ }
1415
+ if (method === "turn/completed") {
1416
+ const turnData = params["turn"];
1417
+ mirrorUpTo(lines.length, true);
1418
+ resolveCompletion({
1419
+ status: String(turnData["status"]),
1420
+ ...turnData["error"] === null || turnData["error"] === void 0 ? {} : { error: turnData["error"] }
1421
+ });
1422
+ }
1423
+ };
1424
+ /** Route one wire notification: round-scoped, turn-id tagged, early ones buffered. */
1425
+ const dispatchNotification = (method, params) => {
1426
+ const paramThreadId = params["threadId"];
1427
+ if (typeof paramThreadId === "string" && runtime?.threadId !== void 0 && paramThreadId !== runtime.threadId) return;
1428
+ if (method === "turn/started") {
1429
+ const id = params["turn"]?.["id"];
1430
+ if (typeof id === "string" && activeTurnId === void 0 && !runtime?.retiredTurnIds.has(id)) pendingTurnId = id;
1431
+ return;
1432
+ }
1433
+ if (![
1434
+ "item/completed",
1435
+ "item/agentMessage/delta",
1436
+ "item/reasoning/textDelta",
1437
+ "thread/tokenUsage/updated",
1438
+ "turn/completed"
1439
+ ].includes(method)) return;
1440
+ const turnId = method === "turn/completed" ? params["turn"]?.["id"] : params["turnId"];
1441
+ if (!(activeTurnId !== void 0 && turnId === activeTurnId) && !(activeTurnId === void 0 && pendingTurnId !== void 0 && turnId === pendingTurnId)) return;
1442
+ if (!turnOpened) {
1443
+ earlyNotifications.push({
1444
+ method,
1445
+ params
1446
+ });
1447
+ return;
1448
+ }
1449
+ dispatchTurnNotification(method, params);
1450
+ };
1451
+ const accepted = (async () => {
1452
+ const rt = await this.ensureRuntime(spec, request.signal);
1453
+ runtime = rt;
1454
+ if (runAbort.signal.aborted) {
1455
+ await this.reclaim(String(childSession.id));
1456
+ throw new Error("subagent-codex: run cancelled locally");
1457
+ }
1458
+ rt.onWireNotification = dispatchNotification;
1459
+ try {
1460
+ await this.ensureThread(rt, spec);
1461
+ const response = await rt.peer.request("turn/start", {
1462
+ threadId: rt.threadId,
1463
+ input: [{
1464
+ type: "text",
1465
+ text: task,
1466
+ text_elements: []
1467
+ }],
1468
+ ...spec.configuration?.model === void 0 ? {} : { model: spec.configuration.model },
1469
+ ...spec.configuration?.effort === void 0 ? {} : { effort: spec.configuration.effort }
1470
+ });
1471
+ if (typeof response?.turn?.id !== "string") throw new Error("subagent-codex live: turn/start returned no turn id");
1472
+ activeTurnId = response.turn.id;
1473
+ } catch (error) {
1474
+ if (!runAbort.signal.aborted) await this.reclaim(String(childSession.id));
1475
+ throw thrown$1(error);
1476
+ }
1477
+ if (runAbort.signal.aborted && runtime !== void 0 && runtime.threadId !== void 0) runtime.peer.request("turn/interrupt", {
1478
+ threadId: runtime.threadId,
1479
+ turnId: activeTurnId
1480
+ }).catch(() => {});
1481
+ openTurn();
1482
+ })();
1483
+ const attempt = accepted.then(async () => {
1484
+ if (runAbort.signal.aborted) throw new Error("subagent-codex: run cancelled locally");
1485
+ const processFailure = runtime.child.done.then((outcome) => Promise.reject(/* @__PURE__ */ new Error(`subagent-codex: the live runtime exited mid-round (code ${String(outcome.exitCode)}, signal ${String(outcome.signal)})`)), (error) => Promise.reject(thrown$1(error)));
1486
+ processFailure.catch(() => {});
1487
+ const outcome = await Promise.race([completion, processFailure]);
1488
+ if (outcome.status === "interrupted") return {
1489
+ output: collectOutput(),
1490
+ stopReason: "aborted"
1491
+ };
1492
+ if (outcome.status === "failed") {
1493
+ const message = typeof outcome.error?.["message"] === "string" ? outcome.error["message"] : "the codex turn failed";
1494
+ throw new Error(`subagent-codex live: ${message}`);
1495
+ }
1496
+ if (outcome.status !== "completed") throw new Error(`subagent-codex live: the turn ended with unexpected status ${outcome.status}`);
1497
+ const output = collectOutput();
1498
+ if (output.length === 0) throw new Error("subagent-codex live: the turn completed but produced no answer");
1499
+ return {
1500
+ output,
1501
+ stopReason: "completed"
1502
+ };
1503
+ });
1504
+ const result = settleRunResult({
1505
+ attempt: () => Promise.race([attempt, abortBranch]),
1506
+ collectOutput,
1507
+ cancelled: () => runAbort.signal.aborted,
1508
+ onError: (error, stopReason) => {
1509
+ const diagnostics = runtime?.diagnostics ?? "";
1510
+ const suffix = diagnostics === "" ? "" : `; ${diagnostics}`;
1511
+ this.ctx.logger.warn(`subagent-codex: live round failed (${stopReason}): ${error.message}${suffix}`);
1512
+ this.reportAuthIfShaped(error);
1513
+ },
1514
+ signal: request.signal,
1515
+ onAbort
1516
+ }).then(async (settled) => {
1517
+ roundSettled = true;
1518
+ liveFlush.dispose();
1519
+ if (turnOpened) {
1520
+ if (mirrored < lines.length) mirrorUpTo(lines.length, true);
1521
+ if (streams.size > 0) {
1522
+ const remaining = [...streams.values()];
1523
+ for (const [position, stream] of remaining.entries()) {
1524
+ appendStreamSnapshot(stream, true, settled.stopReason !== "completed", position === remaining.length - 1, true);
1525
+ streamPublisher?.finish(stream.step);
1526
+ if (stream.opened) childSession.append("step/end", {
1527
+ turn,
1528
+ step: stream.step
1529
+ });
1530
+ }
1531
+ streams.clear();
1532
+ activeStream = void 0;
1533
+ }
1534
+ if (settled.stopReason === "completed") childSession.append("turn/end", {
1535
+ turn,
1536
+ reason: { kind: "completed" }
1537
+ });
1538
+ else if (settled.stopReason === "aborted") childSession.append("turn/end", {
1539
+ turn,
1540
+ reason: {
1541
+ kind: "aborted",
1542
+ reason: { kind: "parent" }
1543
+ }
1544
+ });
1545
+ else childSession.append("turn/end", {
1546
+ turn,
1547
+ reason: {
1548
+ kind: "error",
1549
+ error: {
1550
+ message: "the live round did not complete",
1551
+ code: "UNKNOWN"
1552
+ }
1553
+ }
1554
+ });
1555
+ }
1556
+ if (runtime !== void 0) {
1557
+ if (activeTurnId !== void 0) runtime.retiredTurnIds.add(activeTurnId);
1558
+ runtime.onWireNotification = void 0;
1559
+ }
1560
+ streamPublisher?.dispose();
1561
+ if (turnOpened) {
1562
+ persist();
1563
+ await persistQueue;
1564
+ }
1565
+ return settled;
1566
+ });
1567
+ result.then(async () => {
1568
+ try {
1569
+ if (turnOpened) {
1570
+ await persistQueue.catch(() => {});
1571
+ localAgent?.reportRunProgress(childSession.id, {
1572
+ kind: "mirror",
1573
+ mirroredLines: mirrored
1574
+ });
1575
+ const facts = await codexRolloutRoundFacts(spec.homeDir, {
1576
+ threadId: runtime?.threadId,
1577
+ windowStart: startedAtMs,
1578
+ cwd: spec.cwd
1579
+ });
1580
+ const settledUsage = usage ?? facts.usage;
1581
+ const round = {
1582
+ ...facts.model === void 0 ? {} : { observedModel: facts.model },
1583
+ ...facts.cliVersion === void 0 ? {} : { cliVersion: facts.cliVersion },
1584
+ ...settledUsage === void 0 ? {} : { usage: settledUsage }
1585
+ };
1586
+ localAgent?.recordRoundSettled?.(childSession.id, round);
1587
+ }
1588
+ } catch (error) {
1589
+ this.ctx.logger.warn(`subagent-codex: live settle observation failed: ${thrown$1(error).message}`);
1590
+ } finally {
1591
+ this.armIdleTimer(String(childSession.id));
1592
+ }
1593
+ });
1594
+ await Promise.race([accepted, abortBranch.catch(() => void 0)]);
1595
+ return subprocessRunHandle({
1596
+ id: childSession.id,
1597
+ result,
1598
+ signal: request.signal,
1599
+ onAbort,
1600
+ requestCancel,
1601
+ teardown: async () => {
1602
+ if (roundSettled) return;
1603
+ await Promise.race([completion.catch(() => ({ status: "interrupted" })), delay(this.timeouts.convergeMs)]);
1604
+ }
1605
+ });
1606
+ }
1607
+ };
1608
+ //#endregion
1609
+ //#region lib/types/provision.js
1610
+ /**
1611
+ * Codex scoped-home provisioning: pin file-based credential storage so the
1612
+ * device-code login writes `auth.json` inside the scoped home. Codex's
1613
+ * `cli_auth_credentials_store` defaults to `auto` — on macOS that resolves
1614
+ * to the keychain, which would both leak credentials outside the scoped
1615
+ * home and defeat the `auth.json` presence check this package's records
1616
+ * adapter uses for authentication. An existing config is respected
1617
+ * untouched; sign-out removes the credential file.
1618
+ * @module @khorsheed/dsh-local-agent-codex/provision
1619
+ */
1620
+ /** Credential file inside the scoped home; its presence marks auth. */
1621
+ const AUTH_FILE = "auth.json";
1622
+ /** Minimal scoped config: keep credentials in the scoped home. */
1623
+ const SCOPED_CONFIG = `# Written by dsh-local-agent-codex: keep the device-code
1624
+ # credentials inside the scoped home (auth.json) instead of the OS keychain.
1625
+ cli_auth_credentials_store = "file"
1626
+ `;
1627
+ /**
1628
+ * Provision `<homeDir>/config.toml` unless one already exists. A present
1629
+ * config is respected untouched (a person may have edited it); otherwise a
1630
+ * minimal config pinning file-based credential storage is written so login
1631
+ * lands in the scoped home's `auth.json`.
1632
+ * @param homeDir - the `codex` harness's scoped home.
1633
+ * @returns true when a config was written, false when one already existed.
1634
+ */
1635
+ async function provisionCodexConfig(homeDir) {
1636
+ try {
1637
+ await readFile(join(homeDir, "config.toml"));
1638
+ return false;
1639
+ } catch (error) {
1640
+ if (error.code !== "ENOENT") throw error;
1641
+ }
1642
+ await writeFile(join(homeDir, "config.toml"), SCOPED_CONFIG, { flag: "wx" });
1643
+ return true;
1644
+ }
1645
+ /**
1646
+ * Read the scoped config's effective custom endpoint: the base_url of the
1647
+ * `model_providers` entry named by `model_provider`, or the first custom
1648
+ * provider's base_url when no selection exists (a user manually editing the
1649
+ * config to route through a custom endpoint typically sets both). Only used
1650
+ * for diagnostics — the config is authoritative and a user-edited value is
1651
+ * respected untouched.
1652
+ * @param homeDir - the `codex` harness's scoped home.
1653
+ * @returns the configured custom base URL, or undefined when none is set.
1654
+ */
1655
+ async function readCodexBaseUrl(homeDir) {
1656
+ let text;
1657
+ try {
1658
+ text = await readFile(join(homeDir, "config.toml"), "utf8");
1659
+ } catch {
1660
+ return;
1661
+ }
1662
+ let section = "";
1663
+ let selected;
1664
+ const providers = /* @__PURE__ */ new Map();
1665
+ for (const rawLine of text.split("\n")) {
1666
+ const line = rawLine.trim();
1667
+ const header = /^\[+([^\]]+)\]+$/.exec(line);
1668
+ if (header !== null) {
1669
+ section = header[1];
1670
+ continue;
1671
+ }
1672
+ const key = /^(\w+)\s*=\s*"([^"]*)"$/.exec(line);
1673
+ if (key === null) continue;
1674
+ if (section === "" && key[1] === "model_provider") selected = key[2];
1675
+ const provider = /^model_providers\.(".*"|[\w-]+)$/.exec(section);
1676
+ if (provider !== null && key[1] === "base_url") providers.set(provider[1].replace(/^"(.*)"$/, "$1"), key[2]);
1677
+ }
1678
+ if (selected !== void 0 && providers.has(selected)) return providers.get(selected);
1679
+ if (providers.size === 1) return [...providers.values()][0];
1680
+ }
1681
+ /**
1682
+ * Read the scoped config's effective reasoning effort: the top-level
1683
+ * `model_reasoning_effort` key (codex reads it for reasoning-capable models).
1684
+ * A missing or malformed config, or no key, yields undefined — the honest
1685
+ * "unknown", never a guessed default. Read-only: the config is authoritative
1686
+ * and a user-edited value is reported as-is. The provider passes no effort
1687
+ * override on the argv, so this IS the value every round runs with.
1688
+ * @param homeDir - the `codex` harness's scoped home.
1689
+ * @returns the configured effort, or undefined when none is set.
1690
+ */
1691
+ async function readCodexReasoningEffort(homeDir) {
1692
+ return readTopLevelConfigString(homeDir, "model_reasoning_effort");
1693
+ }
1694
+ /**
1695
+ * Read the scoped config's configured model: the top-level `model` key — the
1696
+ * selection a plain `codex exec` round runs with. A missing or malformed
1697
+ * config, or no key, yields undefined — the honest "unknown", never a guessed
1698
+ * default. Read-only: the config is authoritative and a user-edited value is
1699
+ * reported as-is.
1700
+ * @param homeDir - the `codex` harness's scoped home.
1701
+ * @returns the configured model identifier, or undefined when none is set.
1702
+ */
1703
+ async function readCodexModel(homeDir) {
1704
+ return readTopLevelConfigString(homeDir, "model");
1705
+ }
1706
+ /**
1707
+ * One top-level string key from the scoped config.toml: counts only before
1708
+ * the first table header — the same rule the model_provider scan applies.
1709
+ * Absent file or absent key yields undefined, never a guessed default.
1710
+ */
1711
+ async function readTopLevelConfigString(homeDir, key) {
1712
+ let text;
1713
+ try {
1714
+ text = await readFile(join(homeDir, "config.toml"), "utf8");
1715
+ } catch {
1716
+ return;
1717
+ }
1718
+ let section = "";
1719
+ for (const rawLine of text.split("\n")) {
1720
+ const line = rawLine.trim();
1721
+ const header = /^\[+([^\]]+)\]+$/.exec(line);
1722
+ if (header !== null) {
1723
+ section = header[1];
1724
+ continue;
1725
+ }
1726
+ if (section !== "") continue;
1727
+ const match = /^(\w+)\s*=\s*"([^"]*)"$/.exec(line);
1728
+ if (match !== null && match[1] === key) return match[2];
1729
+ }
1730
+ }
1731
+ /**
1732
+ * Every model identifier the scoped config.toml itself names, deduped in file
1733
+ * order: the top-level `model` key plus each `[profiles.<name>]` table's own
1734
+ * `model` (a profile is how a codex config names a second model at all).
1735
+ * `[model_providers.*]` tables are deliberately NOT collected — their keys
1736
+ * name providers (endpoint definitions), not models, so offering one as a
1737
+ * model choice would be a wrong suggestion, not a discovered one. Absent file
1738
+ * or no model keys yields an empty list, never a hardcoded catalog entry.
1739
+ * @param homeDir - the `codex` harness's scoped home.
1740
+ * @returns the model identifiers the file carries.
1741
+ */
1742
+ async function listCodexConfigModels(homeDir) {
1743
+ let text;
1744
+ try {
1745
+ text = await readFile(join(homeDir, "config.toml"), "utf8");
1746
+ } catch {
1747
+ return [];
1748
+ }
1749
+ const found = [];
1750
+ const seen = /* @__PURE__ */ new Set();
1751
+ const collect = (value) => {
1752
+ if (value === void 0 || value.trim() === "" || seen.has(value)) return;
1753
+ seen.add(value);
1754
+ found.push(value);
1755
+ };
1756
+ let section = "";
1757
+ for (const rawLine of text.split("\n")) {
1758
+ const line = rawLine.trim();
1759
+ const header = /^\[+([^\]]+)\]+$/.exec(line);
1760
+ if (header !== null) {
1761
+ section = header[1];
1762
+ continue;
1763
+ }
1764
+ const match = /^(\w+)\s*=\s*"([^"]*)"$/.exec(line);
1765
+ if (match === null || match[1] !== "model") continue;
1766
+ if (section === "" || section.startsWith("profiles.")) collect(match[2]);
1767
+ }
1768
+ return found;
1769
+ }
1770
+ /**
1771
+ * The credential file's modification stamp (epoch ms), undefined when absent.
1772
+ * A completed device-code login rewrites `auth.json`, so the stamp
1773
+ * distinguishes a fresh login from a leftover (possibly revoked) credential.
1774
+ * @param homeDir - the `codex` harness's scoped home.
1775
+ * @returns the marker's mtime, or undefined when no credential exists.
1776
+ */
1777
+ async function codexCredentialStamp(homeDir) {
1778
+ try {
1779
+ return (await stat(join(homeDir, AUTH_FILE))).mtimeMs;
1780
+ } catch {
1781
+ return;
1782
+ }
1783
+ }
1784
+ /**
1785
+ * Sign out of the scoped account: remove the credential file, so `/<name>
1786
+ * status` reports not authenticated and the next login authorizes a fresh
1787
+ * account. With `cli_auth_credentials_store = "file"` the credentials live
1788
+ * solely in `auth.json`, so removing it is the logout path.
1789
+ * @param homeDir - the `codex` harness's scoped home.
1790
+ */
1791
+ async function codexLogout(homeDir) {
1792
+ await rm(join(homeDir, AUTH_FILE), { force: true });
1793
+ }
1794
+ //#endregion
1795
+ //#region lib/types/codex-cli-provider.js
1796
+ /**
1797
+ * One-shot Codex CLI subagent lifecycle: spawn `codex exec "<task>"` through
1798
+ * the subprocess seam under the harness's scoped home, capture its printed
1799
+ * response as the run output, and dispose to whole-tree quiescence. Mirrors
1800
+ * the kimi-cli provider: every accepted run is a fresh process and a fresh
1801
+ * codex session; there is no continuation across runs.
1802
+ *
1803
+ * The provider name `codex-local` avoids colliding with the official
1804
+ * `subagent-codex` package's provider name `codex` — a composition that
1805
+ * mounts both would fail loud with DUPLICATE_PROVIDER. The tool row in this
1806
+ * bundle's patch takes the official model-facing name `subagent_codex`
1807
+ * instead: the official preset row ships disabled, and the patch disables
1808
+ * it too (a deliberate user re-enable conflicts loud, by design).
1809
+ * @module @khorsheed/dsh-local-agent-codex/codex-cli-provider
1810
+ */
1811
+ /** Quote one TOML basic string for the `-c` config override. */
1812
+ function tomlString(value) {
1813
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
1814
+ }
1815
+ /** Default POSIX grace between subprocess termination tiers. */
1816
+ const DEFAULT_DISPOSE_GRACE_MS = 3e3;
1817
+ /**
1818
+ * Auth-shaped failure signatures in codex's stderr or event stream: the
1819
+ * endpoint rejected the credential. Narrow on purpose — matched against
1820
+ * provider-controlled error output only.
1821
+ */
1822
+ const CODEX_AUTH_FAILURE = /401 unauthorized|unauthorized|authentication failed|not logged in|access token/i;
1823
+ /** Register one member run with the channel; see {@link CodexMemberRunHandle}. */
1824
+ function registerCodexMemberRun(ctx, childSessionId, parentSessionId) {
1825
+ const registry = ctx.localAgent;
1826
+ if (typeof registry.registerMemberRun !== "function" || typeof registry.memberBridgeSocketPath !== "function" || typeof registry.memberBridgeCommand !== "function") return void 0;
1827
+ const token = registry.registerMemberRun({
1828
+ childSessionId,
1829
+ parentSessionId,
1830
+ provider: "codex-local"
1831
+ });
1832
+ const serverName = `dsh-member-${token.slice(0, 8)}`;
1833
+ const bridge = registry.memberBridgeCommand();
1834
+ const configOverride = `mcp_servers.${serverName}={command=${tomlString(bridge.command)},args=[${bridge.args.map(tomlString).join(",")}],env={${MEMBER_BRIDGE_SOCKET_ENV}=${tomlString(registry.memberBridgeSocketPath())},${MEMBER_BRIDGE_TOKEN_ENV}=${tomlString(token)}},default_tools_approval_mode=${tomlString("approve")}}`;
1835
+ let released = false;
1836
+ return {
1837
+ token,
1838
+ configOverride,
1839
+ release: () => {
1840
+ if (released) return;
1841
+ released = true;
1842
+ registry.unregisterMemberRun(token);
1843
+ }
1844
+ };
1845
+ }
1846
+ /**
1847
+ * One-shot and resumable Codex CLI subagent provider: every accepted FRESH run
1848
+ * starts a `codex exec` process in the delegating Session's workspace, under
1849
+ * the harness scoped home; a resume round (the family tool's staged resume
1850
+ * intent) continues the SAME thread with `codex exec --json resume <thread_id>`
1851
+ * inside the SAME dsh child session. With the live driver configured
1852
+ * (`live: true`), rounds instead go to the resident app-server process (see
1853
+ * live-driver.ts); the exec path below stays the fallback.
1854
+ */
1855
+ /**
1856
+ * Mark the harness credential verified after a completed round, degrading
1857
+ * silently on a core that predates the mark (the family's companion-pair
1858
+ * rule: a provider paired with an older core loses the grade, never the run).
1859
+ * @param ctx - host context carrying the family registry.
1860
+ */
1861
+ function markCredentialVerified(ctx, scope) {
1862
+ const registry = ctx.get("localAgent");
1863
+ if (registry === void 0 || typeof registry.reportAuthSuccess !== "function") return;
1864
+ registry.reportAuthSuccess("codex", scope);
1865
+ }
1866
+ /**
1867
+ * The codex build `codex --version` reports, probed through the shared
1868
+ * subprocess seam inside the SCOPED home (never the user's own installation)
1869
+ * and cached by the family probe against the executable's identity. Both the
1870
+ * effective-settings snapshot and a settled round's read-back use it — the
1871
+ * round prefers its own rollout head, which names the build that actually ran.
1872
+ * @param ctx - host context carrying the subprocess seam.
1873
+ * @param homeDir - the harness's scoped home.
1874
+ * @returns the version, or undefined when the CLI cannot be asked.
1875
+ */
1876
+ function codexCliVersion(ctx, homeDir) {
1877
+ const subprocess = ctx.get("subprocess");
1878
+ if (subprocess === void 0) return Promise.resolve(void 0);
1879
+ return probeCliVersion({
1880
+ argv: ["codex", "--version"],
1881
+ cwd: homeDir,
1882
+ spawn: (spec) => subprocess.spawn(spec),
1883
+ env: delegationEnv({ CODEX_HOME: homeDir })
1884
+ });
1885
+ }
1886
+ var CodexCliProvider = class {
1887
+ ctx;
1888
+ sandbox;
1889
+ live;
1890
+ model;
1891
+ overrides;
1892
+ name = "codex-local";
1893
+ capabilities = NO_START_CAPABILITIES;
1894
+ inheritsParentContext = false;
1895
+ /**
1896
+ * @param live - the live driver, or a resolver returning the current
1897
+ * generation's driver per member (the settings toggle swaps generations;
1898
+ * a resolver may return undefined to steer one member's round to exec
1899
+ * while a retiring generation still hosts it).
1900
+ * @param model - resolver for the configured model, read PER ROUND so a
1901
+ * settings-card write takes effect on the next delegation without a
1902
+ * reload. Undefined (the resolver absent, or returning undefined) leaves
1903
+ * the argv exactly as it was before the key existed — the scoped
1904
+ * config.toml's own `model` then decides, as it always did.
1905
+ * @param overrides - resolver for the member's session-level model override
1906
+ * (the composer picker, owned by the model broker). It outranks BOTH the
1907
+ * delegation's recorded model and the configured one: on the exec path it
1908
+ * re-points the next round's `-m`, and on the live path it is the member's
1909
+ * start model — a resident runtime bound to a different model is retired
1910
+ * so the round respawns onto the override.
1911
+ */
1912
+ constructor(ctx, sandbox = "workspace-write", live, model, overrides) {
1913
+ this.ctx = ctx;
1914
+ this.sandbox = sandbox;
1915
+ this.live = live;
1916
+ this.model = model;
1917
+ this.overrides = overrides;
1918
+ }
1919
+ /** Resolve the live driver for one round's member, if live is on for it. */
1920
+ liveDriver(childSessionId) {
1921
+ const live = this.live;
1922
+ if (live === void 0) return void 0;
1923
+ return typeof live === "function" ? live(childSessionId) : live;
1924
+ }
1925
+ /** Per-round member-channel registration for the exec path (see {@link registerCodexMemberRun}). */
1926
+ memberRun(childSessionId, parentSessionId) {
1927
+ return registerCodexMemberRun(this.ctx, childSessionId, parentSessionId);
1928
+ }
1929
+ async start(request) {
1930
+ const intent = this.ctx.localAgent.takeDelegationIntent(request.parent.session.id, this.name);
1931
+ const scope = intent?.scope;
1932
+ const homeDir = this.ctx.localAgent.homeDir("codex", scope);
1933
+ const cwd = resolveChildCwd(request.parent.session.header.cwd, intent?.cwd);
1934
+ if (cwd === void 0) throw new Error("subagent-codex: the parent session has no working directory to run the CLI in");
1935
+ const exec = intent?.exec;
1936
+ if (exec !== void 0) containerScopedHome(exec, "CODEX_HOME", "subagent-codex");
1937
+ if (intent !== void 0 && intent.kind === "resume") {
1938
+ const record = this.ctx.localAgent.getDelegation(intent.childSessionId);
1939
+ assertResumeCwdUnchanged(record, cwd, "subagent-codex");
1940
+ assertResumeScopeUnchanged(record, scope, "subagent-codex");
1941
+ if (typeof this.ctx.localAgent.withMemberConfigurationRound !== "function") return this.startCodexResume(request, intent, cwd, homeDir, exec, scope, record?.model);
1942
+ return this.ctx.localAgent.withMemberConfigurationRound({
1943
+ childSessionId: intent.childSessionId,
1944
+ provider: this.name,
1945
+ parentSessionId: request.parent.session.id,
1946
+ cwd,
1947
+ ...scope === void 0 ? {} : { scope },
1948
+ ...record?.model === void 0 ? {} : { model: record.model },
1949
+ ...record?.effort === void 0 ? {} : { effort: record.effort },
1950
+ ...record?.configurationLock === void 0 ? {} : { configurationLock: record.configurationLock }
1951
+ }, (configuration) => this.startCodexResume(request, intent, cwd, homeDir, exec, scope, record?.model, configuration), request.signal, intent?.onAdmitted);
1952
+ }
1953
+ const childSessionId = SessionId(intent?.preparedMemberId ?? randomUUID());
1954
+ if (typeof this.ctx.localAgent.withMemberConfigurationRound !== "function") {
1955
+ if (intent?.effort !== void 0) throw new Error("Codex effort requires the configuration admission core");
1956
+ return this.startCodexFresh(request, cwd, homeDir, exec, scope, intent?.model, childSessionId);
1957
+ }
1958
+ return this.ctx.localAgent.withMemberConfigurationRound({
1959
+ childSessionId,
1960
+ provider: this.name,
1961
+ parentSessionId: request.parent.session.id,
1962
+ cwd,
1963
+ ...scope === void 0 ? {} : { scope },
1964
+ ...intent?.model === void 0 ? {} : { model: intent.model },
1965
+ ...intent?.effort === void 0 ? {} : { effort: intent.effort },
1966
+ ...intent?.configurationLock === void 0 ? {} : { configurationLock: intent.configurationLock }
1967
+ }, (configuration) => this.startCodexFresh(request, cwd, homeDir, exec, scope, intent?.model, childSessionId, configuration), request.signal, intent?.onAdmitted);
1968
+ }
1969
+ /** Fresh round: record the child session, spawn `codex exec`, append after settle. */
1970
+ async startCodexFresh(request, cwd, homeDir, exec, scope, requestedModel, runId, configuration) {
1971
+ let childSession;
1972
+ try {
1973
+ const sessions = this.ctx.get("sessions");
1974
+ if (sessions === void 0) throw new Error("the sessions service is not mounted");
1975
+ childSession = sessions.get(runId) ?? sessions.create(runId, { meta: {
1976
+ cwd,
1977
+ parentSession: request.parent.session.id,
1978
+ origin: "subagent",
1979
+ delegationDepth: (request.parent.session.header.delegationDepth ?? 0) + 1
1980
+ } });
1981
+ const harness = this.ctx.localAgent.get("codex");
1982
+ const label = subagentDelegationLabel(harness?.displayName ?? "codex", request.descriptor.label);
1983
+ childSession.append("subagent/descriptor", {
1984
+ ...request.descriptor,
1985
+ label
1986
+ });
1987
+ establishSubagentCatalogChild(request.parent.session, childSession.header, label);
1988
+ } catch (error) {
1989
+ this.ctx.logger.warn(`subagent-codex: subagent session record failed: ${error instanceof Error ? error.message : String(error)}`);
1990
+ }
1991
+ const baseUrl = await readCodexBaseUrl(homeDir).catch(() => void 0);
1992
+ this.ctx.logger.info(`subagent-codex: delegating via ${baseUrl ?? "codex default endpoint"}`);
1993
+ const live = exec === void 0 ? this.liveDriver(runId) : void 0;
1994
+ if (live !== void 0 && childSession !== void 0 && !live.disabled) {
1995
+ const startModel = configuration === void 0 ? this.overrides?.(runId) ?? requestedModel : configuration.resolved.model;
1996
+ try {
1997
+ return await live.startRound(request, {
1998
+ cwd,
1999
+ homeDir,
2000
+ childSession,
2001
+ parentSessionId: request.parent.session.id,
2002
+ ...startModel === void 0 ? {} : { startModel },
2003
+ ...configuration === void 0 ? {} : { configuration: configuration.resolved },
2004
+ onThreadId: (threadId) => {
2005
+ this.ctx.localAgent.recordDelegation({
2006
+ childSessionId: runId,
2007
+ provider: this.name,
2008
+ parentSessionId: request.parent.session.id,
2009
+ cliSessionId: threadId,
2010
+ cwd,
2011
+ ...scope === void 0 ? {} : { scope },
2012
+ ...requestedModel === void 0 ? {} : { model: requestedModel }
2013
+ });
2014
+ }
2015
+ });
2016
+ } catch (error) {
2017
+ if (!(error instanceof LiveChannelUnavailableError) || request.signal.aborted) throw error;
2018
+ this.ctx.logger.warn(`subagent-codex: live driver unavailable, using the exec one-shot: ${error.message}`);
2019
+ }
2020
+ }
2021
+ const member = exec === void 0 ? this.memberRun(runId, request.parent.session.id) : void 0;
2022
+ try {
2023
+ const run = await startCodexCliRun(request, {
2024
+ cwd,
2025
+ env: delegationEnv({ CODEX_HOME: homeDir }),
2026
+ ...exec === void 0 ? {} : { exec },
2027
+ endpointLabel: baseUrl,
2028
+ sandbox: this.sandbox,
2029
+ ...configuration?.resolved ?? resolveRoundModel(this.overrides?.(runId) ?? requestedModel, this.model),
2030
+ disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
2031
+ spawn: (spec) => this.ctx.subprocess.spawn(spec),
2032
+ onError: (error, stopReason) => {
2033
+ this.ctx.logger.warn(`subagent-codex: child run failed (${stopReason}) via ${baseUrl ?? "codex default endpoint"}: ${error instanceof Error ? error.message : String(error)}`);
2034
+ },
2035
+ onAuthFailure: (detail) => {
2036
+ this.ctx.localAgent.reportAuthFailure("codex", detail, scope);
2037
+ },
2038
+ onAuthSuccess: () => {
2039
+ markCredentialVerified(this.ctx, scope);
2040
+ },
2041
+ cliVersion: () => codexCliVersion(this.ctx, homeDir),
2042
+ ...member === void 0 ? {} : { member: { configOverride: member.configOverride } },
2043
+ childSession,
2044
+ ctx: this.ctx,
2045
+ onThreadId: (threadId) => {
2046
+ if (threadId === void 0) return;
2047
+ this.ctx.localAgent.recordDelegation({
2048
+ childSessionId: runId,
2049
+ provider: this.name,
2050
+ parentSessionId: request.parent.session.id,
2051
+ cliSessionId: threadId,
2052
+ cwd,
2053
+ ...scope === void 0 ? {} : { scope },
2054
+ ...requestedModel === void 0 ? {} : { model: requestedModel }
2055
+ });
2056
+ },
2057
+ onRoundSettled: (round) => {
2058
+ this.ctx.localAgent.recordRoundSettled(runId, round);
2059
+ }
2060
+ });
2061
+ if (member !== void 0) run.result.then(member.release, member.release);
2062
+ return run;
2063
+ } catch (error) {
2064
+ member?.release();
2065
+ throw error;
2066
+ }
2067
+ }
2068
+ /** Resume round: continue the recorded thread inside the existing child session. */
2069
+ async startCodexResume(request, intent, cwd, homeDir, exec, scope, requestedModel, configuration) {
2070
+ if (!this.ctx.localAgent.acquireResumeLock(intent.childSessionId)) throw new Error(`subagent-codex: 该子会话有进行中的委派,等其完成后再追问 (child session ${intent.childSessionId})`);
2071
+ try {
2072
+ const childSession = this.ctx.get("sessions")?.get(SessionId(intent.childSessionId));
2073
+ if (childSession === void 0) throw new Error(`subagent-codex: resume target child session ${intent.childSessionId} is not live — start a fresh delegation instead`);
2074
+ const nextTurn = childSession.snapshotEvents().filter((event) => event.type === "turn/start").length + 1;
2075
+ const baseUrl = await readCodexBaseUrl(homeDir).catch(() => void 0);
2076
+ this.ctx.logger.info(`subagent-codex: resuming via ${baseUrl ?? "codex default endpoint"}`);
2077
+ const live = exec === void 0 ? this.liveDriver(intent.childSessionId) : void 0;
2078
+ if (live !== void 0 && !live.disabled) {
2079
+ const startModel = configuration === void 0 ? this.overrides?.(intent.childSessionId) ?? requestedModel : configuration.resolved.model;
2080
+ try {
2081
+ const liveRun = await live.startRound(request, {
2082
+ cwd,
2083
+ homeDir,
2084
+ childSession,
2085
+ parentSessionId: request.parent.session.id,
2086
+ resume: {
2087
+ cliSessionId: intent.cliSessionId,
2088
+ turn: nextTurn
2089
+ },
2090
+ ...startModel === void 0 ? {} : { startModel },
2091
+ ...configuration === void 0 ? {} : { configuration: configuration.resolved }
2092
+ });
2093
+ liveRun.result.then(() => {
2094
+ this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
2095
+ }, () => {
2096
+ this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
2097
+ });
2098
+ return liveRun;
2099
+ } catch (error) {
2100
+ if (!(error instanceof LiveChannelUnavailableError) || request.signal.aborted) throw error;
2101
+ this.ctx.logger.warn(`subagent-codex: live driver unavailable, using the exec one-shot: ${error.message}`);
2102
+ }
2103
+ }
2104
+ const member = exec === void 0 ? this.memberRun(intent.childSessionId, request.parent.session.id) : void 0;
2105
+ let run;
2106
+ try {
2107
+ run = await startCodexCliRun(request, {
2108
+ cwd,
2109
+ env: delegationEnv({ CODEX_HOME: homeDir }),
2110
+ ...exec === void 0 ? {} : { exec },
2111
+ endpointLabel: baseUrl,
2112
+ sandbox: this.sandbox,
2113
+ ...configuration?.resolved ?? resolveRoundModel(this.overrides?.(intent.childSessionId) ?? requestedModel, this.model),
2114
+ disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
2115
+ spawn: (spec) => this.ctx.subprocess.spawn(spec),
2116
+ onError: (error, stopReason) => {
2117
+ this.ctx.logger.warn(`subagent-codex: child run failed (${stopReason}) via ${baseUrl ?? "codex default endpoint"}: ${error instanceof Error ? error.message : String(error)}`);
2118
+ },
2119
+ onAuthFailure: (detail) => {
2120
+ this.ctx.localAgent.reportAuthFailure("codex", detail, scope);
2121
+ },
2122
+ onAuthSuccess: () => {
2123
+ markCredentialVerified(this.ctx, scope);
2124
+ },
2125
+ cliVersion: () => codexCliVersion(this.ctx, homeDir),
2126
+ ...member === void 0 ? {} : { member: { configOverride: member.configOverride } },
2127
+ childSession,
2128
+ ctx: this.ctx,
2129
+ resume: {
2130
+ cliSessionId: intent.cliSessionId,
2131
+ turn: nextTurn
2132
+ },
2133
+ onRoundSettled: (round) => {
2134
+ this.ctx.localAgent.recordRoundSettled(intent.childSessionId, round);
2135
+ }
2136
+ });
2137
+ } catch (error) {
2138
+ member?.release();
2139
+ throw error;
2140
+ }
2141
+ run.result.then(() => {
2142
+ this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
2143
+ member?.release();
2144
+ }, () => {
2145
+ this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
2146
+ member?.release();
2147
+ });
2148
+ return run;
2149
+ } catch (error) {
2150
+ this.ctx.localAgent.releaseResumeLock(intent.childSessionId);
2151
+ throw error;
2152
+ }
2153
+ }
2154
+ };
2155
+ function thrown(value) {
2156
+ /* v8 ignore next -- typed subprocess failures reject with Error. */
2157
+ return value instanceof Error ? value : new Error(String(value));
2158
+ }
2159
+ /**
2160
+ * Validate and join the one-shot task before crossing the process boundary.
2161
+ * @param prompt - task content accepted from the shared subagent service.
2162
+ * @returns the joined non-empty text.
2163
+ */
2164
+ function textTask(prompt) {
2165
+ if (prompt.length === 0) throw new Error("subagent-codex: the one-shot task must contain only text blocks");
2166
+ const texts = [];
2167
+ for (const block of prompt) {
2168
+ if (block.type !== "text") throw new Error("subagent-codex: the one-shot task must contain only text blocks");
2169
+ texts.push(block.text);
2170
+ }
2171
+ if (texts.every((text) => text.trim().length === 0)) throw new Error("subagent-codex: the one-shot task must not be empty");
2172
+ return texts.join("\n");
2173
+ }
2174
+ /** Count one tool call under the CLI's own name for the item. */
2175
+ function countToolCall(state, name) {
2176
+ state.toolCalls.set(name, (state.toolCalls.get(name) ?? 0) + 1);
2177
+ }
2178
+ /**
2179
+ * Fold a tool-call counter into the settled shape: `{count, byName}` with the
2180
+ * per-name values summing to `count`. An empty map yields undefined — a
2181
+ * stream that named no tool call reports nothing rather than a zero it did
2182
+ * not observe.
2183
+ * @param counts - the fold's per-name tally.
2184
+ * @returns the settled accounting, or undefined when nothing was counted.
2185
+ */
2186
+ function toolCallsOf(counts) {
2187
+ if (counts.size === 0) return void 0;
2188
+ let count = 0;
2189
+ const byName = {};
2190
+ for (const [name, value] of counts) {
2191
+ count += value;
2192
+ byName[name] = value;
2193
+ }
2194
+ return {
2195
+ count,
2196
+ byName
2197
+ };
2198
+ }
2199
+ /**
2200
+ * Fold one NDJSON line into the stream state. Shared by
2201
+ * {@link parseCodexJsonStream} (settle-time whole-stream parse) and
2202
+ * {@link CodexStreamParser} (live incremental parse) so the two paths cannot
2203
+ * drift.
2204
+ */
2205
+ function foldCodexStreamLine(state, raw) {
2206
+ const line = raw.trim();
2207
+ if (line === "") return;
2208
+ let event;
2209
+ try {
2210
+ event = JSON.parse(line);
2211
+ } catch {
2212
+ return;
2213
+ }
2214
+ if (event.type === "thread.started" && typeof event.thread_id === "string") {
2215
+ state.threadId = event.thread_id;
2216
+ if (typeof event.model === "string" && event.model !== "") state.model = event.model;
2217
+ return;
2218
+ }
2219
+ if (event.type === "turn.completed") {
2220
+ state.completed = true;
2221
+ if (event.usage !== void 0) state.usage = usageFromCodex(event.usage);
2222
+ if (typeof event.model === "string" && event.model !== "") state.model = event.model;
2223
+ return;
2224
+ }
2225
+ if (event.type !== "item.completed" || event.item === void 0) return;
2226
+ const item = event.item;
2227
+ if (item.type === "reasoning" && typeof item.text === "string" && item.text.trim() !== "") state.lines.push({
2228
+ kind: "think",
2229
+ text: item.text
2230
+ });
2231
+ else if (item.type === "agent_message" && typeof item.text === "string") {
2232
+ state.lines.push({
2233
+ kind: "text",
2234
+ text: item.text
2235
+ });
2236
+ state.text = item.text;
2237
+ } else if (item.type === "command_execution") {
2238
+ countToolCall(state, "command_execution");
2239
+ const command = typeof item.command === "string" ? item.command : void 0;
2240
+ const output = typeof item.aggregated_output === "string" && item.aggregated_output.trim() !== "" ? item.aggregated_output : void 0;
2241
+ if (command !== void 0 || output !== void 0) state.lines.push({
2242
+ kind: "tool",
2243
+ id: typeof item.id === "string" ? item.id : `codex-tool-${state.lines.length}`,
2244
+ name: "Bash",
2245
+ ...command === void 0 ? {} : { args: command },
2246
+ ...output === void 0 ? {} : { result: output }
2247
+ });
2248
+ } else if (item.type === "web_search_call") {
2249
+ countToolCall(state, "web_search_call");
2250
+ state.lines.push({
2251
+ kind: "tool",
2252
+ id: typeof item.id === "string" ? item.id : `codex-tool-${state.lines.length}`,
2253
+ name: "WebSearch"
2254
+ });
2255
+ } else if (item.type === "file_change") {
2256
+ countToolCall(state, "file_change");
2257
+ const args = (item.changes ?? []).map((change) => `${change.kind ?? "update"}: ${change.path ?? "?"}`).join("\n");
2258
+ if (args !== "") state.lines.push({
2259
+ kind: "tool",
2260
+ id: typeof item.id === "string" ? item.id : `codex-tool-${state.lines.length}`,
2261
+ name: "ApplyPatch",
2262
+ args
2263
+ });
2264
+ } else if (item.type === "function_call") {
2265
+ countToolCall(state, "function_call");
2266
+ state.lines.push({
2267
+ kind: "tool",
2268
+ id: typeof item.id === "string" ? item.id : `codex-tool-${state.lines.length}`,
2269
+ name: item.name ?? "function",
2270
+ ...typeof item.arguments === "string" && item.arguments !== "" ? { args: item.arguments } : {}
2271
+ });
2272
+ } else if (item.type === "function_call_output") {
2273
+ const output = typeof item.output === "string" ? item.output : void 0;
2274
+ if (output !== void 0 && output.trim() !== "") {
2275
+ const last = state.lines[state.lines.length - 1];
2276
+ if (last !== void 0 && last.kind === "tool") state.lines[state.lines.length - 1] = {
2277
+ ...last,
2278
+ result: last.result === void 0 ? output : `${last.result}\n${output}`
2279
+ };
2280
+ }
2281
+ }
2282
+ }
2283
+ /**
2284
+ * Incremental `codex exec --json` parser: feed stdout chunks as they arrive
2285
+ * and the folded transcript accumulates line by line. The last line is
2286
+ * volatile until the terminal `turn.completed` — a `function_call_output`
2287
+ * merges into a pending tool line — so live consumers hold it back (see the
2288
+ * run's live mirror).
2289
+ */
2290
+ var CodexStreamParser = class {
2291
+ buffer = "";
2292
+ lines = [];
2293
+ text;
2294
+ usage;
2295
+ threadId;
2296
+ model;
2297
+ completed = false;
2298
+ toolCalls = /* @__PURE__ */ new Map();
2299
+ /** Fold every complete NDJSON line in the chunk; the tail stays buffered. */
2300
+ push(chunk) {
2301
+ this.buffer += chunk;
2302
+ const parts = this.buffer.split("\n");
2303
+ this.buffer = parts.pop() ?? "";
2304
+ for (const raw of parts) foldCodexStreamLine(this, raw);
2305
+ }
2306
+ };
2307
+ /**
2308
+ * Parse a `codex exec --json` NDJSON event stream into an ordered transcript
2309
+ * (thinking, agent text, tool/command activity in event order), plus the final
2310
+ * answer text, token usage, the thread id a later resume round continues, and
2311
+ * the model identifier the stream named (usually absent — codex 0.144.0 does
2312
+ * not put one on the wire; the settle path then reads the rollout's
2313
+ * turn_context). Item types covered: `reasoning` (thinking), `agent_message`
2314
+ * (reply text), `command_execution` (shell command with aggregated output),
2315
+ * `web_search_call`, and `function_call_output`. The last `agent_message`
2316
+ * wins as the run output (the final answer), and `turn.completed` carries the
2317
+ * turn's usage. Malformed lines are skipped.
2318
+ * @param stream - the collected stdout NDJSON text.
2319
+ * @returns the ordered transcript, final answer text, usage, thread id, and
2320
+ * any stream-named model.
2321
+ */
2322
+ function parseCodexJsonStream(stream) {
2323
+ const state = {
2324
+ lines: [],
2325
+ text: void 0,
2326
+ usage: void 0,
2327
+ threadId: void 0,
2328
+ model: void 0,
2329
+ completed: false,
2330
+ toolCalls: /* @__PURE__ */ new Map()
2331
+ };
2332
+ for (const raw of stream.split("\n")) foldCodexStreamLine(state, raw);
2333
+ const toolCalls = toolCallsOf(state.toolCalls);
2334
+ return {
2335
+ lines: state.lines,
2336
+ ...state.text === void 0 ? {} : { text: state.text },
2337
+ ...state.usage === void 0 ? {} : { usage: state.usage },
2338
+ ...state.threadId === void 0 ? {} : { threadId: state.threadId },
2339
+ ...state.model === void 0 ? {} : { model: state.model },
2340
+ ...toolCalls === void 0 ? {} : { toolCalls }
2341
+ };
2342
+ }
2343
+ /**
2344
+ * Start the real `codex exec` child and publish its run. The codex
2345
+ * reply arrives as an NDJSON event stream on stdout (`--json`): the final
2346
+ * `agent_message` item is the run output and the `turn.completed` usage is
2347
+ * the token accounting. stderr is piped for diagnostics and never folded
2348
+ * into the run output. A resume round spawns `codex exec --json resume
2349
+ * <thread_id>` instead, continuing the same thread. The run id is the child
2350
+ * session id for a session-backed run.
2351
+ * @param request - resolved shared subagent request.
2352
+ * @param spec - workspace, environment, process service, and diagnostic policy.
2353
+ * @returns the published run after the child starts.
2354
+ */
2355
+ function startCodexCliRun(request, spec) {
2356
+ const task = textTask(request.prompt);
2357
+ if (request.signal.aborted) throw new Error("subagent-codex: request was aborted before the CLI started");
2358
+ const turn = spec.resume?.turn ?? 1;
2359
+ const memberArgv = spec.member === void 0 ? [] : ["-c", spec.member.configOverride];
2360
+ const modelArgv = [...spec.model === void 0 ? [] : ["-m", spec.model], ...spec.effort === void 0 ? [] : ["-c", `model_reasoning_effort=${JSON.stringify(spec.effort)}`]];
2361
+ const argv = spec.resume === void 0 ? [
2362
+ "codex",
2363
+ "exec",
2364
+ ...memberArgv,
2365
+ ...modelArgv,
2366
+ "--sandbox",
2367
+ spec.sandbox,
2368
+ "--skip-git-repo-check",
2369
+ "--json",
2370
+ task
2371
+ ] : [
2372
+ "codex",
2373
+ "exec",
2374
+ ...memberArgv,
2375
+ ...modelArgv,
2376
+ "--sandbox",
2377
+ spec.sandbox,
2378
+ "--skip-git-repo-check",
2379
+ "--json",
2380
+ "resume",
2381
+ spec.resume.cliSessionId,
2382
+ task
2383
+ ];
2384
+ const launch = spec.exec === void 0 ? {
2385
+ argv,
2386
+ env: spec.env
2387
+ } : containerExecSpawn(spec.exec, {
2388
+ argv,
2389
+ env: spec.env
2390
+ }, "subagent-codex");
2391
+ const child = spec.spawn({
2392
+ argv: launch.argv,
2393
+ cwd: spec.cwd,
2394
+ stdio: {
2395
+ stdin: "ignore",
2396
+ stdout: "pipe",
2397
+ stderr: "pipe"
2398
+ },
2399
+ graceMs: spec.disposeGraceMs,
2400
+ env: launch.env
2401
+ });
2402
+ const startedAtMs = Date.now();
2403
+ spec.childSession?.append("turn/start", { turn });
2404
+ const liveMirror = spec.childSession !== void 0 && spec.ctx !== void 0 ? createCodexLiveMirror(spec, task, turn) : void 0;
2405
+ let output = "";
2406
+ child.stdout?.on("data", (chunk) => {
2407
+ const text = chunk.toString();
2408
+ output += text;
2409
+ liveMirror?.push(text);
2410
+ });
2411
+ let stderr = "";
2412
+ child.stderr?.on("data", (chunk) => {
2413
+ stderr += chunk.toString();
2414
+ });
2415
+ const disposeProcess = async () => {
2416
+ child.terminate();
2417
+ await child.waitForExit().catch(() => false);
2418
+ await child.done.catch(() => {});
2419
+ };
2420
+ const runAbort = new AbortController();
2421
+ const requestCancel = () => {
2422
+ if (runAbort.signal.aborted) return;
2423
+ runAbort.abort(/* @__PURE__ */ new Error("subagent-codex: run cancelled locally"));
2424
+ };
2425
+ const onAbort = () => {
2426
+ requestCancel();
2427
+ };
2428
+ request.signal.addEventListener("abort", onAbort, { once: true });
2429
+ const abortBranch = new Promise((_, reject) => {
2430
+ runAbort.signal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("subagent-codex: run cancelled locally")), { once: true });
2431
+ });
2432
+ const processFailure = child.done.then((outcome) => Promise.reject(/* @__PURE__ */ new Error(`subagent-codex: CLI exited before the run settled (code ${String(outcome.exitCode)}, signal ${String(outcome.signal)})`)), (error) => Promise.reject(thrown(error)));
2433
+ processFailure.catch(() => {});
2434
+ const collectOutput = () => {
2435
+ const drained = child.collected.stdout?.readFrom(0).text;
2436
+ const text = parseCodexJsonStream(drained !== void 0 && drained !== "" ? drained : output).text?.trim();
2437
+ return text === void 0 || text === "" ? [] : [{
2438
+ type: "text",
2439
+ text
2440
+ }];
2441
+ };
2442
+ let exitCode = null;
2443
+ /** The flattened failure's own message, snapshotted for the diagnostic. */
2444
+ let settleFailure;
2445
+ const result = settleRunResult({
2446
+ attempt: () => Promise.race([
2447
+ child.done.then((outcome) => {
2448
+ if (outcome.exitCode !== 0) {
2449
+ const via = spec.endpointLabel ?? "codex default endpoint";
2450
+ exitCode = outcome.exitCode;
2451
+ throw new Error(`subagent-codex: codex exec exited with code ${String(outcome.exitCode)} via ${via}`);
2452
+ }
2453
+ if (parseCodexJsonStream(output).lines.length === 0) {
2454
+ spec.onError?.(/* @__PURE__ */ new Error("subagent-codex: exited 0 but parsed no codex events — check the codex --json event schema"), "error");
2455
+ throw new Error("subagent-codex: codex exec exited 0 but produced no answer");
2456
+ }
2457
+ const answer = collectOutput();
2458
+ if (answer.length === 0) throw new Error("subagent-codex: codex exec exited 0 but produced no answer");
2459
+ return {
2460
+ output: answer,
2461
+ stopReason: "completed"
2462
+ };
2463
+ }),
2464
+ processFailure,
2465
+ abortBranch
2466
+ ]),
2467
+ collectOutput,
2468
+ collectDiagnostic: () => {
2469
+ const detail = [];
2470
+ if (settleFailure !== void 0) detail.push(settleFailure);
2471
+ const stderrTail = (child.collected.stderr?.readFrom(0).text || stderr).trim();
2472
+ if (stderrTail !== "") detail.push(`stderr: ${stderrTail.slice(-500)}`);
2473
+ const homeDir = spec.env["CODEX_HOME"];
2474
+ if (homeDir !== void 0 && homeDir !== "") {
2475
+ const threadId = parseCodexJsonStream(output).threadId;
2476
+ const when = new Date(startedAtMs);
2477
+ const dir = `${homeDir}/sessions/${String(when.getFullYear())}/${String(when.getMonth() + 1).padStart(2, "0")}/${String(when.getDate()).padStart(2, "0")}`;
2478
+ detail.push(threadId === void 0 ? `rollout 目录: ${dir}` : `rollout: ${dir}/rollout-*-${threadId}.jsonl`);
2479
+ }
2480
+ return detail.length === 0 ? void 0 : detail.join("\n");
2481
+ },
2482
+ cancelled: () => runAbort.signal.aborted,
2483
+ onError: (error, stopReason) => {
2484
+ settleFailure = error instanceof Error ? error.message : String(error);
2485
+ spec.onError?.(error instanceof Error ? error : new Error(String(error)), stopReason);
2486
+ },
2487
+ signal: request.signal,
2488
+ onAbort
2489
+ }).then((settled) => {
2490
+ if (settled.stopReason === "completed") spec.onAuthSuccess?.();
2491
+ if (spec.childSession !== void 0) {
2492
+ if (settled.stopReason === "completed") spec.childSession.append("turn/end", {
2493
+ turn,
2494
+ reason: { kind: "completed" }
2495
+ });
2496
+ else if (settled.stopReason === "aborted") spec.childSession.append("turn/end", {
2497
+ turn,
2498
+ reason: {
2499
+ kind: "aborted",
2500
+ reason: { kind: "parent" }
2501
+ }
2502
+ });
2503
+ else spec.childSession.append("turn/end", {
2504
+ turn,
2505
+ reason: {
2506
+ kind: "error",
2507
+ error: {
2508
+ message: `codex exec exited with code ${String(exitCode)}`,
2509
+ code: "UNKNOWN"
2510
+ }
2511
+ }
2512
+ });
2513
+ }
2514
+ return settled;
2515
+ });
2516
+ result.then(() => child.done).then(() => {
2517
+ if (spec.onAuthFailure !== void 0) {
2518
+ const authDetail = `${child.collected.stderr?.readFrom(0).text || stderr}\n${child.collected.stdout?.readFrom(0).text || output}`;
2519
+ if (CODEX_AUTH_FAILURE.test(authDetail)) spec.onAuthFailure(authDetail.split("\n").find((line) => line.trim() !== "") ?? "auth failure");
2520
+ }
2521
+ return mirrorCodexAfterExit(spec, task, turn, output, liveMirror, startedAtMs);
2522
+ }, () => {});
2523
+ return Promise.resolve(subprocessRunHandle({
2524
+ id: spec.childSession?.id ?? SessionId(randomUUID()),
2525
+ result,
2526
+ signal: request.signal,
2527
+ onAbort,
2528
+ requestCancel,
2529
+ teardown: disposeProcess
2530
+ }));
2531
+ }
2532
+ /** One assistant-role message event, attributed to the codex route. */
2533
+ function codexAssistantEvent(blocks) {
2534
+ return createAssistantMessage({
2535
+ content: blocks,
2536
+ source: {
2537
+ provider: "codex-local",
2538
+ model: "codex"
2539
+ }
2540
+ });
2541
+ }
2542
+ /**
2543
+ * Fold one transcript line into the child session as one assistant step,
2544
+ * wrapped in the step/start–step/end boundary pair the live conversation
2545
+ * assembler requires: without a boundary the step never enters the location
2546
+ * index, the line's assistant message resolves to a turn-level location, and
2547
+ * the real-time view renders nothing (only a full rebuild, which mints step
2548
+ * drafts from the explicit coordinates, recovers it). Tool cards escape that
2549
+ * fate only because their definition does not consult the step location.
2550
+ * @returns whether the line folded (false when the options skipped it).
2551
+ */
2552
+ function appendCodexTranscriptLine(childSession, turn, step, line, usage) {
2553
+ childSession.append("step/start", {
2554
+ turn,
2555
+ step
2556
+ });
2557
+ if (line.kind === "tool") {
2558
+ const call = childSession.append("tool/call", {
2559
+ turn,
2560
+ step,
2561
+ callId: line.id,
2562
+ name: line.name,
2563
+ arguments: line.args ?? ""
2564
+ });
2565
+ if (line.result !== void 0) childSession.append("tool/result", {
2566
+ turn,
2567
+ step,
2568
+ message: createToolResultMessage({
2569
+ callId: line.id,
2570
+ content: [{
2571
+ type: "text",
2572
+ text: line.result
2573
+ }],
2574
+ isError: false
2575
+ })
2576
+ }, {
2577
+ surfaceOp: "append",
2578
+ sourceEventSeqs: [call.seq]
2579
+ });
2580
+ childSession.append("step/end", {
2581
+ turn,
2582
+ step
2583
+ });
2584
+ return true;
2585
+ }
2586
+ const blocks = line.kind === "think" ? [{
2587
+ type: "reasoning",
2588
+ text: line.text
2589
+ }] : [{
2590
+ type: "text",
2591
+ text: line.text
2592
+ }];
2593
+ childSession.append("assistant/message", {
2594
+ turn,
2595
+ step,
2596
+ message: codexAssistantEvent(blocks),
2597
+ stream: [],
2598
+ ...usage === void 0 ? {} : { usage }
2599
+ }, { surfaceOp: "append" });
2600
+ childSession.append("step/end", {
2601
+ turn,
2602
+ step
2603
+ });
2604
+ return true;
2605
+ }
2606
+ /** Fold one transcript line into the run's child session as one assistant step. */
2607
+ function appendCodexLine(spec, turn, step, line, usage) {
2608
+ if (spec.childSession === void 0) return;
2609
+ appendCodexTranscriptLine(spec.childSession, turn, step, line, usage);
2610
+ }
2611
+ /** The delta-progress text for one transcript line. */
2612
+ function codexLineText(line) {
2613
+ return line.kind === "tool" ? `[工具 ${line.name}]${line.args !== void 0 ? ` ${line.args}` : ""}${line.result !== void 0 ? `\n${line.result}` : ""}` : line.text;
2614
+ }
2615
+ /** Create the per-run live mirror over the incremental parser. */
2616
+ function createCodexLiveMirror(spec, task, turn) {
2617
+ const parser = new CodexStreamParser();
2618
+ const childSession = spec.childSession;
2619
+ const ctx = spec.ctx;
2620
+ let mirrored = 0;
2621
+ let userMirrored = false;
2622
+ let queue = Promise.resolve();
2623
+ const mirror = {
2624
+ get mirroredLines() {
2625
+ return mirrored;
2626
+ },
2627
+ get userMirrored() {
2628
+ return userMirrored;
2629
+ },
2630
+ enqueue(task) {
2631
+ queue = queue.then(task);
2632
+ return queue;
2633
+ },
2634
+ push(chunk) {
2635
+ parser.push(chunk);
2636
+ const upto = parser.completed ? parser.lines.length : parser.lines.length - 1;
2637
+ if (upto <= mirrored) return;
2638
+ mirror.enqueue(async () => {
2639
+ try {
2640
+ const localAgent = ctx.get("localAgent");
2641
+ if (!userMirrored) {
2642
+ userMirrored = true;
2643
+ childSession.append("user/message", createUserMessage({
2644
+ content: [{
2645
+ type: "text",
2646
+ text: task
2647
+ }],
2648
+ source: { kind: "user" }
2649
+ }), { surfaceOp: "append" });
2650
+ localAgent?.reportRunProgress(childSession.id, {
2651
+ kind: "delta",
2652
+ text: task
2653
+ });
2654
+ }
2655
+ let carrier = -1;
2656
+ if (parser.completed && parser.usage !== void 0) {
2657
+ for (let scan = 0; scan < parser.lines.length; scan += 1) if (parser.lines[scan]?.kind !== "tool") carrier = scan;
2658
+ }
2659
+ for (let index = mirrored; index < upto; index += 1) {
2660
+ const line = parser.lines[index];
2661
+ if (line === void 0) continue;
2662
+ const usage = index === carrier ? parser.usage : void 0;
2663
+ appendCodexLine(spec, turn, index + 1, line, usage);
2664
+ mirrored = index + 1;
2665
+ localAgent?.reportRunProgress(childSession.id, {
2666
+ kind: "delta",
2667
+ text: codexLineText(line)
2668
+ });
2669
+ }
2670
+ await persistChildSession(ctx, childSession);
2671
+ localAgent?.reportRunProgress(childSession.id, {
2672
+ kind: "mirror",
2673
+ mirroredLines: mirrored
2674
+ });
2675
+ } catch (error) {
2676
+ ctx.logger.warn(`subagent-codex: live mirror failed: ${thrown(error).message}`);
2677
+ }
2678
+ });
2679
+ }
2680
+ };
2681
+ return mirror;
2682
+ }
2683
+ /**
2684
+ * Mirror the delegation's user prompt and the codex event transcript into the
2685
+ * dsh subagent session, then persist. Runs detached from the settle race — the
2686
+ * run result settles with the child exit, and a failure here is
2687
+ * diagnostic-only (the subagent record degrades to a final-text-only view).
2688
+ * The transcript's reasoning folds to `reasoning` blocks, agent text and tool
2689
+ * activity to text blocks (tool lines render `[工具 Bash] <command> → output`),
2690
+ * and the final assistant message carries the round's usage when codex
2691
+ * reported it, so the tokenUsage projection counts the delegation.
2692
+ * @param spec - the run spec carrying the child session and host context.
2693
+ * @param task - the one-shot task text (the user prompt).
2694
+ * @param turn - the round's turn number (1 for a fresh round, incremented on resume).
2695
+ * @param parsed - the parsed NDJSON transcript, final output, and usage.
2696
+ * @param fromLines - transcript lines the live mirror already appended.
2697
+ * @param userMirrored - whether the live mirror already appended the prompt.
2698
+ */
2699
+ async function appendCodexResponse(spec, task, turn, parsed, fromLines = 0, userMirrored = false) {
2700
+ if (spec.childSession === void 0 || spec.ctx === void 0) return;
2701
+ const childSession = spec.childSession;
2702
+ const localAgent = spec.ctx.get("localAgent");
2703
+ if (!userMirrored) {
2704
+ childSession.append("user/message", createUserMessage({
2705
+ content: [{
2706
+ type: "text",
2707
+ text: task
2708
+ }],
2709
+ source: { kind: "user" }
2710
+ }), { surfaceOp: "append" });
2711
+ localAgent?.reportRunProgress(childSession.id, {
2712
+ kind: "delta",
2713
+ text: task
2714
+ });
2715
+ }
2716
+ let usageIndex = -1;
2717
+ for (let index = 0; index < parsed.lines.length; index += 1) if (parsed.lines[index]?.kind !== "tool") usageIndex = index;
2718
+ let step = fromLines + 1;
2719
+ for (const line of parsed.lines.slice(fromLines)) {
2720
+ appendCodexLine(spec, turn, step, line, step - 1 === usageIndex ? parsed.usage : void 0);
2721
+ localAgent?.reportRunProgress(childSession.id, {
2722
+ kind: "delta",
2723
+ text: codexLineText(line)
2724
+ });
2725
+ step += 1;
2726
+ }
2727
+ if (parsed.usage !== void 0 && usageIndex !== -1 && usageIndex < fromLines) spec.ctx?.logger.warn(`subagent-codex: round ${turn} usage arrived after its carrier line was mirrored; the accounting is dropped`);
2728
+ await persistChildSession(spec.ctx, childSession);
2729
+ localAgent?.reportRunProgress(childSession.id, {
2730
+ kind: "mirror",
2731
+ mirroredLines: parsed.lines.length
2732
+ });
2733
+ }
2734
+ /**
2735
+ * Mirror the codex NDJSON stream into the child session AFTER the CLI process
2736
+ * has exited, whatever its stop reason. A cancelled or failed round still
2737
+ * preserves the events the stream already emitted (reasoning, commands,
2738
+ * replies, usage) instead of leaving the child blank — the run result settles
2739
+ * 'aborted'/'error' at the cancel moment, but codex may have produced content
2740
+ * before the kill landed. Also records the thread id (fresh rounds) so a
2741
+ * later resume can continue the partial thread.
2742
+ *
2743
+ * **Usage recovery**: a non-completed terminal state never emits
2744
+ * `turn.completed`, so the parsed stream carries no usage even though codex
2745
+ * wrote the run's real token spend to its rollout file. When the stream's
2746
+ * usage is absent, the mirror falls back to the run's rollout file — located
2747
+ * by the thread id (the `session_meta` head id) or by the spawn-time window —
2748
+ * and attaches the file's LAST `token_count` entry (same
2749
+ * `input − cached` caliber as `turn.completed`, via the shared
2750
+ * {@link usageFromCodex}), so a killed or failed run still books its tokens.
2751
+ * The fallback is best-effort and silent: no rollout file, an unreadable
2752
+ * home, or a hard kill that wrote no token_count leaves the child without
2753
+ * usage, exactly as before.
2754
+ *
2755
+ * **Model observation**: the exec --json wire carries no model field (codex
2756
+ * 0.144.0), so the run's model identifier is read from the located rollout
2757
+ * file's LAST `turn_context` line whose timestamp falls inside this run's
2758
+ * window — the turn codex actually started for this round. When the stream
2759
+ * itself names a model (a future CLI), the stream wins. Absent on any miss;
2760
+ * the observation rides {@link CodexCliRunSpec.onRoundSettled} along with the
2761
+ * round's usage.
2762
+ * @param spec - the run spec carrying the child session and host context.
2763
+ * @param task - the one-shot task text (the user prompt).
2764
+ * @param turn - the round's turn number.
2765
+ * @param output - the collected NDJSON stdout.
2766
+ * @param startedAtMs - the spawn moment, anchoring the rollout time window.
2767
+ */
2768
+ async function mirrorCodexAfterExit(spec, task, turn, output, live, startedAtMs) {
2769
+ if (spec.childSession === void 0 || spec.ctx === void 0) return;
2770
+ const work = async () => {
2771
+ const parsed = parseCodexJsonStream(output);
2772
+ if (spec.resume === void 0) spec.onThreadId?.(parsed.threadId);
2773
+ const fromLines = live?.mirroredLines ?? 0;
2774
+ const userMirrored = live?.userMirrored ?? false;
2775
+ let usage = parsed.usage;
2776
+ let observedModel = parsed.model;
2777
+ let cliVersion;
2778
+ const homeDir = spec.env["CODEX_HOME"];
2779
+ if (homeDir !== void 0 && homeDir !== "") {
2780
+ const facts = await codexRolloutRoundFacts(homeDir, {
2781
+ threadId: parsed.threadId,
2782
+ windowStart: startedAtMs,
2783
+ cwd: spec.cwd
2784
+ });
2785
+ usage ??= facts.usage;
2786
+ observedModel ??= facts.model;
2787
+ cliVersion = facts.cliVersion;
2788
+ }
2789
+ cliVersion ??= await spec.cliVersion?.();
2790
+ spec.onRoundSettled?.({
2791
+ ...observedModel === void 0 ? {} : { observedModel },
2792
+ ...cliVersion === void 0 ? {} : { cliVersion },
2793
+ ...usage === void 0 ? {} : { usage },
2794
+ ...parsed.toolCalls === void 0 ? {} : { toolCalls: parsed.toolCalls }
2795
+ });
2796
+ if (parsed.lines.length === 0 && !userMirrored) return;
2797
+ await appendCodexResponse(spec, task, turn, {
2798
+ lines: parsed.lines,
2799
+ output: collectOutputBlocks(parsed.text),
2800
+ ...usage === void 0 ? {} : { usage }
2801
+ }, fromLines, userMirrored);
2802
+ };
2803
+ try {
2804
+ if (live === void 0) await work();
2805
+ else await live.enqueue(work);
2806
+ } catch (error) {
2807
+ spec.onError?.(thrown(error), "error");
2808
+ }
2809
+ }
2810
+ /** Build the run-output blocks from the final codex answer text. */
2811
+ function collectOutputBlocks(text) {
2812
+ const trimmed = text?.trim();
2813
+ return trimmed === void 0 || trimmed === "" ? [] : [{
2814
+ type: "text",
2815
+ text: trimmed
2816
+ }];
2817
+ }
2818
+ //#endregion
2819
+ //#region lib/types/live-switch.js
2820
+ /**
2821
+ * The settings-driven live-driver switch: owns the active driver generation
2822
+ * and retires old ones on a settings change, without a plugin reload.
2823
+ *
2824
+ * Toggle OFF (or unload): the retiring generation is DRAINED, not killed —
2825
+ * new rounds are refused (the provider's catch falls back to exec), in-flight
2826
+ * rounds finish on their runtime, idle runtimes are reclaimed at once.
2827
+ * Toggling ON builds the next generation lazily (no process until the first
2828
+ * round). Legacy granularity settings are ignored.
2829
+ *
2830
+ * @module @khorsheed/dsh-local-agent-codex — internal, unit-tested directly.
2831
+ */
2832
+ var LiveDriverSwitch = class {
2833
+ ctx;
2834
+ options;
2835
+ liveOn = false;
2836
+ active;
2837
+ /** Retiring generations still draining their in-flight rounds. */
2838
+ draining = /* @__PURE__ */ new Set();
2839
+ unwatch;
2840
+ constructor(ctx, face, options) {
2841
+ this.ctx = ctx;
2842
+ this.options = options;
2843
+ this.apply(face.get());
2844
+ this.unwatch = face.watch((next) => {
2845
+ this.apply(next);
2846
+ });
2847
+ }
2848
+ /**
2849
+ * Resolve the live driver for one round's member (the provider's call
2850
+ * site). A retiring generation still hosting the member gates the new one —
2851
+ * the round falls back to exec rather than double-loading the same codex
2852
+ * thread into two processes.
2853
+ */
2854
+ resolve = (childSessionId) => {
2855
+ if (this.active === void 0) return void 0;
2856
+ for (const retiring of this.draining) if (retiring.hasRuntime(childSessionId)) return void 0;
2857
+ return this.active;
2858
+ };
2859
+ /**
2860
+ * The model the member's live runtime bound at spawn (the broker's switch
2861
+ * check): the identifier, undefined for "bound to no model", null for "no
2862
+ * live runtime". Consults only the ACTIVE generation — a draining one is on
2863
+ * its way out anyway.
2864
+ */
2865
+ boundModel(childSessionId) {
2866
+ return this.active?.boundModelOf(childSessionId) ?? null;
2867
+ }
2868
+ /**
2869
+ * Retire the member's resident runtime in the active generation so the next
2870
+ * round respawns onto the member's new effective model (the composer's
2871
+ * model switch path; the CLI session itself carries over via thread/resume).
2872
+ */
2873
+ async retireRuntime(childSessionId) {
2874
+ await this.active?.retireRuntime(childSessionId);
2875
+ }
2876
+ /** Mirror the resolved settings into the driver generation. */
2877
+ apply(next) {
2878
+ if (next.live === this.liveOn) return;
2879
+ this.liveOn = next.live;
2880
+ const retiring = this.active;
2881
+ this.active = next.live ? new CodexLiveDriver(this.ctx, {
2882
+ ...this.options.sandbox === void 0 ? {} : { sandbox: this.options.sandbox },
2883
+ ...this.options.liveIdleMs === void 0 ? {} : { liveIdleMs: this.options.liveIdleMs },
2884
+ ...this.options.model === void 0 ? {} : { model: this.options.model }
2885
+ }) : void 0;
2886
+ if (retiring !== void 0) {
2887
+ this.draining.add(retiring);
2888
+ retiring.drain().then(() => {
2889
+ this.draining.delete(retiring);
2890
+ }, (error) => {
2891
+ this.draining.delete(retiring);
2892
+ this.ctx.logger.warn(`local-agent-codex: live driver drain failed: ${error instanceof Error ? error.message : String(error)}`);
2893
+ });
2894
+ }
2895
+ }
2896
+ /** Plugin unload: interrupt whatever survives (disposeAll, not drain). */
2897
+ dispose() {
2898
+ this.unwatch();
2899
+ this.active?.disposeAll();
2900
+ for (const retiring of this.draining) retiring.disposeAll();
2901
+ this.draining.clear();
2902
+ }
2903
+ };
2904
+ //#endregion
2905
+ //#region lib/types/model-broker.js
2906
+ /**
2907
+ * The codex harness's model broker: the member-aware model surface the core
2908
+ * gateway routes to (`harnessModel` for the settings card, `memberModel` /
2909
+ * `setMemberModel` for the composer picker). It owns the session-level
2910
+ * override map — shared with the exec provider (which consults it per round)
2911
+ * and the live driver (which binds it at runtime spawn) — and retires a
2912
+ * member's resident runtime when a switch leaves it bound to a different
2913
+ * model, so the NEXT round respawns onto the new one while the codex thread
2914
+ * itself carries over via thread/resume.
2915
+ *
2916
+ * Resolution order is the family's fixed one (first match wins): override →
2917
+ * the delegation's recorded model → the plugin-config `model` (settings) →
2918
+ * the scoped config.toml's own `model` (cliDefault) → the account catalog's
2919
+ * `isDefault` slug (catalogDefault) → the CLI's built-in default, which names
2920
+ * nothing. The catalogDefault layer is the ONE case source `cli-builtin`
2921
+ * carries an `effective` model: the app-server probe marks the account's
2922
+ * compiled default with `isDefault`, so "follow the CLI default" finally has
2923
+ * a name. The `cliDefault` FIELD stays scoped-config-only — the two layers
2924
+ * must stay distinguishable (the file is user-editable, the catalog answer is
2925
+ * the account's). The pickable vocabulary is what the instance actually
2926
+ * knows — settings + cliDefault + the identifiers the scoped config itself
2927
+ * carries (profiles) + the app-server `model/list` account catalog (probed,
2928
+ * cached, best-effort — see model-catalog.ts) + the card's recent-model
2929
+ * memory — never a hardcoded catalog.
2930
+ * @module @khorsheed/dsh-local-agent-codex/model-broker
2931
+ */
2932
+ /** Dedupe the choice vocabulary, dropping unset layers and blanks. */
2933
+ function dedupeChoices(layers) {
2934
+ const seen = /* @__PURE__ */ new Set();
2935
+ const choices = [];
2936
+ for (const layer of layers) {
2937
+ const value = layer?.trim();
2938
+ if (value === void 0 || value === "" || seen.has(value)) continue;
2939
+ seen.add(value);
2940
+ choices.push(value);
2941
+ }
2942
+ return choices;
2943
+ }
2944
+ var CodexModelBroker = class {
2945
+ deps;
2946
+ constructor(deps) {
2947
+ this.deps = deps;
2948
+ }
2949
+ configurationAdapter(binding) {
2950
+ const home = this.deps.ctx.localAgent.homeDir("codex", binding.scope);
2951
+ const resolve = async (selection) => {
2952
+ let directory = this.deps.directory?.(home, binding.cwd);
2953
+ if (directory?.status === "loading") {
2954
+ await this.deps.refreshDirectory?.(home, binding.cwd);
2955
+ directory = this.deps.directory?.(home, binding.cwd);
2956
+ }
2957
+ const model = selection.model.mode === "value" ? selection.model.value : (selection.model.mode === "inherit" ? binding.model : void 0) ?? this.deps.settingsModel() ?? await readCodexModel(home) ?? this.deps.catalogDefault(home, binding.cwd);
2958
+ const entry = directory?.entries.find((entry) => entry.value === model);
2959
+ const explicitEffort = selection.effort.mode === "value" ? selection.effort.value : selection.effort.mode === "inherit" ? binding.effort : void 0;
2960
+ const effort = explicitEffort ?? await readCodexReasoningEffort(home) ?? entry?.reasoning?.default;
2961
+ if (explicitEffort !== void 0 && !entry?.reasoning?.options.some((option) => option.value === explicitEffort)) throw new Error("Codex has not advertised this reasoning effort for the selected model; refresh its native directory");
2962
+ return {
2963
+ ...model === void 0 ? {} : { model },
2964
+ ...effort === void 0 ? {} : { effort }
2965
+ };
2966
+ };
2967
+ return {
2968
+ validate: async (selection) => {
2969
+ await resolve(selection);
2970
+ },
2971
+ prepare: resolve,
2972
+ apply: async (selection) => {
2973
+ const resolved = await resolve(selection);
2974
+ await this.deps.retireRuntime(binding.childSessionId);
2975
+ return resolved;
2976
+ },
2977
+ reconcile: async (state) => {
2978
+ if (this.activeDelegations().includes(binding.childSessionId)) return {
2979
+ active: true,
2980
+ matches: "unknown",
2981
+ resolved: {}
2982
+ };
2983
+ await this.deps.retireRuntime(binding.childSessionId);
2984
+ return {
2985
+ active: false,
2986
+ matches: "current",
2987
+ resolved: await resolve(state.current.selection)
2988
+ };
2989
+ }
2990
+ };
2991
+ }
2992
+ async modelDirectory(childSessionId, refresh = false) {
2993
+ const home = this.deps.homeDir(childSessionId);
2994
+ if (refresh) await this.deps.refreshDirectory?.(home, this.deps.cwd?.(childSessionId));
2995
+ return (await this.modelInfo(childSessionId, childSessionId === void 0 ? void 0 : this.deps.ctx.localAgent.getDelegation(childSessionId)?.model)).directory ?? {
2996
+ entries: [],
2997
+ complete: false,
2998
+ customInput: true,
2999
+ status: "unsupported",
3000
+ refreshing: false,
3001
+ revision: 0
3002
+ };
3003
+ }
3004
+ async *followModelDirectory(childSessionId, signal) {
3005
+ if (this.deps.followDirectory === void 0) {
3006
+ yield await this.modelDirectory(childSessionId);
3007
+ return;
3008
+ }
3009
+ for await (const _snapshot of this.deps.followDirectory(this.deps.homeDir(childSessionId), this.deps.cwd?.(childSessionId), signal)) yield await this.modelDirectory(childSessionId);
3010
+ }
3011
+ /** The member's in-flight rounds (an empty answer on a core that predates the read). */
3012
+ activeDelegations() {
3013
+ const registry = this.deps.ctx.localAgent;
3014
+ return typeof registry.activeDelegations === "function" ? registry.activeDelegations() : [];
3015
+ }
3016
+ /** The model the member's NEXT spawn binds explicitly: override → recorded → settings. */
3017
+ boundLayer(childSessionId) {
3018
+ const override = this.deps.overrides.get(childSessionId);
3019
+ if (override !== void 0) return override;
3020
+ const recorded = this.deps.ctx.localAgent.getDelegation(childSessionId)?.model?.trim();
3021
+ if (recorded !== void 0 && recorded !== "") return recorded;
3022
+ return this.deps.settingsModel();
3023
+ }
3024
+ /**
3025
+ * Read the model surface. The cliDefault and the discovered identifiers come
3026
+ * from the scoped config.toml LIVE (a person-edited file is authoritative);
3027
+ * a read failure degrades that layer to absent, never to a guessed value.
3028
+ * The account catalog comes from the probe cache SYNCHRONOUSLY (a cold
3029
+ * cache reads empty and re-probes in the background — the settings card
3030
+ * re-fetches on open, so it never blocks on a CLI boot).
3031
+ */
3032
+ async modelInfo(childSessionId, delegationModel) {
3033
+ const settings = this.deps.settingsModel();
3034
+ const homeDir = this.deps.homeDir(childSessionId);
3035
+ const [cliDefault, discovered] = await Promise.all([readCodexModel(homeDir).catch(() => void 0), listCodexConfigModels(homeDir).catch(() => [])]);
3036
+ const override = childSessionId === void 0 ? void 0 : this.deps.overrides.get(childSessionId);
3037
+ const delegation = delegationModel?.trim() === "" ? void 0 : delegationModel;
3038
+ const catalogDefaultRaw = this.deps.catalogDefault(homeDir, this.deps.cwd?.(childSessionId))?.trim();
3039
+ const effective = override ?? delegation ?? settings ?? cliDefault ?? (catalogDefaultRaw === void 0 || catalogDefaultRaw === "" ? void 0 : catalogDefaultRaw);
3040
+ const source = override !== void 0 ? "override" : delegation !== void 0 ? "delegation" : settings !== void 0 ? "settings" : cliDefault !== void 0 ? "cli-config" : "cli-builtin";
3041
+ const inFlight = childSessionId !== void 0 && this.activeDelegations().includes(childSessionId);
3042
+ return {
3043
+ ...effective === void 0 ? {} : { effective },
3044
+ source,
3045
+ ...override === void 0 ? {} : { override },
3046
+ ...delegation === void 0 ? {} : { delegation },
3047
+ ...settings === void 0 ? {} : { settings },
3048
+ ...cliDefault === void 0 ? {} : { cliDefault },
3049
+ ...this.deps.directory === void 0 ? {} : { directory: extendModelDirectory(this.deps.directory(homeDir, this.deps.cwd?.(childSessionId)), [
3050
+ settings,
3051
+ cliDefault,
3052
+ ...discovered
3053
+ ], this.deps.recentModels()) },
3054
+ choices: dedupeChoices([
3055
+ settings,
3056
+ cliDefault,
3057
+ ...discovered,
3058
+ ...this.deps.catalog(homeDir, this.deps.cwd?.(childSessionId)),
3059
+ ...this.deps.recentModels()
3060
+ ]),
3061
+ live: this.deps.live(),
3062
+ switchable: !inFlight,
3063
+ ...inFlight ? { reason: "成员有进行中的委派轮次,等其完成后再切换模型" } : {}
3064
+ };
3065
+ }
3066
+ /**
3067
+ * Set (or clear) the member's session-level override. Same-model is a
3068
+ * no-op; a different model with a live runtime bound elsewhere retires the
3069
+ * runtime so the next round respawns (the codex thread resumes). Throws
3070
+ * while a round is in flight — retiring mid-round would kill the run.
3071
+ */
3072
+ async setMemberModel(childSessionId, model) {
3073
+ if (typeof this.deps.ctx.localAgent.setMemberModel === "function") return this.deps.ctx.localAgent.setMemberModel(childSessionId, model);
3074
+ if (this.activeDelegations().includes(childSessionId)) throw new Error(`subagent-codex: 成员有进行中的委派轮次,等其完成后再切换模型 (child session ${childSessionId})`);
3075
+ const trimmed = model?.trim();
3076
+ const next = trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
3077
+ if (this.deps.overrides.get(childSessionId) === next) return;
3078
+ if (next === void 0) this.deps.overrides.delete(childSessionId);
3079
+ else this.deps.overrides.set(childSessionId, next);
3080
+ const bound = this.deps.liveBoundModel(childSessionId);
3081
+ if (bound !== null && bound !== this.boundLayer(childSessionId)) await this.deps.retireRuntime(childSessionId);
3082
+ }
3083
+ };
3084
+ /** Grace between stdin EOF and terminate when the probe tears the process down. */
3085
+ const PROBE_EOF_GRACE_MS = 1e3;
3086
+ /** Preserve native metadata, including hidden candidates; reject malformed pages. */
3087
+ function catalogEntries(result) {
3088
+ const page = result;
3089
+ if (!Array.isArray(page?.data)) throw new Error("model/list returned no model array");
3090
+ const entries = [];
3091
+ let defaultModel;
3092
+ for (const entry of page.data) {
3093
+ if (typeof entry !== "object" || entry === null) continue;
3094
+ const record = entry;
3095
+ const raw = typeof record["id"] === "string" ? record["id"] : record["model"];
3096
+ const value = typeof raw === "string" ? raw.trim() : "";
3097
+ if (value === "") continue;
3098
+ if (record["isDefault"] === true && defaultModel === void 0) defaultModel = value;
3099
+ const nativeEfforts = record["supportedReasoningEfforts"];
3100
+ const options = Array.isArray(nativeEfforts) ? nativeEfforts.flatMap((option) => {
3101
+ if (typeof option !== "object" || option === null) return [];
3102
+ const effort = option;
3103
+ const choice = effort["reasoningEffort"];
3104
+ if (typeof choice !== "string" || choice === "") return [];
3105
+ return [{
3106
+ value: choice,
3107
+ label: choice,
3108
+ ...typeof effort["description"] === "string" ? { description: effort["description"] } : {}
3109
+ }];
3110
+ }) : void 0;
3111
+ const defaultEffort = record["defaultReasoningEffort"];
3112
+ entries.push({
3113
+ value,
3114
+ label: typeof record["displayName"] === "string" ? record["displayName"] : value,
3115
+ source: "native",
3116
+ hidden: record["hidden"] === true,
3117
+ ...typeof record["description"] === "string" ? { description: record["description"] } : {},
3118
+ ...options === void 0 ? {} : { reasoning: {
3119
+ options,
3120
+ ...typeof defaultEffort === "string" ? { default: defaultEffort } : {}
3121
+ } }
3122
+ });
3123
+ }
3124
+ return {
3125
+ entries,
3126
+ ...defaultModel === void 0 ? {} : { defaultModel },
3127
+ ...typeof page.nextCursor === "string" && page.nextCursor !== "" ? { nextCursor: page.nextCursor } : {}
3128
+ };
3129
+ }
3130
+ /** Synchronous compatibility reads plus a rich, subscribable directory. */
3131
+ var CodexModelCatalog = class {
3132
+ deps;
3133
+ timeoutMs;
3134
+ cache;
3135
+ constructor(deps) {
3136
+ this.deps = deps;
3137
+ this.timeoutMs = deps.timeoutMs ?? 5e3;
3138
+ this.cache = new ModelDirectoryCache({
3139
+ load: (key, signal) => {
3140
+ const context = JSON.parse(key);
3141
+ return this.probe(context.homeDir, context.cwd, signal);
3142
+ },
3143
+ ttlMs: deps.ttlMs ?? 3e5,
3144
+ ...deps.now === void 0 ? {} : { now: deps.now },
3145
+ onError: (error) => deps.warn?.(`local-agent-codex: the model catalog probe degraded: ${error instanceof Error ? error.message : String(error)}`)
3146
+ });
3147
+ }
3148
+ key(homeDir, cwd = homeDir) {
3149
+ const files = [join(homeDir, "config.toml"), join(homeDir, "auth.json")];
3150
+ for (let directory = cwd;; directory = dirname(directory)) {
3151
+ files.push(join(directory, ".codex", "config.toml"));
3152
+ if (dirname(directory) === directory) break;
3153
+ }
3154
+ return modelDirectoryContextKey({
3155
+ provider: "codex",
3156
+ homeDir,
3157
+ cwd,
3158
+ cli: ["codex"],
3159
+ files
3160
+ });
3161
+ }
3162
+ read(homeDir, cwd) {
3163
+ return this.directory(homeDir, cwd).entries.filter((entry) => !entry.hidden).map((entry) => entry.value);
3164
+ }
3165
+ readDefault(homeDir, cwd) {
3166
+ return this.directory(homeDir, cwd).defaultModel;
3167
+ }
3168
+ directory(homeDir, cwd) {
3169
+ return this.cache.read(this.key(homeDir, cwd));
3170
+ }
3171
+ follow(homeDir, signal, cwd) {
3172
+ return this.cache.follow(() => this.key(homeDir, cwd), signal);
3173
+ }
3174
+ invalidate(homeDir, cwd) {
3175
+ this.cache.invalidate(this.key(homeDir, cwd));
3176
+ }
3177
+ dispose() {
3178
+ this.cache.dispose();
3179
+ }
3180
+ async refresh(homeDir, cwd) {
3181
+ return (await this.cache.refresh(this.key(homeDir, cwd))).entries.filter((entry) => !entry.hidden).map((entry) => entry.value);
3182
+ }
3183
+ /**
3184
+ * One bounded probe: spawn the app-server against the scoped home, answer
3185
+ * the initialize handshake exactly as the live driver shapes it, ask
3186
+ * `model/list`, then tear the process down (stdin EOF → grace →
3187
+ * terminate). The overall timeout kills the process on expiry. Failures propagate to the shared cache, which preserves prior data.
3188
+ */
3189
+ async probe(homeDir, cwd, signal) {
3190
+ const spec = {
3191
+ argv: [
3192
+ "codex",
3193
+ "app-server",
3194
+ "--stdio"
3195
+ ],
3196
+ cwd,
3197
+ stdio: {
3198
+ stdin: "pipe",
3199
+ stdout: "pipe",
3200
+ stderr: "pipe"
3201
+ },
3202
+ graceMs: DEFAULT_DISPOSE_GRACE_MS,
3203
+ env: delegationEnv({ CODEX_HOME: homeDir })
3204
+ };
3205
+ const child = this.deps.spawn(spec);
3206
+ const decoder = new StringDecoder("utf8");
3207
+ let buffer = "";
3208
+ const pending = /* @__PURE__ */ new Map();
3209
+ let nextId = 0;
3210
+ let done = false;
3211
+ const finish = () => {
3212
+ if (done) return;
3213
+ done = true;
3214
+ for (const request of pending.values()) request.reject(/* @__PURE__ */ new Error("the model catalog probe closed"));
3215
+ pending.clear();
3216
+ };
3217
+ child.stdout?.on("data", (chunk) => {
3218
+ buffer += decoder.write(chunk);
3219
+ let index = buffer.indexOf("\n");
3220
+ while (index >= 0) {
3221
+ const line = buffer.slice(0, index);
3222
+ buffer = buffer.slice(index + 1);
3223
+ index = buffer.indexOf("\n");
3224
+ if (line.trim() === "") continue;
3225
+ let message;
3226
+ try {
3227
+ message = JSON.parse(line);
3228
+ } catch {
3229
+ continue;
3230
+ }
3231
+ const id = typeof message.id === "number" ? message.id : void 0;
3232
+ if (id !== void 0 && typeof message.method === "string") {
3233
+ child.stdin?.write(JSON.stringify({
3234
+ jsonrpc: "2.0",
3235
+ id,
3236
+ error: {
3237
+ code: -32601,
3238
+ message: "unsupported"
3239
+ }
3240
+ }) + "\n");
3241
+ continue;
3242
+ }
3243
+ if (id === void 0) continue;
3244
+ const request = pending.get(id);
3245
+ if (request === void 0) continue;
3246
+ pending.delete(id);
3247
+ const wireError = message.error;
3248
+ if (wireError !== void 0 && wireError !== null) request.reject(new Error(wireError.message ?? "wire error"));
3249
+ else request.resolve(message.result);
3250
+ }
3251
+ });
3252
+ child.done.then(finish, finish);
3253
+ const abort = () => {
3254
+ finish();
3255
+ child.terminate();
3256
+ };
3257
+ signal.addEventListener("abort", abort, { once: true });
3258
+ if (signal.aborted) abort();
3259
+ const request = (method, params) => {
3260
+ if (done) return Promise.reject(/* @__PURE__ */ new Error("the model catalog probe closed"));
3261
+ nextId += 1;
3262
+ const id = nextId;
3263
+ return new Promise((resolve, reject) => {
3264
+ pending.set(id, {
3265
+ resolve: (result) => resolve(result),
3266
+ reject
3267
+ });
3268
+ child.stdin?.write(JSON.stringify({
3269
+ jsonrpc: "2.0",
3270
+ id,
3271
+ method,
3272
+ params
3273
+ }) + "\n");
3274
+ });
3275
+ };
3276
+ const teardown = async () => {
3277
+ finish();
3278
+ child.stdin?.end();
3279
+ await Promise.race([child.done.catch(() => {}), new Promise((resolve) => setTimeout(resolve, PROBE_EOF_GRACE_MS))]);
3280
+ child.terminate();
3281
+ await child.waitForExit().catch(() => {});
3282
+ };
3283
+ let timer;
3284
+ try {
3285
+ return await Promise.race([(async () => {
3286
+ await request("initialize", {
3287
+ clientInfo: {
3288
+ name: "dsh-local-agent-codex",
3289
+ title: "dsh local-agent codex model catalog probe",
3290
+ version: "0.1.0"
3291
+ },
3292
+ capabilities: {
3293
+ experimentalApi: false,
3294
+ requestAttestation: false
3295
+ }
3296
+ });
3297
+ child.stdin?.write(JSON.stringify({
3298
+ jsonrpc: "2.0",
3299
+ method: "initialized"
3300
+ }) + "\n");
3301
+ const entries = /* @__PURE__ */ new Map();
3302
+ const cursors = /* @__PURE__ */ new Set();
3303
+ let cursor;
3304
+ let defaultModel;
3305
+ do {
3306
+ const page = catalogEntries(await request("model/list", {
3307
+ includeHidden: true,
3308
+ ...cursor === void 0 ? {} : { cursor }
3309
+ }));
3310
+ for (const entry of page.entries) if (!entries.has(entry.value)) entries.set(entry.value, entry);
3311
+ defaultModel ??= page.defaultModel;
3312
+ cursor = page.nextCursor;
3313
+ if (cursor !== void 0) {
3314
+ if (cursors.has(cursor)) throw new Error("model/list repeated a pagination cursor");
3315
+ cursors.add(cursor);
3316
+ }
3317
+ } while (cursor !== void 0);
3318
+ return {
3319
+ entries: [...entries.values()],
3320
+ complete: true,
3321
+ customInput: true,
3322
+ ...defaultModel === void 0 ? {} : { defaultModel }
3323
+ };
3324
+ })(), new Promise((_, reject) => {
3325
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("the model catalog probe timed out")), this.timeoutMs);
3326
+ timer.unref();
3327
+ })]);
3328
+ } finally {
3329
+ if (timer !== void 0) clearTimeout(timer);
3330
+ signal.removeEventListener("abort", abort);
3331
+ await teardown();
3332
+ }
3333
+ }
3334
+ };
3335
+ //#endregion
3336
+ //#region lib/types/index.js
3337
+ /**
3338
+ * Codex harness instantiation of the local-agent family. Registers the
3339
+ * `codex` harness into the core registry: scoped home under the shared homes
3340
+ * root (`CODEX_HOME`), device-code login through `codex login --device-auth`
3341
+ * (prompt captured from stdout), and rollout-file session records.
3342
+ * Delegation mounts through the `codex-local` provider (one-shot
3343
+ * `codex exec` under the scoped home); the bundle patch's
3344
+ * `tool-subagent-codex-local` row puts the tool on the profile root, so
3345
+ * every agent preset can delegate.
3346
+ *
3347
+ * @module @khorsheed/dsh-local-agent-codex
3348
+ */
3349
+ /** Stable Cordis plugin name; the bundle patch row id. */
3350
+ const name = "local-agent-codex";
3351
+ /**
3352
+ * Boot delay before the model catalog's eager warmup probe fires. The probe
3353
+ * spawns a `codex app-server` — cheap, but not free — so it waits out the
3354
+ * boot storm instead of competing with it, and the FIRST settings-card open
3355
+ * usually hits a warm cache. A card opened before the timer fires simply
3356
+ * triggers the probe itself (the read path's lazy kick), so the delay never
3357
+ * costs correctness. Kept short: a setting visible within seconds of boot.
3358
+ */
3359
+ const CATALOG_WARMUP_DELAY_MS = 1500;
3360
+ /** Services required before the harness can register. */
3361
+ const inject = [
3362
+ "localAgent",
3363
+ "subagents",
3364
+ "subprocess",
3365
+ "settings"
3366
+ ];
3367
+ /**
3368
+ * The settings fields (`live`, `liveMirrorGranularity`, `model`,
3369
+ * `recentModels`) ride the plugin Config, marked `.volatile()` where the
3370
+ * host's schemastery has it (3.18.4+, the rc.1 line — probed, never sniffed):
3371
+ * there the SettingsForms writes hot-track the running fiber's Volatile
3372
+ * references without a remount, and the official one-shot import moves an old
3373
+ * settings.yaml section into this row's config. On 0.1.5 the call is absent,
3374
+ * the fields stay plain config keys (a semantic superset — the profile row
3375
+ * can now preset them too), and the legacy settings namespace below carries
3376
+ * the user layer exactly as before. `model` deliberately carries NO default:
3377
+ * an unset key must resolve to undefined, which is what keeps the pre-key
3378
+ * behavior byte-identical.
3379
+ */
3380
+ const SETTINGS_FIELDS = {
3381
+ live: z.boolean().default(false),
3382
+ liveMirrorGranularity: z.union([z.const("event"), z.const("token")]).default("token"),
3383
+ model: z.string(),
3384
+ recentModels: z.array(z.string()).default([])
3385
+ };
3386
+ /** Mark one schema field volatile when the host's schemastery has the method (rc.1), pass it through plain when not (0.1.5). */
3387
+ function volatilize(field) {
3388
+ return typeof field.volatile === "function" ? field.volatile() : field;
3389
+ }
3390
+ const Config = z.object({
3391
+ sandbox: z.union([
3392
+ z.const("read-only"),
3393
+ z.const("workspace-write"),
3394
+ z.const("danger-full-access")
3395
+ ]),
3396
+ model: volatilize(SETTINGS_FIELDS.model),
3397
+ live: volatilize(SETTINGS_FIELDS.live),
3398
+ liveIdleMs: z.number().default(DEFAULT_LIVE_IDLE_MS),
3399
+ liveMirrorGranularity: volatilize(SETTINGS_FIELDS.liveMirrorGranularity),
3400
+ recentModels: volatilize(SETTINGS_FIELDS.recentModels)
3401
+ });
3402
+ /** The sandbox policy a fresh delegation defaults to. */
3403
+ const DEFAULT_SANDBOX = "workspace-write";
3404
+ /**
3405
+ * Settings namespace for the settings-page card. The Cordis config feeds the
3406
+ * composition `base` layer, so a field absent from the user layer inherits the
3407
+ * YAML value — the card only ever stores deliberate overrides.
3408
+ */
3409
+ const CODEX_SETTINGS_NAMESPACE = "local-agent-codex";
3410
+ /**
3411
+ * The card's 0.1.5 schema; field defaults are the innermost layer below
3412
+ * `base`. Shared with the Config schema's settings fields above.
3413
+ */
3414
+ const CODEX_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
3415
+ /**
3416
+ * Register the Codex harness into the local-agent registry.
3417
+ * @param ctx - plugin context carrying the registry.
3418
+ * @param config - plugin config; `sandbox` selects the `codex exec` policy.
3419
+ */
3420
+ function apply(ctx, config) {
3421
+ ctx.effect(() => {
3422
+ const sandbox = config.sandbox ?? "workspace-write";
3423
+ const homeDir = ctx.localAgent.homeDir("codex");
3424
+ provisionCodexConfig(homeDir).catch((error) => {
3425
+ ctx.logger.warn(`local-agent-codex: config.toml provisioning failed: ${error instanceof Error ? error.message : String(error)}`);
3426
+ });
3427
+ const readSettings = () => {
3428
+ const model = vol(config.model);
3429
+ const recentModels = vol(config.recentModels);
3430
+ return {
3431
+ live: vol(config.live) ?? false,
3432
+ liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? "token",
3433
+ ...model === void 0 ? {} : { model },
3434
+ ...recentModels === void 0 ? {} : { recentModels }
3435
+ };
3436
+ };
3437
+ const legacyBase = {};
3438
+ for (const field of [
3439
+ "live",
3440
+ "liveMirrorGranularity",
3441
+ "model"
3442
+ ]) {
3443
+ const value = vol(config[field]);
3444
+ if (value !== void 0) legacyBase[field] = value;
3445
+ }
3446
+ const face = settingsFace(ctx, {
3447
+ namespace: CODEX_SETTINGS_NAMESPACE,
3448
+ legacySchema: CODEX_SETTINGS_SCHEMA,
3449
+ legacyBase,
3450
+ read: readSettings
3451
+ });
3452
+ const resolveModel = () => {
3453
+ const model = face.get().model?.trim();
3454
+ return model === void 0 || model === "" ? void 0 : model;
3455
+ };
3456
+ const memberModelOverrides = /* @__PURE__ */ new Map();
3457
+ const modelCatalog = new CodexModelCatalog({
3458
+ spawn: (spec) => ctx.subprocess.spawn(spec),
3459
+ warn: (message) => ctx.logger.warn(message)
3460
+ });
3461
+ const warmup = setTimeout(() => {
3462
+ modelCatalog.read(homeDir);
3463
+ }, CATALOG_WARMUP_DELAY_MS);
3464
+ warmup.unref();
3465
+ const liveSwitch = new LiveDriverSwitch(ctx, face, {
3466
+ sandbox,
3467
+ model: (childSessionId) => memberModelOverrides.get(childSessionId) ?? resolveModel(),
3468
+ ...config.liveIdleMs === void 0 ? {} : { liveIdleMs: config.liveIdleMs }
3469
+ });
3470
+ const modelBroker = new CodexModelBroker({
3471
+ ctx,
3472
+ settingsModel: resolveModel,
3473
+ recentModels: () => face.get().recentModels ?? [],
3474
+ homeDir: (childSessionId) => ctx.localAgent.homeDir("codex", childSessionId === void 0 ? void 0 : (ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.scope),
3475
+ cwd: (childSessionId) => childSessionId === void 0 ? void 0 : (ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.cwd,
3476
+ directory: (home, cwd) => modelCatalog.directory(home, cwd),
3477
+ refreshDirectory: (home, cwd) => modelCatalog.refresh(home, cwd),
3478
+ followDirectory: (home, cwd, signal) => modelCatalog.follow(home, signal, cwd),
3479
+ live: () => face.get().live,
3480
+ overrides: memberModelOverrides,
3481
+ liveBoundModel: (childSessionId) => liveSwitch.boundModel(childSessionId),
3482
+ retireRuntime: (childSessionId) => liveSwitch.retireRuntime(childSessionId),
3483
+ catalog: (scopedHome, cwd) => modelCatalog.read(scopedHome, cwd),
3484
+ catalogDefault: (scopedHome, cwd) => modelCatalog.readDefault(scopedHome, cwd)
3485
+ });
3486
+ const disposeProvider = ctx.subagents.registerProvider(new CodexCliProvider(ctx, sandbox, liveSwitch.resolve, resolveModel, (childSessionId) => memberModelOverrides.get(childSessionId)));
3487
+ const disposeHarness = ctx.localAgent.register({
3488
+ prepareMember: async ({ binding, childSession, configuration, signal }) => {
3489
+ const driver = liveSwitch.resolve(binding.childSessionId);
3490
+ if (driver === void 0 || driver.disabled) throw new Error("Enable the native live harness before preparing a coordinator");
3491
+ await driver.prepare({
3492
+ cwd: binding.cwd,
3493
+ homeDir: ctx.localAgent.homeDir("codex", binding.scope),
3494
+ childSession,
3495
+ parentSessionId: binding.parentSessionId,
3496
+ configuration: configuration.resolved
3497
+ }, signal);
3498
+ },
3499
+ name: "codex",
3500
+ displayName: "Codex",
3501
+ homeEnvVar: "CODEX_HOME",
3502
+ delegationProvider: "codex-local",
3503
+ modelBroker,
3504
+ login: { pty: {
3505
+ command: "codex",
3506
+ args: ["login"]
3507
+ } },
3508
+ records: { listSessions: (homeDir) => listCodexSessions(homeDir) },
3509
+ provision: (scopedHome) => provisionCodexConfig(scopedHome).then(() => {}),
3510
+ isAuthenticated: codexAuthenticated,
3511
+ credentialStamp: codexCredentialStamp,
3512
+ logout: codexLogout,
3513
+ effectiveSettings: async (scopedHome) => {
3514
+ const [reasoningEffort, baseUrl, scopedModel, cliVersion] = await Promise.all([
3515
+ readCodexReasoningEffort(scopedHome).catch(() => void 0),
3516
+ readCodexBaseUrl(scopedHome).catch(() => void 0),
3517
+ readCodexModel(scopedHome).catch(() => void 0),
3518
+ codexCliVersion(ctx, scopedHome).catch(() => void 0)
3519
+ ]);
3520
+ const model = resolveModel() ?? scopedModel;
3521
+ const baseUrlHost = baseUrl !== void 0 ? endpointHost(baseUrl) : void 0;
3522
+ return {
3523
+ drive: face.get().live ? "live" : "exec",
3524
+ sandbox,
3525
+ ...reasoningEffort !== void 0 ? { reasoningEffort } : {},
3526
+ baseUrlSet: baseUrl !== void 0,
3527
+ ...baseUrlHost !== void 0 ? { baseUrlHost } : {},
3528
+ ...model !== void 0 ? { model } : {},
3529
+ ...cliVersion !== void 0 ? { cliVersion } : {}
3530
+ };
3531
+ },
3532
+ subcommand: (input, invocation) => {
3533
+ const [verb] = input.split(/\s+/);
3534
+ if (verb === "sessions") return listCodexSessions(invocation.agent.ctx.localAgent.homeDir("codex")).then((records) => ({
3535
+ kind: "success",
3536
+ text: records.length === 0 ? "No codex sessions in the scoped home yet." : records.map((r) => `${r.id} ${r.workDir}`).join("\n")
3537
+ }));
3538
+ }
3539
+ });
3540
+ return () => {
3541
+ clearTimeout(warmup);
3542
+ modelCatalog.dispose();
3543
+ disposeProvider();
3544
+ disposeHarness();
3545
+ liveSwitch.dispose();
3546
+ };
3547
+ }, "local-agent-codex: harness");
3548
+ }
3549
+ //#endregion
3550
+ export { CATALOG_WARMUP_DELAY_MS, CODEX_SETTINGS_NAMESPACE, Config, DEFAULT_SANDBOX, apply, inject, name };