@khorsheed/dsh-local-agent-codex 0.2.0-rc.1 → 0.2.0
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/CHANGELOG.md +25 -0
- package/README.en.md +23 -122
- package/README.i18n.yaml +2 -2
- package/README.md +23 -122
- package/lib/client.js +811 -317
- package/lib/index.js +818 -252
- package/lib/types/client/SettingsCard.d.ts +73 -24
- package/lib/types/client/SettingsCard.js +110 -140
- package/lib/types/client/icons.d.ts +1 -1
- package/lib/types/client/index.d.ts +6 -26
- package/lib/types/client/index.js +136 -50
- package/lib/types/client/locales.d.ts +34 -24
- package/lib/types/client/locales.js +38 -28
- package/lib/types/codex-cli-provider.d.ts +54 -8
- package/lib/types/codex-cli-provider.js +204 -70
- package/lib/types/endpoint-auth.d.ts +194 -0
- package/lib/types/endpoint-auth.js +300 -0
- package/lib/types/index.d.ts +19 -3
- package/lib/types/index.js +127 -28
- package/lib/types/live-driver.d.ts +9 -5
- package/lib/types/live-driver.js +247 -108
- package/lib/types/live-switch.d.ts +7 -0
- package/lib/types/live-switch.js +1 -0
- package/lib/types/model-broker.d.ts +12 -2
- package/lib/types/model-broker.js +51 -6
- package/lib/types/model-catalog.d.ts +53 -9
- package/lib/types/model-catalog.js +95 -12
- package/lib/types/provision.d.ts +12 -27
- package/lib/types/provision.js +19 -40
- package/lib/types/records.d.ts +6 -11
- package/lib/types/records.js +14 -25
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +20 -21
- package/lib/invariant.js +0 -31
- package/lib/types/invariant.d.ts +0 -16
- package/lib/types/invariant.js +0 -33
package/lib/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
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";
|
|
2
|
+
import { LIVE_FLUSH_INTERVAL_MS, LiveFlush, LiveStreamPublisher, ModelDirectoryCache, assertResumeCwdUnchanged, assertResumeScopeUnchanged, containerExecSpawn, containerScopedHome, delegationEnv, establishSubagentCatalogChild, extendModelDirectory, firstModelOf, modelDirectoryContextKey, persistChildSession, probeCliVersion, readModelList, readModelListSync, resolveChildCwd, resolveRoundModel, settingsFace, subagentDelegationLabel, vol, writeModelList } from "@khorsheed/dsh-local-agent";
|
|
3
3
|
import { MEMBER_BRIDGE_SOCKET_ENV, MEMBER_BRIDGE_TOKEN_ENV, endpointHost } from "@khorsheed/dsh-local-agent/types";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { createAssistantMessage, createToolResultMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
6
6
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
7
|
-
import { NO_START_CAPABILITIES, settleRunResult, subprocessRunHandle } from "@deepseek-ai/dsh-subagent";
|
|
7
|
+
import { NO_START_CAPABILITIES, SUBAGENT_DESCRIPTOR_VERSION, settleRunResult, subprocessRunHandle } from "@deepseek-ai/dsh-subagent";
|
|
8
8
|
import { StringDecoder } from "node:string_decoder";
|
|
9
|
-
import { open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
9
|
+
import { mkdir, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
10
10
|
import { dirname, join, resolve } from "node:path";
|
|
11
11
|
//#region lib/types/records.js
|
|
12
12
|
/**
|
|
@@ -38,21 +38,6 @@ const ROLLOUT_WINDOW_SLACK_MS = 3e5;
|
|
|
38
38
|
/** Extra future slack for the window end (file timestamps may skew ahead). */
|
|
39
39
|
const ROLLOUT_WINDOW_FUTURE_MS = 6e4;
|
|
40
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
41
|
* Parse the first line of a rollout file for the listing fields. Unknown or
|
|
57
42
|
* malformed values yield an undefined record rather than failing the list.
|
|
58
43
|
* @param text - the raw rollout file content (only the head is read).
|
|
@@ -150,10 +135,12 @@ async function listCodexSessions(homeDir) {
|
|
|
150
135
|
* `input_tokens` is the TOTAL input including cache hits (OpenAI-style
|
|
151
136
|
* accounting, confirmed against `total_tokens` in the rollout token_count),
|
|
152
137
|
* and `cached_input_tokens` is the cache-read subset — so the uncached
|
|
153
|
-
* bucket subtracts the cached portion to avoid double counting.
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
138
|
+
* bucket subtracts the cached portion to avoid double counting.
|
|
139
|
+
* `cached_input_tokens` rides every payload (zero included), so
|
|
140
|
+
* `cacheReadTokens` is written whenever the field is present — a reader can
|
|
141
|
+
* tell "no hits" from "not reported". Codex also reports cache creation
|
|
142
|
+
* (`cache_write_input_tokens`), mapped to `cacheWriteTokens` (the shared
|
|
143
|
+
* four-bucket contract: uncached input, output, cache read, cache write).
|
|
157
144
|
* @param usage - the raw codex usage object (`turn.completed`'s `usage` or a
|
|
158
145
|
* rollout `token_count` `info.last_token_usage`/`total_token_usage`).
|
|
159
146
|
* @returns the shared usage record.
|
|
@@ -162,12 +149,14 @@ function usageFromCodex(usage) {
|
|
|
162
149
|
const raw = usage;
|
|
163
150
|
const input = Number(raw.input_tokens);
|
|
164
151
|
const cached = Number(raw.cached_input_tokens);
|
|
152
|
+
const cacheWrite = Number(raw.cache_write_input_tokens);
|
|
165
153
|
const output = Number(raw.output_tokens);
|
|
166
154
|
const usageRecord = {
|
|
167
|
-
inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached) : Number.isFinite(input) ? input : 0,
|
|
155
|
+
inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached - (Number.isFinite(cacheWrite) ? cacheWrite : 0)) : Number.isFinite(input) ? input : 0,
|
|
168
156
|
outputTokens: Number.isFinite(output) ? output : 0
|
|
169
157
|
};
|
|
170
|
-
if (Number.isFinite(cached)
|
|
158
|
+
if (Number.isFinite(cached)) usageRecord.cacheReadTokens = cached;
|
|
159
|
+
if (Number.isFinite(cacheWrite) && cacheWrite > 0) usageRecord.cacheWriteTokens = cacheWrite;
|
|
171
160
|
return usageRecord;
|
|
172
161
|
}
|
|
173
162
|
/**
|
|
@@ -430,6 +419,293 @@ async function codexRolloutRoundFacts(homeDir, locator) {
|
|
|
430
419
|
};
|
|
431
420
|
}
|
|
432
421
|
//#endregion
|
|
422
|
+
//#region lib/types/endpoint-auth.js
|
|
423
|
+
/**
|
|
424
|
+
* Endpoint-key authentication — the codex harness's ONLY auth mode. There is
|
|
425
|
+
* no web login anywhere in this package: a delegation round reaches its
|
|
426
|
+
* endpoint with a custom `model_providers` entry on the argv pointing at the
|
|
427
|
+
* relay and the key traveling as `OPENAI_API_KEY` in the process environment
|
|
428
|
+
* (verified against codex-cli 0.163.0: a fresh scoped home with only these
|
|
429
|
+
* runs `codex exec` with no login file at all). With no credential the spawn
|
|
430
|
+
* carries no auth at all — it fails loudly at the endpoint, exactly the
|
|
431
|
+
* honest reading the status shows.
|
|
432
|
+
*
|
|
433
|
+
* Verified against codex-cli 0.163.0 (2026-10-20): a fresh scoped home with
|
|
434
|
+
* only `OPENAI_API_KEY` plus the two `-c` overrides below runs
|
|
435
|
+
* `codex exec -m <model>` end-to-end through an OpenAI-compatible relay
|
|
436
|
+
* (openproxy, `wire_api = "responses"`) — no login, no auth.json, no
|
|
437
|
+
* onboarding gate.
|
|
438
|
+
*
|
|
439
|
+
* Two layers, merged per field, file over config:
|
|
440
|
+
*
|
|
441
|
+
* - `<scopedHome>/endpoint-auth.json` (mode 0600) — the per-scope credential.
|
|
442
|
+
* Its name deliberately matches the eval contract hash's credential denylist
|
|
443
|
+
* (`auth`), so the key material never enters an evaluation's condition
|
|
444
|
+
* fingerprint; the file's mtime serves as the credential stamp, which makes
|
|
445
|
+
* "edit the file" the recovery gesture for a `rejected` grade.
|
|
446
|
+
* - The plugin config's `baseUrl` / `apiKey` — the deployment default every
|
|
447
|
+
* scope without a file shares. Read PER ROUND, so a profile edit applies to
|
|
448
|
+
* the next round (and a host restart clears the in-memory `rejected` mark).
|
|
449
|
+
*
|
|
450
|
+
* `wireApi` is a file-only escape hatch: the settings card never edits it,
|
|
451
|
+
* the default `responses` is codex's native wire (relay-verified), and a
|
|
452
|
+
* hand-edit to `"chat"` survives the card's whole-file writes because the
|
|
453
|
+
* read merges it back. The env layer rides the family's existing plumbing:
|
|
454
|
+
* `delegationEnv` on the host, `containerExecSpawn`'s name-only `-e`
|
|
455
|
+
* forwarding into a container — a containerized round authenticates with the
|
|
456
|
+
* same credential as the host, nothing is authorized inside the unit.
|
|
457
|
+
* @module @khorsheed/dsh-local-agent-codex/endpoint-auth
|
|
458
|
+
*/
|
|
459
|
+
/**
|
|
460
|
+
* The scoped-home credential file's name. Chosen to match the eval contract
|
|
461
|
+
* hash's credential-shaped denylist (`/token|key|credential|secret|password|auth|\.env/i`),
|
|
462
|
+
* so the file is excluded from condition fingerprints by name.
|
|
463
|
+
*/
|
|
464
|
+
const ENDPOINT_AUTH_FILE = "endpoint-auth.json";
|
|
465
|
+
/** The custom provider's TOML key in the `-c` overrides (bare-key safe). */
|
|
466
|
+
const ENDPOINT_PROVIDER_NAME = "dsh-endpoint";
|
|
467
|
+
/** The env var the custom provider reads the key from (`env_key`). */
|
|
468
|
+
const ENDPOINT_ENV_KEY = "OPENAI_API_KEY";
|
|
469
|
+
/** Trim and normalize one credential field; blank reads as absent. */
|
|
470
|
+
function field(value) {
|
|
471
|
+
if (typeof value !== "string") return void 0;
|
|
472
|
+
const trimmed = value.trim();
|
|
473
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
474
|
+
}
|
|
475
|
+
/** Whether a resolved endpoint carries a usable credential. */
|
|
476
|
+
function hasEndpointCredential(auth) {
|
|
477
|
+
return auth !== void 0 && field(auth.apiKey) !== void 0;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Read the scope's endpoint-auth.json. Absent or unreadable yields undefined
|
|
481
|
+
* (the caller falls back to the plugin config); malformed JSON or a non-object
|
|
482
|
+
* payload also yields undefined — a torn file must not fail a status read.
|
|
483
|
+
* Non-string fields are dropped, never carried through.
|
|
484
|
+
* @param homeDir - the `codex` harness's scoped home.
|
|
485
|
+
* @returns the file's endpoint credential, or undefined.
|
|
486
|
+
*/
|
|
487
|
+
async function readEndpointAuthFile(homeDir) {
|
|
488
|
+
let text;
|
|
489
|
+
try {
|
|
490
|
+
text = await readFile(join(homeDir, ENDPOINT_AUTH_FILE), "utf8");
|
|
491
|
+
} catch {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
const parsed = JSON.parse(text);
|
|
496
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
497
|
+
const baseUrl = field(parsed["baseUrl"]);
|
|
498
|
+
const apiKey = field(parsed["apiKey"]);
|
|
499
|
+
const wireApi = field(parsed["wireApi"]);
|
|
500
|
+
return {
|
|
501
|
+
...baseUrl === void 0 ? {} : { baseUrl },
|
|
502
|
+
...apiKey === void 0 ? {} : { apiKey },
|
|
503
|
+
...wireApi === void 0 ? {} : { wireApi }
|
|
504
|
+
};
|
|
505
|
+
} catch {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Resolve one scope's effective endpoint credential: the scope's
|
|
511
|
+
* endpoint-auth.json merged FIELD BY FIELD over the plugin config's
|
|
512
|
+
* `baseUrl` / `apiKey` (a file that names only a key rotates the key while
|
|
513
|
+
* keeping the deployment's relay URL). The result may carry no credential at
|
|
514
|
+
* all — {@link hasEndpointCredential} is the judgment, kept separate so the
|
|
515
|
+
* caller can still route/log the resolved baseUrl.
|
|
516
|
+
* @param homeDir - the scope's scoped home.
|
|
517
|
+
* @param config - the plugin config's deployment-level endpoint pair.
|
|
518
|
+
* @returns the merged endpoint auth.
|
|
519
|
+
*/
|
|
520
|
+
async function resolveEndpointAuth(homeDir, config) {
|
|
521
|
+
const file = await readEndpointAuthFile(homeDir);
|
|
522
|
+
const configBaseUrl = field(config.baseUrl);
|
|
523
|
+
const configApiKey = field(config.apiKey);
|
|
524
|
+
return {
|
|
525
|
+
...configBaseUrl === void 0 ? {} : { baseUrl: configBaseUrl },
|
|
526
|
+
...configApiKey === void 0 ? {} : { apiKey: configApiKey },
|
|
527
|
+
...file
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* The spawn-env layer for a resolved endpoint: the key under the custom
|
|
532
|
+
* provider's `env_key` name, nothing else. A credential-less resolution
|
|
533
|
+
* yields an empty layer — the spread then adds no keys at all, byte-identical
|
|
534
|
+
* to the pre-endpoint env. With no baseUrl the CLI's built-in OpenAI provider
|
|
535
|
+
* reads the same variable, so an official platform key needs no `-c`
|
|
536
|
+
* override either.
|
|
537
|
+
* @param auth - the resolved endpoint auth, or undefined.
|
|
538
|
+
* @returns the explicit env entries for the delegation spawn.
|
|
539
|
+
*/
|
|
540
|
+
function endpointAuthEnv(auth) {
|
|
541
|
+
const apiKey = field(auth?.apiKey);
|
|
542
|
+
return apiKey === void 0 ? {} : { [ENDPOINT_ENV_KEY]: apiKey };
|
|
543
|
+
}
|
|
544
|
+
/** Quote one TOML basic string for the `-c` config override. */
|
|
545
|
+
function tomlString$1(value) {
|
|
546
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* The `-c` config overrides routing the CLI at the resolved relay: a custom
|
|
550
|
+
* `model_providers` entry plus the `model_provider` selection, as a flat argv
|
|
551
|
+
* fragment (`-c <override>` pairs). Empty unless BOTH a key and a baseUrl are
|
|
552
|
+
* present — a key without a relay URL is served by the built-in provider
|
|
553
|
+
* through the env layer alone, and a relay URL without a key would route
|
|
554
|
+
* every round into a 401. Verified end-to-end against codex-cli 0.163.0.
|
|
555
|
+
* @param auth - the resolved endpoint auth, or undefined.
|
|
556
|
+
* @returns the argv fragment, possibly empty.
|
|
557
|
+
*/
|
|
558
|
+
function endpointConfigOverrides(auth) {
|
|
559
|
+
const baseUrl = field(auth?.baseUrl);
|
|
560
|
+
const apiKey = field(auth?.apiKey);
|
|
561
|
+
if (baseUrl === void 0 || apiKey === void 0) return [];
|
|
562
|
+
const wireApi = field(auth?.wireApi) ?? "responses";
|
|
563
|
+
return [
|
|
564
|
+
"-c",
|
|
565
|
+
`model_provider=${tomlString$1(ENDPOINT_PROVIDER_NAME)}`,
|
|
566
|
+
"-c",
|
|
567
|
+
`model_providers.${ENDPOINT_PROVIDER_NAME}={name=${tomlString$1(ENDPOINT_PROVIDER_NAME)},base_url=${tomlString$1(baseUrl)},env_key=${tomlString$1(ENDPOINT_ENV_KEY)},wire_api=${tomlString$1(wireApi)}}`
|
|
568
|
+
];
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* The credential marker's modification stamp (epoch ms): the endpoint file's
|
|
572
|
+
* mtime, or undefined when the scope has no file (a config-sourced credential
|
|
573
|
+
* has no marker — rotating it pairs with the profile edit and instance
|
|
574
|
+
* restart that clear the in-memory grade marks). A fresh file write is a
|
|
575
|
+
* fresh stamp, which is how an edited-in key recovers a `rejected` grade.
|
|
576
|
+
* @param homeDir - the scope's scoped home.
|
|
577
|
+
* @returns the stamp, or undefined.
|
|
578
|
+
*/
|
|
579
|
+
async function endpointAuthStamp(homeDir) {
|
|
580
|
+
try {
|
|
581
|
+
return (await stat(join(homeDir, ENDPOINT_AUTH_FILE))).mtimeMs;
|
|
582
|
+
} catch {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/** Whether the scope carries its own endpoint file at all. */
|
|
587
|
+
async function endpointAuthFileExists(homeDir) {
|
|
588
|
+
return await endpointAuthStamp(homeDir) !== void 0;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Write the scope's endpoint-auth.json (mode 0600, mkdir -p first). The
|
|
592
|
+
* payload preserves the file-only `wireApi` across card writes: callers pass
|
|
593
|
+
* the merged object (current file + edits), never a bare card payload.
|
|
594
|
+
* @param homeDir - the scope's scoped home.
|
|
595
|
+
* @param auth - the endpoint credential to persist.
|
|
596
|
+
*/
|
|
597
|
+
async function writeEndpointAuth(homeDir, auth) {
|
|
598
|
+
await mkdir(homeDir, { recursive: true });
|
|
599
|
+
const baseUrl = field(auth.baseUrl);
|
|
600
|
+
const apiKey = field(auth.apiKey);
|
|
601
|
+
const wireApi = field(auth.wireApi);
|
|
602
|
+
const payload = {
|
|
603
|
+
...baseUrl === void 0 ? {} : { baseUrl },
|
|
604
|
+
...apiKey === void 0 ? {} : { apiKey },
|
|
605
|
+
...wireApi === void 0 ? {} : { wireApi }
|
|
606
|
+
};
|
|
607
|
+
await writeFile(join(homeDir, ENDPOINT_AUTH_FILE), JSON.stringify(payload, void 0, 2) + "\n", { mode: 384 });
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Clear the scope's OWN endpoint credential (the endpoint-auth.json file).
|
|
611
|
+
* A deployment-level credential from the plugin config is untouched — the
|
|
612
|
+
* core's logout reply then names the profile row as the place to edit.
|
|
613
|
+
* @param homeDir - the scope's scoped home.
|
|
614
|
+
*/
|
|
615
|
+
async function clearEndpointAuth(homeDir) {
|
|
616
|
+
await rm(join(homeDir, ENDPOINT_AUTH_FILE), { force: true });
|
|
617
|
+
}
|
|
618
|
+
/** How many served model ids the check reply lists before truncating. */
|
|
619
|
+
const CHECK_MODEL_LIST_LIMIT = 8;
|
|
620
|
+
/** Bound on the error-body excerpt a failed check reports. */
|
|
621
|
+
const CHECK_ERROR_BODY_LIMIT = 200;
|
|
622
|
+
/**
|
|
623
|
+
* `GET <baseUrl>/v1/models` with `Authorization: Bearer <key>` — the
|
|
624
|
+
* token-free endpoint probe shared by the `/codex check` verdict and any
|
|
625
|
+
* future catalog. OpenAI's own API serves this route with the platform key;
|
|
626
|
+
* relays (openproxy verified 2026-10-20, 139 models) proxy it and answer the
|
|
627
|
+
* same OpenAI-list shape. Never throws: every failure mode folds into the
|
|
628
|
+
* result.
|
|
629
|
+
* @param endpoint - the resolved endpoint auth with a decided baseUrl.
|
|
630
|
+
* @param fetchImpl - the HTTP fetch; default the host's global fetch.
|
|
631
|
+
* @param signal - abort for the whole probe (timeout bundled by the caller).
|
|
632
|
+
* @returns the folded result.
|
|
633
|
+
*/
|
|
634
|
+
async function fetchEndpointModels(endpoint, fetchImpl, signal) {
|
|
635
|
+
const headers = {};
|
|
636
|
+
if (endpoint.apiKey !== void 0) headers["authorization"] = `Bearer ${endpoint.apiKey}`;
|
|
637
|
+
const fetcher = fetchImpl ?? globalThis.fetch;
|
|
638
|
+
const root = endpoint.baseUrl.replace(/\/+$/, "");
|
|
639
|
+
const url = `${root}${root.endsWith("/v1") ? "" : "/v1"}/models`;
|
|
640
|
+
let response;
|
|
641
|
+
try {
|
|
642
|
+
response = await fetcher(url, {
|
|
643
|
+
headers,
|
|
644
|
+
signal: signal ?? AbortSignal.timeout(8e3)
|
|
645
|
+
});
|
|
646
|
+
} catch (error) {
|
|
647
|
+
return {
|
|
648
|
+
kind: "unreachable",
|
|
649
|
+
message: error instanceof Error ? error.message : String(error)
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
if (!response.ok) return {
|
|
653
|
+
kind: "http",
|
|
654
|
+
status: response.status,
|
|
655
|
+
body: (await response.text().catch(() => "")).slice(0, CHECK_ERROR_BODY_LIMIT)
|
|
656
|
+
};
|
|
657
|
+
let data;
|
|
658
|
+
try {
|
|
659
|
+
data = JSON.parse(await response.text()).data;
|
|
660
|
+
} catch {
|
|
661
|
+
return { kind: "malformed" };
|
|
662
|
+
}
|
|
663
|
+
if (!Array.isArray(data)) return { kind: "malformed" };
|
|
664
|
+
const models = [];
|
|
665
|
+
for (const entry of data) {
|
|
666
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
667
|
+
const id = entry.id;
|
|
668
|
+
if (typeof id === "string" && id !== "") models.push(id);
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
kind: "models",
|
|
672
|
+
models
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Probe the resolved endpoint with a cheap, token-free round-trip: `GET
|
|
677
|
+
* <baseUrl>/v1/models` with the resolved credential's Bearer header. A 200
|
|
678
|
+
* proves both reachability and credential validity — the exact evidence a
|
|
679
|
+
* "verify" button needs — and the served model list rides the reply. One
|
|
680
|
+
* transport-level retry rides along: a local proxy restart (a Surge/clash
|
|
681
|
+
* reload) stalls the first attempt into the timeout and answers the retry
|
|
682
|
+
* immediately. Never throws: every failure mode folds into the reply text,
|
|
683
|
+
* since the reply IS the diagnostic.
|
|
684
|
+
* @param endpoint - the resolved endpoint auth (scope file over settings).
|
|
685
|
+
* @param fetchImpl - the HTTP fetch; default the host's global fetch.
|
|
686
|
+
* @param timeoutMs - one attempt's deadline (two attempts at most).
|
|
687
|
+
* @returns the human-readable verdict for the check reply.
|
|
688
|
+
*/
|
|
689
|
+
async function checkCodexEndpoint(endpoint, fetchImpl, timeoutMs = 2e4) {
|
|
690
|
+
if (!hasEndpointCredential(endpoint)) return "No endpoint credential configured — fill Base URL and API Key on the settings card (or the profile row) first.";
|
|
691
|
+
const baseUrl = (endpoint.baseUrl ?? "https://api.openai.com").replace(/\/+$/, "");
|
|
692
|
+
let result = await fetchEndpointModels({
|
|
693
|
+
...endpoint,
|
|
694
|
+
baseUrl
|
|
695
|
+
}, fetchImpl, AbortSignal.timeout(timeoutMs));
|
|
696
|
+
if (result.kind === "unreachable") result = await fetchEndpointModels({
|
|
697
|
+
...endpoint,
|
|
698
|
+
baseUrl
|
|
699
|
+
}, fetchImpl, AbortSignal.timeout(timeoutMs));
|
|
700
|
+
if (result.kind === "unreachable") return `Endpoint unreachable (${baseUrl}): ${result.message}`;
|
|
701
|
+
if (result.kind === "http") return `Endpoint answered HTTP ${String(result.status)} (${baseUrl})${result.body === "" ? "" : `: ${result.body}`}`;
|
|
702
|
+
if (result.kind === "malformed") return `Endpoint reachable (${baseUrl}) but the model list did not parse — the credential worked, the reply was not /v1/models-shaped.`;
|
|
703
|
+
if (result.models.length === 0) return `Endpoint reachable (${baseUrl}); it serves no models.`;
|
|
704
|
+
const listed = result.models.slice(0, CHECK_MODEL_LIST_LIMIT).join(", ");
|
|
705
|
+
const more = result.models.length > CHECK_MODEL_LIST_LIMIT ? `, … +${String(result.models.length - CHECK_MODEL_LIST_LIMIT)} more` : "";
|
|
706
|
+
return `Endpoint reachable (${baseUrl}): ${String(result.models.length)} model(s) served — ${listed}${more}`;
|
|
707
|
+
}
|
|
708
|
+
//#endregion
|
|
433
709
|
//#region lib/types/live-driver.js
|
|
434
710
|
/**
|
|
435
711
|
* The codex provider's live driver: one resident `codex app-server --stdio`
|
|
@@ -468,11 +744,6 @@ const DEFAULT_LIVE_CHANNEL_RETRY_MS = 3e5;
|
|
|
468
744
|
const RECLAIM_EOF_GRACE_MS = 1e3;
|
|
469
745
|
/** Default minimum interval between one streaming item's snapshot messages. */
|
|
470
746
|
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
747
|
var LiveChannelUnavailableError = class extends Error {
|
|
477
748
|
constructor(message) {
|
|
478
749
|
super(message);
|
|
@@ -554,11 +825,14 @@ function codexAppServerItemToLine(item) {
|
|
|
554
825
|
case "dynamicToolCall": {
|
|
555
826
|
const tool = typeof item["tool"] === "string" ? item.tool : "dynamic";
|
|
556
827
|
const args = item["arguments"] === void 0 || item["arguments"] === null ? void 0 : typeof item["arguments"] === "string" ? item["arguments"] : JSON.stringify(item["arguments"]);
|
|
828
|
+
const content = item["contentItems"];
|
|
829
|
+
const result = Array.isArray(content) ? content.filter((block) => typeof block === "object" && block !== null).map((block) => typeof block["text"] === "string" ? block["text"] : void 0).filter((text) => text !== void 0 && text !== "").join("\n") : void 0;
|
|
557
830
|
return {
|
|
558
831
|
kind: "tool",
|
|
559
832
|
id: itemId ?? `codex-live-${randomUUID()}`,
|
|
560
833
|
name: tool,
|
|
561
|
-
...args === void 0 || args === "" ? {} : { args }
|
|
834
|
+
...args === void 0 || args === "" ? {} : { args },
|
|
835
|
+
...result === void 0 || result === "" ? {} : { result }
|
|
562
836
|
};
|
|
563
837
|
}
|
|
564
838
|
case "collabAgentToolCall": {
|
|
@@ -589,12 +863,14 @@ function codexAppServerItemToLine(item) {
|
|
|
589
863
|
function usageFromAppServer(breakdown) {
|
|
590
864
|
const input = Number(breakdown["inputTokens"]);
|
|
591
865
|
const cached = Number(breakdown["cachedInputTokens"]);
|
|
866
|
+
const cacheWrite = Number(breakdown["cacheWriteInputTokens"]);
|
|
592
867
|
const output = Number(breakdown["outputTokens"]);
|
|
593
868
|
const usage = {
|
|
594
|
-
inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached) : Number.isFinite(input) ? input : 0,
|
|
869
|
+
inputTokens: Number.isFinite(input) && Number.isFinite(cached) ? Math.max(0, input - cached - (Number.isFinite(cacheWrite) ? cacheWrite : 0)) : Number.isFinite(input) ? input : 0,
|
|
595
870
|
outputTokens: Number.isFinite(output) ? output : 0
|
|
596
871
|
};
|
|
597
|
-
if (Number.isFinite(cached)
|
|
872
|
+
if (Number.isFinite(cached)) usage.cacheReadTokens = cached;
|
|
873
|
+
if (Number.isFinite(cacheWrite) && cacheWrite > 0) usage.cacheWriteTokens = cacheWrite;
|
|
598
874
|
return usage;
|
|
599
875
|
}
|
|
600
876
|
/**
|
|
@@ -948,11 +1224,13 @@ var CodexLiveDriver = class {
|
|
|
948
1224
|
const member = registerCodexMemberRun(this.ctx, String(spec.childSession.id), spec.parentSessionId);
|
|
949
1225
|
const startModel = spec.startModel?.trim();
|
|
950
1226
|
const model = spec.configuration !== void 0 ? spec.configuration.model : startModel !== void 0 && startModel !== "" ? startModel : this.config.model?.(key)?.trim();
|
|
1227
|
+
const endpoint = this.config.endpoint === void 0 ? void 0 : await this.config.endpoint(spec.homeDir).catch(() => void 0);
|
|
951
1228
|
const spawnSpec = {
|
|
952
1229
|
argv: [
|
|
953
1230
|
"codex",
|
|
954
1231
|
"app-server",
|
|
955
1232
|
...member === void 0 ? [] : ["-c", member.configOverride],
|
|
1233
|
+
...endpointConfigOverrides(endpoint),
|
|
956
1234
|
...model === void 0 || model === "" ? [] : ["-c", `model=${JSON.stringify(model)}`],
|
|
957
1235
|
...spec.configuration?.effort === void 0 ? [] : ["-c", `model_reasoning_effort=${JSON.stringify(spec.configuration.effort)}`],
|
|
958
1236
|
"--stdio"
|
|
@@ -964,7 +1242,10 @@ var CodexLiveDriver = class {
|
|
|
964
1242
|
stderr: "pipe"
|
|
965
1243
|
},
|
|
966
1244
|
graceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
967
|
-
env: delegationEnv({
|
|
1245
|
+
env: delegationEnv({
|
|
1246
|
+
CODEX_HOME: spec.homeDir,
|
|
1247
|
+
...endpointAuthEnv(endpoint)
|
|
1248
|
+
})
|
|
968
1249
|
};
|
|
969
1250
|
let child;
|
|
970
1251
|
try {
|
|
@@ -1151,27 +1432,31 @@ var CodexLiveDriver = class {
|
|
|
1151
1432
|
const lines = [];
|
|
1152
1433
|
let mirrored = 0;
|
|
1153
1434
|
let usage;
|
|
1154
|
-
/**
|
|
1155
|
-
let
|
|
1435
|
+
/** The cumulative turn total the wire's `tokenUsage.total` names, for the settled report. */
|
|
1436
|
+
let totalUsage;
|
|
1156
1437
|
/**
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
*
|
|
1161
|
-
* mid-stream therefore folds ABOVE the stream's step and the projection
|
|
1162
|
-
* renders the round's chronological order.
|
|
1438
|
+
* Per-call usage by transcript line: each `thread/tokenUsage/updated`
|
|
1439
|
+
* notification closes one API call (its items have all completed), and
|
|
1440
|
+
* the call's LAST line carries its `tokenUsage.last`. The map is
|
|
1441
|
+
* authoritative per line, independent of the flush's withUsage flag.
|
|
1163
1442
|
*/
|
|
1164
|
-
const
|
|
1443
|
+
const usageByLine = /* @__PURE__ */ new Map();
|
|
1444
|
+
/** Whether a folded line already carries usage (per-call, or the round's fallback — no backfill event exists). */
|
|
1445
|
+
let usageCarried = false;
|
|
1165
1446
|
/**
|
|
1166
|
-
* The streaming items seen this round by item id:
|
|
1167
|
-
*
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1447
|
+
* The streaming items seen this round by item id: deltas accumulate into
|
|
1448
|
+
* throttled snapshots at the item's step once it may open in the log
|
|
1449
|
+
* (previewed live before that) — the host folds repeated settles at one
|
|
1450
|
+
* coordinate into one live-updating chat node. The item's completion
|
|
1451
|
+
* folds at the same step and finalizes it; an entry whose completion
|
|
1452
|
+
* never arrives is force-finalized at settle. A tool that completes
|
|
1453
|
+
* mid-stream folds after the stream, keeping the round's chronology.
|
|
1173
1454
|
*/
|
|
1174
1455
|
const streams = /* @__PURE__ */ new Map();
|
|
1456
|
+
/** Provisional stream keys (live flush and preview identity), never log steps. */
|
|
1457
|
+
let streamKeys = 0;
|
|
1458
|
+
/** Line indexes already written (a line may fold ahead of an earlier one it chronologically precedes). */
|
|
1459
|
+
const folded = /* @__PURE__ */ new Set();
|
|
1175
1460
|
/** The item deltas currently accumulate into (items stream sequentially). */
|
|
1176
1461
|
let activeStream;
|
|
1177
1462
|
/** Items/completions that arrived before the turn id was known. */
|
|
@@ -1215,6 +1500,27 @@ var CodexLiveDriver = class {
|
|
|
1215
1500
|
*/
|
|
1216
1501
|
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
1502
|
const liveFlush = new LiveFlush((error) => this.ctx.logger.warn(`live mirror flush failed: ${String(error)}`), this.config.snapshotMinIntervalMs ?? DEFAULT_SNAPSHOT_MIN_INTERVAL_MS);
|
|
1503
|
+
let lastStep = 0;
|
|
1504
|
+
let openStep;
|
|
1505
|
+
const openStreamStep = (stream) => {
|
|
1506
|
+
lastStep += 1;
|
|
1507
|
+
childSession.append("step/start", {
|
|
1508
|
+
turn,
|
|
1509
|
+
step: lastStep
|
|
1510
|
+
});
|
|
1511
|
+
stream.durable = lastStep;
|
|
1512
|
+
stream.opened = true;
|
|
1513
|
+
openStep = lastStep;
|
|
1514
|
+
};
|
|
1515
|
+
const closeOpenStep = () => {
|
|
1516
|
+
if (openStep === void 0) return;
|
|
1517
|
+
childSession.append("step/end", {
|
|
1518
|
+
turn,
|
|
1519
|
+
step: openStep
|
|
1520
|
+
});
|
|
1521
|
+
openStep = void 0;
|
|
1522
|
+
};
|
|
1523
|
+
const canOpenStream = (stream) => openStep === void 0 && mirrored >= stream.after && streams.keys().next().value === stream.itemId;
|
|
1218
1524
|
const pendingStreamArrival = /* @__PURE__ */ new Map();
|
|
1219
1525
|
const appendStreamSnapshot = (stream, force, interrupted, withUsage = false, final = false) => {
|
|
1220
1526
|
if (stream.text.trim() === "") return;
|
|
@@ -1227,30 +1533,29 @@ var CodexLiveDriver = class {
|
|
|
1227
1533
|
liveFlush.cancel(stream.step);
|
|
1228
1534
|
const receivedAt = pendingStreamArrival.get(stream.step) ?? now;
|
|
1229
1535
|
pendingStreamArrival.delete(stream.step);
|
|
1230
|
-
if (
|
|
1231
|
-
if (
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1536
|
+
if (!stream.opened) {
|
|
1537
|
+
if (canOpenStream(stream)) openStreamStep(stream);
|
|
1538
|
+
else if (!final) {
|
|
1539
|
+
streamPublisher?.preview(stream.step, stream.kind, stream.text, {
|
|
1540
|
+
itemId: stream.itemId,
|
|
1541
|
+
receivedAt
|
|
1235
1542
|
});
|
|
1236
|
-
|
|
1543
|
+
return;
|
|
1544
|
+
} else {
|
|
1545
|
+
closeOpenStep();
|
|
1546
|
+
openStreamStep(stream);
|
|
1237
1547
|
}
|
|
1238
|
-
|
|
1548
|
+
}
|
|
1549
|
+
if (streamPublisher !== void 0 && !final) {
|
|
1550
|
+
streamPublisher.update(stream.durable, stream.kind, stream.text, { receivedAt });
|
|
1239
1551
|
stream.lastSnapshotAt = now;
|
|
1240
1552
|
stream.lastSnapshotLen = stream.text.length;
|
|
1241
1553
|
return;
|
|
1242
1554
|
}
|
|
1243
|
-
|
|
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;
|
|
1555
|
+
const attachUsage = withUsage && !usageCarried && usageByLine.size === 0 && usage !== void 0;
|
|
1251
1556
|
childSession.append("assistant/message", {
|
|
1252
1557
|
turn,
|
|
1253
|
-
step: stream.
|
|
1558
|
+
step: stream.durable,
|
|
1254
1559
|
message: codexAssistantEvent([stream.kind === "think" ? {
|
|
1255
1560
|
type: "reasoning",
|
|
1256
1561
|
text: stream.text
|
|
@@ -1259,7 +1564,7 @@ var CodexLiveDriver = class {
|
|
|
1259
1564
|
text: stream.text
|
|
1260
1565
|
}]),
|
|
1261
1566
|
stream: [],
|
|
1262
|
-
...withUsage && !usageCarried && usage !== void 0 ? { usage } : {},
|
|
1567
|
+
...withUsage && !usageCarried && usageByLine.size === 0 && usage !== void 0 ? { usage } : {},
|
|
1263
1568
|
...interrupted ? { interrupted: true } : {}
|
|
1264
1569
|
}, { surfaceOp: "append" });
|
|
1265
1570
|
if (attachUsage) usageCarried = true;
|
|
@@ -1267,11 +1572,19 @@ var CodexLiveDriver = class {
|
|
|
1267
1572
|
stream.lastSnapshotLen = stream.text.length;
|
|
1268
1573
|
persist();
|
|
1269
1574
|
};
|
|
1575
|
+
/** Close a stream's live presentation once its final text is in the log. */
|
|
1576
|
+
const finishStream = (stream) => {
|
|
1577
|
+
liveFlush.cancel(stream.step);
|
|
1578
|
+
streams.delete(stream.itemId);
|
|
1579
|
+
if (activeStream === stream.itemId) activeStream = void 0;
|
|
1580
|
+
streamPublisher?.finish(stream.step);
|
|
1581
|
+
if (stream.durable !== void 0) streamPublisher?.finish(stream.durable);
|
|
1582
|
+
};
|
|
1270
1583
|
/**
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1273
|
-
* different item's deltas force one
|
|
1274
|
-
*
|
|
1584
|
+
* Track one new streaming item: its provisional key (live flush and
|
|
1585
|
+
* preview identity) and how many lines had completed when it began. A
|
|
1586
|
+
* different item's deltas force one freshness snapshot of the current
|
|
1587
|
+
* stream.
|
|
1275
1588
|
*/
|
|
1276
1589
|
const reserveStream = (itemId, kind) => {
|
|
1277
1590
|
if (activeStream !== void 0 && activeStream !== itemId) {
|
|
@@ -1281,12 +1594,12 @@ var CodexLiveDriver = class {
|
|
|
1281
1594
|
}
|
|
1282
1595
|
let stream = streams.get(itemId);
|
|
1283
1596
|
if (stream === void 0) {
|
|
1284
|
-
|
|
1285
|
-
reservedSteps.push(step);
|
|
1597
|
+
streamKeys += 1;
|
|
1286
1598
|
stream = {
|
|
1287
1599
|
itemId,
|
|
1288
1600
|
kind,
|
|
1289
|
-
step,
|
|
1601
|
+
step: streamKeys,
|
|
1602
|
+
after: lines.length,
|
|
1290
1603
|
text: "",
|
|
1291
1604
|
lastSnapshotAt: 0,
|
|
1292
1605
|
lastSnapshotLen: 0,
|
|
@@ -1296,60 +1609,78 @@ var CodexLiveDriver = class {
|
|
|
1296
1609
|
}
|
|
1297
1610
|
activeStream = itemId;
|
|
1298
1611
|
};
|
|
1299
|
-
/** The
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1612
|
+
/** The stream a completed line finalizes, if it was streamed. */
|
|
1613
|
+
const streamOfLine = (line) => line === void 0 || line.kind === "tool" ? void 0 : streams.get(line.itemId ?? "");
|
|
1614
|
+
/** Write lines[index] at the next step(s). */
|
|
1615
|
+
const foldLine = (index, lineUsage) => {
|
|
1616
|
+
const line = lines[index];
|
|
1617
|
+
const stream = streamOfLine(line);
|
|
1618
|
+
if (stream !== void 0) {
|
|
1619
|
+
if (!stream.opened) {
|
|
1620
|
+
closeOpenStep();
|
|
1621
|
+
openStreamStep(stream);
|
|
1622
|
+
}
|
|
1623
|
+
const blocks = stream.kind === "think" ? [{
|
|
1624
|
+
type: "reasoning",
|
|
1625
|
+
text: line.kind === "tool" ? "" : line.text
|
|
1626
|
+
}] : [{
|
|
1627
|
+
type: "text",
|
|
1628
|
+
text: line.kind === "tool" ? "" : line.text
|
|
1629
|
+
}];
|
|
1630
|
+
childSession.append("assistant/message", {
|
|
1631
|
+
turn,
|
|
1632
|
+
step: stream.durable,
|
|
1633
|
+
message: codexAssistantEvent(blocks),
|
|
1634
|
+
stream: [],
|
|
1635
|
+
...lineUsage === void 0 ? {} : { usage: lineUsage }
|
|
1636
|
+
}, { surfaceOp: "append" });
|
|
1637
|
+
finishStream(stream);
|
|
1638
|
+
closeOpenStep();
|
|
1639
|
+
} else {
|
|
1640
|
+
closeOpenStep();
|
|
1641
|
+
lastStep += 1;
|
|
1642
|
+
appendCodexTranscriptLine(childSession, turn, lastStep, line, lineUsage);
|
|
1643
|
+
}
|
|
1644
|
+
folded.add(index);
|
|
1645
|
+
localAgent?.reportRunProgress(childSession.id, {
|
|
1646
|
+
kind: "delta",
|
|
1647
|
+
text: codexLineText(line)
|
|
1648
|
+
});
|
|
1305
1649
|
};
|
|
1306
|
-
/**
|
|
1650
|
+
/**
|
|
1651
|
+
* Mirror the completed lines before `upto` in chronological order; the
|
|
1652
|
+
* last line is held back until completion. A line waits behind every
|
|
1653
|
+
* stream that began before it completed: that stream's line folds first,
|
|
1654
|
+
* or — while it is still streaming — the mirror stops until it lands.
|
|
1655
|
+
*/
|
|
1307
1656
|
const mirrorUpTo = (upto, withUsage) => {
|
|
1308
1657
|
let usageIndex = -1;
|
|
1309
|
-
if (withUsage) {
|
|
1658
|
+
if (withUsage && usageByLine.size === 0) {
|
|
1310
1659
|
for (let index = 0; index < lines.length; index += 1) if (lines[index]?.kind !== "tool") usageIndex = index;
|
|
1311
1660
|
}
|
|
1312
|
-
|
|
1313
|
-
const
|
|
1314
|
-
if (
|
|
1661
|
+
const usageFor = (index) => {
|
|
1662
|
+
const own = usageByLine.get(index);
|
|
1663
|
+
if (own !== void 0) {
|
|
1664
|
+
usageCarried = true;
|
|
1665
|
+
return own;
|
|
1666
|
+
}
|
|
1315
1667
|
const lineUsage = withUsage && index === usageIndex ? usage : void 0;
|
|
1316
1668
|
if (lineUsage !== void 0) usageCarried = true;
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
});
|
|
1669
|
+
return lineUsage;
|
|
1670
|
+
};
|
|
1671
|
+
outer: for (let index = mirrored; index < upto; index += 1) {
|
|
1672
|
+
if (folded.has(index)) continue;
|
|
1673
|
+
const own = streamOfLine(lines[index]);
|
|
1674
|
+
const all = [...streams.values()];
|
|
1675
|
+
const before = own === void 0 ? all.filter((stream) => stream.after <= index) : all.slice(0, all.indexOf(own));
|
|
1676
|
+
for (const earlier of before) {
|
|
1677
|
+
const at = lines.findIndex((line, position) => position > index && position < upto && !folded.has(position) && streamOfLine(line) === earlier);
|
|
1678
|
+
if (at < 0) break outer;
|
|
1679
|
+
foldLine(at, usageFor(at));
|
|
1680
|
+
}
|
|
1681
|
+
foldLine(index, usageFor(index));
|
|
1352
1682
|
}
|
|
1683
|
+
while (folded.has(mirrored)) mirrored += 1;
|
|
1353
1684
|
if (upto > 0) persist();
|
|
1354
1685
|
};
|
|
1355
1686
|
let resolveCompletion;
|
|
@@ -1358,6 +1689,11 @@ var CodexLiveDriver = class {
|
|
|
1358
1689
|
});
|
|
1359
1690
|
const openTurn = () => {
|
|
1360
1691
|
childSession.append("turn/start", { turn });
|
|
1692
|
+
if (spec.configuration?.model !== void 0) appendCodexRequestHeader(childSession, {
|
|
1693
|
+
provider: "codex-local",
|
|
1694
|
+
model: spec.configuration.model,
|
|
1695
|
+
...spec.configuration.effort === void 0 ? {} : { reasoningEffort: spec.configuration.effort }
|
|
1696
|
+
});
|
|
1361
1697
|
childSession.append("user/message", createUserMessage({
|
|
1362
1698
|
content: [{
|
|
1363
1699
|
type: "text",
|
|
@@ -1408,17 +1744,29 @@ var CodexLiveDriver = class {
|
|
|
1408
1744
|
return;
|
|
1409
1745
|
}
|
|
1410
1746
|
if (method === "thread/tokenUsage/updated") {
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1747
|
+
const tokenUsage = params["tokenUsage"];
|
|
1748
|
+
const last = tokenUsage?.["last"];
|
|
1749
|
+
const total = tokenUsage?.["total"];
|
|
1750
|
+
if (total !== void 0 && (Number.isFinite(Number(total["inputTokens"])) || Number.isFinite(Number(total["outputTokens"])))) totalUsage = usageFromAppServer(total);
|
|
1751
|
+
if (last !== void 0) {
|
|
1752
|
+
usage = usageFromAppServer(last);
|
|
1753
|
+
if (lines.length > 0) {
|
|
1754
|
+
usageByLine.set(lines.length - 1, usage);
|
|
1755
|
+
mirrorUpTo(lines.length - 1, true);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1413
1758
|
return;
|
|
1414
1759
|
}
|
|
1415
1760
|
if (method === "turn/completed") {
|
|
1416
1761
|
const turnData = params["turn"];
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1762
|
+
try {
|
|
1763
|
+
mirrorUpTo(lines.length, true);
|
|
1764
|
+
} finally {
|
|
1765
|
+
resolveCompletion({
|
|
1766
|
+
status: String(turnData["status"]),
|
|
1767
|
+
...turnData["error"] === null || turnData["error"] === void 0 ? {} : { error: turnData["error"] }
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1422
1770
|
}
|
|
1423
1771
|
};
|
|
1424
1772
|
/** Route one wire notification: round-scoped, turn-id tagged, early ones buffered. */
|
|
@@ -1518,15 +1866,19 @@ var CodexLiveDriver = class {
|
|
|
1518
1866
|
liveFlush.dispose();
|
|
1519
1867
|
if (turnOpened) {
|
|
1520
1868
|
if (mirrored < lines.length) mirrorUpTo(lines.length, true);
|
|
1869
|
+
while (mirrored < lines.length && streams.size > 0) {
|
|
1870
|
+
const oldest = streams.values().next().value;
|
|
1871
|
+
appendStreamSnapshot(oldest, true, settled.stopReason !== "completed", false, true);
|
|
1872
|
+
finishStream(oldest);
|
|
1873
|
+
closeOpenStep();
|
|
1874
|
+
mirrorUpTo(lines.length, true);
|
|
1875
|
+
}
|
|
1521
1876
|
if (streams.size > 0) {
|
|
1522
1877
|
const remaining = [...streams.values()];
|
|
1523
1878
|
for (const [position, stream] of remaining.entries()) {
|
|
1524
1879
|
appendStreamSnapshot(stream, true, settled.stopReason !== "completed", position === remaining.length - 1, true);
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
turn,
|
|
1528
|
-
step: stream.step
|
|
1529
|
-
});
|
|
1880
|
+
finishStream(stream);
|
|
1881
|
+
closeOpenStep();
|
|
1530
1882
|
}
|
|
1531
1883
|
streams.clear();
|
|
1532
1884
|
activeStream = void 0;
|
|
@@ -1577,7 +1929,7 @@ var CodexLiveDriver = class {
|
|
|
1577
1929
|
windowStart: startedAtMs,
|
|
1578
1930
|
cwd: spec.cwd
|
|
1579
1931
|
});
|
|
1580
|
-
const settledUsage = usage ?? facts.usage;
|
|
1932
|
+
const settledUsage = totalUsage ?? usage ?? facts.usage;
|
|
1581
1933
|
const round = {
|
|
1582
1934
|
...facts.model === void 0 ? {} : { observedModel: facts.model },
|
|
1583
1935
|
...facts.cliVersion === void 0 ? {} : { cliVersion: facts.cliVersion },
|
|
@@ -1593,7 +1945,7 @@ var CodexLiveDriver = class {
|
|
|
1593
1945
|
});
|
|
1594
1946
|
await Promise.race([accepted, abortBranch.catch(() => void 0)]);
|
|
1595
1947
|
return subprocessRunHandle({
|
|
1596
|
-
id:
|
|
1948
|
+
id: SessionId(randomUUID()),
|
|
1597
1949
|
result,
|
|
1598
1950
|
signal: request.signal,
|
|
1599
1951
|
onAbort,
|
|
@@ -1608,31 +1960,33 @@ var CodexLiveDriver = class {
|
|
|
1608
1960
|
//#endregion
|
|
1609
1961
|
//#region lib/types/provision.js
|
|
1610
1962
|
/**
|
|
1611
|
-
* Codex scoped-home provisioning:
|
|
1612
|
-
*
|
|
1613
|
-
* `
|
|
1614
|
-
* to
|
|
1615
|
-
*
|
|
1616
|
-
*
|
|
1617
|
-
* untouched; sign-out removes the credential file.
|
|
1963
|
+
* Codex scoped-home provisioning: a working `config.toml` (a person may edit
|
|
1964
|
+
* it — custom providers, model, effort) plus the one-way sweep of the web
|
|
1965
|
+
* login's leftover `auth.json`. The harness is endpoint-key only: a stale
|
|
1966
|
+
* grant sitting next to an endpoint credential would keep authenticating the
|
|
1967
|
+
* built-in provider while the status reads "unauthenticated", so provisioning
|
|
1968
|
+
* deletes it on every boot (claude-code's OAuth-remnant sweep, same idea).
|
|
1618
1969
|
* @module @khorsheed/dsh-local-agent-codex/provision
|
|
1619
1970
|
*/
|
|
1620
|
-
/**
|
|
1621
|
-
const AUTH_FILE = "auth.json";
|
|
1622
|
-
/** Minimal scoped config: keep credentials in the scoped home. */
|
|
1971
|
+
/** Minimal scoped config: keep any hand-run login's credentials in the scoped home. */
|
|
1623
1972
|
const SCOPED_CONFIG = `# Written by dsh-local-agent-codex: keep the device-code
|
|
1624
1973
|
# credentials inside the scoped home (auth.json) instead of the OS keychain.
|
|
1625
1974
|
cli_auth_credentials_store = "file"
|
|
1626
1975
|
`;
|
|
1976
|
+
/** The web login's credential file, swept on every boot (endpoint-key only now). */
|
|
1977
|
+
const LEGACY_AUTH_FILE = "auth.json";
|
|
1627
1978
|
/**
|
|
1628
|
-
* Provision
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
1631
|
-
*
|
|
1979
|
+
* Provision the scoped home: ensure a `config.toml` exists (a minimal one —
|
|
1980
|
+
* codex reads it for custom providers, model, and effort; the historical
|
|
1981
|
+
* `cli_auth_credentials_store = "file"` pin stays so a hand-run `codex login`
|
|
1982
|
+
* under the scoped home can never spill into the macOS keychain), and sweep
|
|
1983
|
+
* the web login's leftover `auth.json` (the harness no longer consults it —
|
|
1984
|
+
* endpoint key or nothing).
|
|
1632
1985
|
* @param homeDir - the `codex` harness's scoped home.
|
|
1633
1986
|
* @returns true when a config was written, false when one already existed.
|
|
1634
1987
|
*/
|
|
1635
1988
|
async function provisionCodexConfig(homeDir) {
|
|
1989
|
+
await rm(join(homeDir, LEGACY_AUTH_FILE), { force: true });
|
|
1636
1990
|
try {
|
|
1637
1991
|
await readFile(join(homeDir, "config.toml"));
|
|
1638
1992
|
return false;
|
|
@@ -1767,47 +2121,6 @@ async function listCodexConfigModels(homeDir) {
|
|
|
1767
2121
|
}
|
|
1768
2122
|
return found;
|
|
1769
2123
|
}
|
|
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
2124
|
/** Quote one TOML basic string for the `-c` config override. */
|
|
1812
2125
|
function tomlString(value) {
|
|
1813
2126
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
@@ -1831,7 +2144,7 @@ function registerCodexMemberRun(ctx, childSessionId, parentSessionId) {
|
|
|
1831
2144
|
});
|
|
1832
2145
|
const serverName = `dsh-member-${token.slice(0, 8)}`;
|
|
1833
2146
|
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")}}`;
|
|
2147
|
+
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")},tool_timeout_sec=1800}`;
|
|
1835
2148
|
let released = false;
|
|
1836
2149
|
return {
|
|
1837
2150
|
token,
|
|
@@ -1889,6 +2202,7 @@ var CodexCliProvider = class {
|
|
|
1889
2202
|
live;
|
|
1890
2203
|
model;
|
|
1891
2204
|
overrides;
|
|
2205
|
+
endpoint;
|
|
1892
2206
|
name = "codex-local";
|
|
1893
2207
|
capabilities = NO_START_CAPABILITIES;
|
|
1894
2208
|
inheritsParentContext = false;
|
|
@@ -1899,22 +2213,33 @@ var CodexCliProvider = class {
|
|
|
1899
2213
|
* while a retiring generation still hosts it).
|
|
1900
2214
|
* @param model - resolver for the configured model, read PER ROUND so a
|
|
1901
2215
|
* settings-card write takes effect on the next delegation without a
|
|
1902
|
-
* reload
|
|
1903
|
-
*
|
|
1904
|
-
*
|
|
2216
|
+
* reload; it receives the round's scope (a named scope's curated list
|
|
2217
|
+
* answers first). Undefined (the resolver absent, or returning
|
|
2218
|
+
* undefined) leaves the argv exactly as it was before the key existed —
|
|
2219
|
+
* the scoped config.toml's own `model` then decides, as it always did.
|
|
1905
2220
|
* @param overrides - resolver for the member's session-level model override
|
|
1906
2221
|
* (the composer picker, owned by the model broker). It outranks BOTH the
|
|
1907
2222
|
* delegation's recorded model and the configured one: on the exec path it
|
|
1908
2223
|
* re-points the next round's `-m`, and on the live path it is the member's
|
|
1909
2224
|
* start model — a resident runtime bound to a different model is retired
|
|
1910
2225
|
* so the round respawns onto the override.
|
|
2226
|
+
* @param endpoint - resolver for the scope's endpoint credential, read PER
|
|
2227
|
+
* ROUND so a settings-card write takes effect on the next delegation
|
|
2228
|
+
* without a reload. A resolution carrying a key routes the round at the
|
|
2229
|
+
* relay (a custom `model_providers` entry on the argv, the key in the
|
|
2230
|
+
* env); with no key the spawn carries no credential at all.
|
|
1911
2231
|
*/
|
|
1912
|
-
constructor(ctx, sandbox = "workspace-write", live, model, overrides) {
|
|
2232
|
+
constructor(ctx, sandbox = "workspace-write", live, model, overrides, endpoint) {
|
|
1913
2233
|
this.ctx = ctx;
|
|
1914
2234
|
this.sandbox = sandbox;
|
|
1915
2235
|
this.live = live;
|
|
1916
2236
|
this.model = model;
|
|
1917
2237
|
this.overrides = overrides;
|
|
2238
|
+
this.endpoint = endpoint;
|
|
2239
|
+
}
|
|
2240
|
+
/** The scope's endpoint credential for this round; absent resolver = never. */
|
|
2241
|
+
resolveEndpoint(homeDir) {
|
|
2242
|
+
return this.endpoint === void 0 ? Promise.resolve(void 0) : this.endpoint(homeDir).catch(() => void 0);
|
|
1918
2243
|
}
|
|
1919
2244
|
/** Resolve the live driver for one round's member, if live is on for it. */
|
|
1920
2245
|
liveDriver(childSessionId) {
|
|
@@ -1950,7 +2275,7 @@ var CodexCliProvider = class {
|
|
|
1950
2275
|
...record?.configurationLock === void 0 ? {} : { configurationLock: record.configurationLock }
|
|
1951
2276
|
}, (configuration) => this.startCodexResume(request, intent, cwd, homeDir, exec, scope, record?.model, configuration), request.signal, intent?.onAdmitted);
|
|
1952
2277
|
}
|
|
1953
|
-
const childSessionId = SessionId(intent?.preparedMemberId ?? randomUUID());
|
|
2278
|
+
const childSessionId = SessionId(intent?.preparedMemberId ?? intent?.childSessionId ?? randomUUID());
|
|
1954
2279
|
if (typeof this.ctx.localAgent.withMemberConfigurationRound !== "function") {
|
|
1955
2280
|
if (intent?.effort !== void 0) throw new Error("Codex effort requires the configuration admission core");
|
|
1956
2281
|
return this.startCodexFresh(request, cwd, homeDir, exec, scope, intent?.model, childSessionId);
|
|
@@ -1979,16 +2304,19 @@ var CodexCliProvider = class {
|
|
|
1979
2304
|
delegationDepth: (request.parent.session.header.delegationDepth ?? 0) + 1
|
|
1980
2305
|
} });
|
|
1981
2306
|
const harness = this.ctx.localAgent.get("codex");
|
|
1982
|
-
const label = subagentDelegationLabel(harness?.displayName ?? "codex", request.
|
|
2307
|
+
const label = subagentDelegationLabel(harness?.displayName ?? "codex", request.label || void 0);
|
|
1983
2308
|
childSession.append("subagent/descriptor", {
|
|
1984
|
-
|
|
2309
|
+
version: SUBAGENT_DESCRIPTOR_VERSION,
|
|
2310
|
+
mode: "one-shot",
|
|
2311
|
+
provider: this.name,
|
|
1985
2312
|
label
|
|
1986
2313
|
});
|
|
1987
2314
|
establishSubagentCatalogChild(request.parent.session, childSession.header, label);
|
|
1988
2315
|
} catch (error) {
|
|
1989
2316
|
this.ctx.logger.warn(`subagent-codex: subagent session record failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1990
2317
|
}
|
|
1991
|
-
const
|
|
2318
|
+
const endpoint = await this.resolveEndpoint(homeDir);
|
|
2319
|
+
const baseUrl = hasEndpointCredential(endpoint) ? endpoint.baseUrl : await readCodexBaseUrl(homeDir).catch(() => void 0);
|
|
1992
2320
|
this.ctx.logger.info(`subagent-codex: delegating via ${baseUrl ?? "codex default endpoint"}`);
|
|
1993
2321
|
const live = exec === void 0 ? this.liveDriver(runId) : void 0;
|
|
1994
2322
|
if (live !== void 0 && childSession !== void 0 && !live.disabled) {
|
|
@@ -2022,11 +2350,15 @@ var CodexCliProvider = class {
|
|
|
2022
2350
|
try {
|
|
2023
2351
|
const run = await startCodexCliRun(request, {
|
|
2024
2352
|
cwd,
|
|
2025
|
-
env: delegationEnv({
|
|
2353
|
+
env: delegationEnv({
|
|
2354
|
+
CODEX_HOME: homeDir,
|
|
2355
|
+
...endpointAuthEnv(endpoint)
|
|
2356
|
+
}),
|
|
2026
2357
|
...exec === void 0 ? {} : { exec },
|
|
2027
2358
|
endpointLabel: baseUrl,
|
|
2359
|
+
endpointArgv: endpointConfigOverrides(endpoint),
|
|
2028
2360
|
sandbox: this.sandbox,
|
|
2029
|
-
...configuration?.resolved ?? resolveRoundModel(this.overrides?.(runId) ?? requestedModel, this.model),
|
|
2361
|
+
...configuration?.resolved ?? resolveRoundModel(this.overrides?.(runId) ?? requestedModel, () => this.model?.(scope)),
|
|
2030
2362
|
disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
2031
2363
|
spawn: (spec) => this.ctx.subprocess.spawn(spec),
|
|
2032
2364
|
onError: (error, stopReason) => {
|
|
@@ -2072,7 +2404,8 @@ var CodexCliProvider = class {
|
|
|
2072
2404
|
const childSession = this.ctx.get("sessions")?.get(SessionId(intent.childSessionId));
|
|
2073
2405
|
if (childSession === void 0) throw new Error(`subagent-codex: resume target child session ${intent.childSessionId} is not live — start a fresh delegation instead`);
|
|
2074
2406
|
const nextTurn = childSession.snapshotEvents().filter((event) => event.type === "turn/start").length + 1;
|
|
2075
|
-
const
|
|
2407
|
+
const endpoint = await this.resolveEndpoint(homeDir);
|
|
2408
|
+
const baseUrl = hasEndpointCredential(endpoint) ? endpoint.baseUrl : await readCodexBaseUrl(homeDir).catch(() => void 0);
|
|
2076
2409
|
this.ctx.logger.info(`subagent-codex: resuming via ${baseUrl ?? "codex default endpoint"}`);
|
|
2077
2410
|
const live = exec === void 0 ? this.liveDriver(intent.childSessionId) : void 0;
|
|
2078
2411
|
if (live !== void 0 && !live.disabled) {
|
|
@@ -2106,11 +2439,15 @@ var CodexCliProvider = class {
|
|
|
2106
2439
|
try {
|
|
2107
2440
|
run = await startCodexCliRun(request, {
|
|
2108
2441
|
cwd,
|
|
2109
|
-
env: delegationEnv({
|
|
2442
|
+
env: delegationEnv({
|
|
2443
|
+
CODEX_HOME: homeDir,
|
|
2444
|
+
...endpointAuthEnv(endpoint)
|
|
2445
|
+
}),
|
|
2110
2446
|
...exec === void 0 ? {} : { exec },
|
|
2111
2447
|
endpointLabel: baseUrl,
|
|
2448
|
+
endpointArgv: endpointConfigOverrides(endpoint),
|
|
2112
2449
|
sandbox: this.sandbox,
|
|
2113
|
-
...configuration?.resolved ?? resolveRoundModel(this.overrides?.(intent.childSessionId) ?? requestedModel, this.model),
|
|
2450
|
+
...configuration?.resolved ?? resolveRoundModel(this.overrides?.(intent.childSessionId) ?? requestedModel, () => this.model?.(scope)),
|
|
2114
2451
|
disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
2115
2452
|
spawn: (spec) => this.ctx.subprocess.spawn(spec),
|
|
2116
2453
|
onError: (error, stopReason) => {
|
|
@@ -2278,6 +2615,23 @@ function foldCodexStreamLine(state, raw) {
|
|
|
2278
2615
|
result: last.result === void 0 ? output : `${last.result}\n${output}`
|
|
2279
2616
|
};
|
|
2280
2617
|
}
|
|
2618
|
+
} else if (item.type === "custom_tool_call") {
|
|
2619
|
+
countToolCall(state, "custom_tool_call");
|
|
2620
|
+
state.lines.push({
|
|
2621
|
+
kind: "tool",
|
|
2622
|
+
id: typeof item.id === "string" ? item.id : `codex-tool-${state.lines.length}`,
|
|
2623
|
+
name: item.name ?? "custom",
|
|
2624
|
+
...typeof item.input === "string" && item.input !== "" ? { args: item.input } : {}
|
|
2625
|
+
});
|
|
2626
|
+
} else if (item.type === "custom_tool_call_output") {
|
|
2627
|
+
const output = typeof item.output === "string" ? item.output : Array.isArray(item.output) ? item.output.filter((block) => typeof block === "object" && block !== null && typeof block.text === "string").map((block) => block.text).join("\n") : void 0;
|
|
2628
|
+
if (output !== void 0 && output.trim() !== "") {
|
|
2629
|
+
const last = state.lines[state.lines.length - 1];
|
|
2630
|
+
if (last !== void 0 && last.kind === "tool") state.lines[state.lines.length - 1] = {
|
|
2631
|
+
...last,
|
|
2632
|
+
result: last.result === void 0 ? output : `${last.result}\n${output}`
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2281
2635
|
}
|
|
2282
2636
|
}
|
|
2283
2637
|
/**
|
|
@@ -2312,7 +2666,8 @@ var CodexStreamParser = class {
|
|
|
2312
2666
|
* not put one on the wire; the settle path then reads the rollout's
|
|
2313
2667
|
* turn_context). Item types covered: `reasoning` (thinking), `agent_message`
|
|
2314
2668
|
* (reply text), `command_execution` (shell command with aggregated output),
|
|
2315
|
-
* `web_search_call`,
|
|
2669
|
+
* `web_search_call`, `custom_tool_call`/`custom_tool_call_output` (deferred
|
|
2670
|
+
* and dynamic tools), and `function_call_output`. The last `agent_message`
|
|
2316
2671
|
* wins as the run output (the final answer), and `turn.completed` carries the
|
|
2317
2672
|
* turn's usage. Malformed lines are skipped.
|
|
2318
2673
|
* @param stream - the collected stdout NDJSON text.
|
|
@@ -2357,11 +2712,13 @@ function startCodexCliRun(request, spec) {
|
|
|
2357
2712
|
if (request.signal.aborted) throw new Error("subagent-codex: request was aborted before the CLI started");
|
|
2358
2713
|
const turn = spec.resume?.turn ?? 1;
|
|
2359
2714
|
const memberArgv = spec.member === void 0 ? [] : ["-c", spec.member.configOverride];
|
|
2715
|
+
const endpointArgv = spec.endpointArgv ?? [];
|
|
2360
2716
|
const modelArgv = [...spec.model === void 0 ? [] : ["-m", spec.model], ...spec.effort === void 0 ? [] : ["-c", `model_reasoning_effort=${JSON.stringify(spec.effort)}`]];
|
|
2361
2717
|
const argv = spec.resume === void 0 ? [
|
|
2362
2718
|
"codex",
|
|
2363
2719
|
"exec",
|
|
2364
2720
|
...memberArgv,
|
|
2721
|
+
...endpointArgv,
|
|
2365
2722
|
...modelArgv,
|
|
2366
2723
|
"--sandbox",
|
|
2367
2724
|
spec.sandbox,
|
|
@@ -2372,6 +2729,7 @@ function startCodexCliRun(request, spec) {
|
|
|
2372
2729
|
"codex",
|
|
2373
2730
|
"exec",
|
|
2374
2731
|
...memberArgv,
|
|
2732
|
+
...endpointArgv,
|
|
2375
2733
|
...modelArgv,
|
|
2376
2734
|
"--sandbox",
|
|
2377
2735
|
spec.sandbox,
|
|
@@ -2401,6 +2759,11 @@ function startCodexCliRun(request, spec) {
|
|
|
2401
2759
|
});
|
|
2402
2760
|
const startedAtMs = Date.now();
|
|
2403
2761
|
spec.childSession?.append("turn/start", { turn });
|
|
2762
|
+
if (spec.model !== void 0 && spec.childSession !== void 0) appendCodexRequestHeader(spec.childSession, {
|
|
2763
|
+
provider: "codex-local",
|
|
2764
|
+
model: spec.model,
|
|
2765
|
+
...spec.effort === void 0 ? {} : { reasoningEffort: spec.effort }
|
|
2766
|
+
});
|
|
2404
2767
|
const liveMirror = spec.childSession !== void 0 && spec.ctx !== void 0 ? createCodexLiveMirror(spec, task, turn) : void 0;
|
|
2405
2768
|
let output = "";
|
|
2406
2769
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -2488,7 +2851,16 @@ function startCodexCliRun(request, spec) {
|
|
|
2488
2851
|
onAbort
|
|
2489
2852
|
}).then((settled) => {
|
|
2490
2853
|
if (settled.stopReason === "completed") spec.onAuthSuccess?.();
|
|
2854
|
+
return settled;
|
|
2855
|
+
});
|
|
2856
|
+
result.then(() => child.done).then(async () => {
|
|
2857
|
+
if (spec.onAuthFailure !== void 0) {
|
|
2858
|
+
const authDetail = `${child.collected.stderr?.readFrom(0).text || stderr}\n${child.collected.stdout?.readFrom(0).text || output}`;
|
|
2859
|
+
if (CODEX_AUTH_FAILURE.test(authDetail)) spec.onAuthFailure(authDetail.split("\n").find((line) => line.trim() !== "") ?? "auth failure");
|
|
2860
|
+
}
|
|
2861
|
+
await mirrorCodexAfterExit(spec, task, turn, output, liveMirror, startedAtMs);
|
|
2491
2862
|
if (spec.childSession !== void 0) {
|
|
2863
|
+
const settled = await result;
|
|
2492
2864
|
if (settled.stopReason === "completed") spec.childSession.append("turn/end", {
|
|
2493
2865
|
turn,
|
|
2494
2866
|
reason: { kind: "completed" }
|
|
@@ -2510,18 +2882,13 @@ function startCodexCliRun(request, spec) {
|
|
|
2510
2882
|
}
|
|
2511
2883
|
}
|
|
2512
2884
|
});
|
|
2885
|
+
if (spec.ctx !== void 0) await persistChildSession(spec.ctx, spec.childSession).catch((error) => {
|
|
2886
|
+
spec.ctx?.logger.warn(`subagent-codex: turn/end persistence failed: ${thrown(error).message}`);
|
|
2887
|
+
});
|
|
2513
2888
|
}
|
|
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
2889
|
}, () => {});
|
|
2523
2890
|
return Promise.resolve(subprocessRunHandle({
|
|
2524
|
-
id:
|
|
2891
|
+
id: SessionId(randomUUID()),
|
|
2525
2892
|
result,
|
|
2526
2893
|
signal: request.signal,
|
|
2527
2894
|
onAbort,
|
|
@@ -2529,6 +2896,22 @@ function startCodexCliRun(request, spec) {
|
|
|
2529
2896
|
teardown: disposeProcess
|
|
2530
2897
|
}));
|
|
2531
2898
|
}
|
|
2899
|
+
/**
|
|
2900
|
+
* Write the round's request/header when the config differs from the child
|
|
2901
|
+
* log's last one (the host logs changed snapshots, not per-call duplicates):
|
|
2902
|
+
* the first write is `initial`, a differing one `change`, an identical one
|
|
2903
|
+
* skipped. Context Lens reads the child log's config from these headers.
|
|
2904
|
+
*/
|
|
2905
|
+
function appendCodexRequestHeader(childSession, config) {
|
|
2906
|
+
const serialized = JSON.stringify(config);
|
|
2907
|
+
let last;
|
|
2908
|
+
for (const event of childSession.snapshotEvents()) if (event.type === "request/header") last = JSON.stringify(event.data.header?.config);
|
|
2909
|
+
if (last === serialized) return;
|
|
2910
|
+
childSession.append("request/header", {
|
|
2911
|
+
header: { config },
|
|
2912
|
+
reason: last === void 0 ? "initial" : "change"
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2532
2915
|
/** One assistant-role message event, attributed to the codex route. */
|
|
2533
2916
|
function codexAssistantEvent(blocks) {
|
|
2534
2917
|
return createAssistantMessage({
|
|
@@ -2545,8 +2928,17 @@ function codexAssistantEvent(blocks) {
|
|
|
2545
2928
|
* assembler requires: without a boundary the step never enters the location
|
|
2546
2929
|
* index, the line's assistant message resolves to a turn-level location, and
|
|
2547
2930
|
* the real-time view renders nothing (only a full rebuild, which mints step
|
|
2548
|
-
* drafts from the explicit coordinates, recovers it).
|
|
2549
|
-
*
|
|
2931
|
+
* drafts from the explicit coordinates, recovers it).
|
|
2932
|
+
*
|
|
2933
|
+
* A tool line folds the NATIVE lifecycle inside its one step: an assistant
|
|
2934
|
+
* message advertising the tool-call block, the `tool/call`, then its
|
|
2935
|
+
* `tool/result` — the released-log reader pairs call and result against the
|
|
2936
|
+
* advertisement and resolves every advertised call before the step closes.
|
|
2937
|
+
* Every fold sees a COMPLETED item (the exec live mirror holds the volatile
|
|
2938
|
+
* last line; the live driver folds at item/completed), so a result the
|
|
2939
|
+
* stream has not recorded by fold time will never arrive — the call folds
|
|
2940
|
+
* with the synthesized interrupted result instead of leaving the step
|
|
2941
|
+
* unresolvable.
|
|
2550
2942
|
* @returns whether the line folded (false when the options skipped it).
|
|
2551
2943
|
*/
|
|
2552
2944
|
function appendCodexTranscriptLine(childSession, turn, step, line, usage) {
|
|
@@ -2555,23 +2947,36 @@ function appendCodexTranscriptLine(childSession, turn, step, line, usage) {
|
|
|
2555
2947
|
step
|
|
2556
2948
|
});
|
|
2557
2949
|
if (line.kind === "tool") {
|
|
2950
|
+
const args = line.args ?? "";
|
|
2951
|
+
childSession.append("assistant/message", {
|
|
2952
|
+
turn,
|
|
2953
|
+
step,
|
|
2954
|
+
message: codexAssistantEvent([{
|
|
2955
|
+
type: "tool-call",
|
|
2956
|
+
id: line.id,
|
|
2957
|
+
name: line.name,
|
|
2958
|
+
arguments: args
|
|
2959
|
+
}]),
|
|
2960
|
+
stream: [],
|
|
2961
|
+
...usage === void 0 ? {} : { usage }
|
|
2962
|
+
}, { surfaceOp: "append" });
|
|
2558
2963
|
const call = childSession.append("tool/call", {
|
|
2559
2964
|
turn,
|
|
2560
2965
|
step,
|
|
2561
2966
|
callId: line.id,
|
|
2562
2967
|
name: line.name,
|
|
2563
|
-
arguments:
|
|
2968
|
+
arguments: args
|
|
2564
2969
|
});
|
|
2565
|
-
|
|
2970
|
+
childSession.append("tool/result", {
|
|
2566
2971
|
turn,
|
|
2567
2972
|
step,
|
|
2568
2973
|
message: createToolResultMessage({
|
|
2569
2974
|
callId: line.id,
|
|
2570
2975
|
content: [{
|
|
2571
2976
|
type: "text",
|
|
2572
|
-
text: line.result
|
|
2977
|
+
text: line.result ?? "(工具调用被中断,未能记录返回结果)"
|
|
2573
2978
|
}],
|
|
2574
|
-
isError:
|
|
2979
|
+
isError: line.result === void 0
|
|
2575
2980
|
})
|
|
2576
2981
|
}, {
|
|
2577
2982
|
surfaceOp: "append",
|
|
@@ -2881,7 +3286,8 @@ var LiveDriverSwitch = class {
|
|
|
2881
3286
|
this.active = next.live ? new CodexLiveDriver(this.ctx, {
|
|
2882
3287
|
...this.options.sandbox === void 0 ? {} : { sandbox: this.options.sandbox },
|
|
2883
3288
|
...this.options.liveIdleMs === void 0 ? {} : { liveIdleMs: this.options.liveIdleMs },
|
|
2884
|
-
...this.options.model === void 0 ? {} : { model: this.options.model }
|
|
3289
|
+
...this.options.model === void 0 ? {} : { model: this.options.model },
|
|
3290
|
+
...this.options.endpoint === void 0 ? {} : { endpoint: this.options.endpoint }
|
|
2885
3291
|
}) : void 0;
|
|
2886
3292
|
if (retiring !== void 0) {
|
|
2887
3293
|
this.draining.add(retiring);
|
|
@@ -2941,6 +3347,47 @@ function dedupeChoices(layers) {
|
|
|
2941
3347
|
}
|
|
2942
3348
|
return choices;
|
|
2943
3349
|
}
|
|
3350
|
+
/**
|
|
3351
|
+
* Scope the served directory to the surface asking for it (the same helper
|
|
3352
|
+
* lives in each provider's broker — fold into the family core when the
|
|
3353
|
+
* directory faces next evolve):
|
|
3354
|
+
*
|
|
3355
|
+
* - memberless (the card's add-model dialog, the invite picker): the
|
|
3356
|
+
* endpoint's served catalog only — the configuration/history extras the
|
|
3357
|
+
* broker's info extension adds for STATUS display must never become add
|
|
3358
|
+
* candidates;
|
|
3359
|
+
* - a member whose harness has a curated list: the list IS the vocabulary,
|
|
3360
|
+
* in the list's own order (the same rule `choices` already follows) — a
|
|
3361
|
+
* curated value the endpoint no longer serves stays as a bare row (the
|
|
3362
|
+
* card shows it the same way), and the member's current effective model is
|
|
3363
|
+
* kept visible as a bare entry when it sits outside the list, so a pre-list
|
|
3364
|
+
* binding never renders blank;
|
|
3365
|
+
* - a member without a curated list: the legacy extended vocabulary.
|
|
3366
|
+
*/
|
|
3367
|
+
function scopeServedDirectory(directory, opts) {
|
|
3368
|
+
if (opts.member && opts.curated.length === 0) return directory;
|
|
3369
|
+
const served = directory.entries.filter((entry) => entry.source === "native");
|
|
3370
|
+
if (!opts.member) return {
|
|
3371
|
+
...directory,
|
|
3372
|
+
entries: served
|
|
3373
|
+
};
|
|
3374
|
+
const byValue = new Map(served.flatMap((entry) => entry.resolvedModel === void 0 ? [[entry.value, entry]] : [[entry.value, entry], [entry.resolvedModel, entry]]));
|
|
3375
|
+
const entries = opts.curated.map((value) => byValue.get(value) ?? {
|
|
3376
|
+
value,
|
|
3377
|
+
label: value,
|
|
3378
|
+
source: "configuration"
|
|
3379
|
+
});
|
|
3380
|
+
const current = opts.current?.trim();
|
|
3381
|
+
if (current !== void 0 && current !== "" && !entries.some((entry) => entry.value === current || entry.resolvedModel === current)) entries.push({
|
|
3382
|
+
value: current,
|
|
3383
|
+
label: current,
|
|
3384
|
+
source: "configuration"
|
|
3385
|
+
});
|
|
3386
|
+
return {
|
|
3387
|
+
...directory,
|
|
3388
|
+
entries
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
2944
3391
|
var CodexModelBroker = class {
|
|
2945
3392
|
deps;
|
|
2946
3393
|
constructor(deps) {
|
|
@@ -2954,7 +3401,7 @@ var CodexModelBroker = class {
|
|
|
2954
3401
|
await this.deps.refreshDirectory?.(home, binding.cwd);
|
|
2955
3402
|
directory = this.deps.directory?.(home, binding.cwd);
|
|
2956
3403
|
}
|
|
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);
|
|
3404
|
+
const model = selection.model.mode === "value" ? selection.model.value : (selection.model.mode === "inherit" ? binding.model : void 0) ?? this.deps.settingsModel(home) ?? await readCodexModel(home) ?? this.deps.catalogDefault(home, binding.cwd);
|
|
2958
3405
|
const entry = directory?.entries.find((entry) => entry.value === model);
|
|
2959
3406
|
const explicitEffort = selection.effort.mode === "value" ? selection.effort.value : selection.effort.mode === "inherit" ? binding.effort : void 0;
|
|
2960
3407
|
const effort = explicitEffort ?? await readCodexReasoningEffort(home) ?? entry?.reasoning?.default;
|
|
@@ -2992,14 +3439,19 @@ var CodexModelBroker = class {
|
|
|
2992
3439
|
async modelDirectory(childSessionId, refresh = false) {
|
|
2993
3440
|
const home = this.deps.homeDir(childSessionId);
|
|
2994
3441
|
if (refresh) await this.deps.refreshDirectory?.(home, this.deps.cwd?.(childSessionId));
|
|
2995
|
-
|
|
3442
|
+
const info = await this.modelInfo(childSessionId, childSessionId === void 0 ? void 0 : this.deps.ctx.localAgent.getDelegation(childSessionId)?.model);
|
|
3443
|
+
return scopeServedDirectory(info.directory ?? {
|
|
2996
3444
|
entries: [],
|
|
2997
3445
|
complete: false,
|
|
2998
3446
|
customInput: true,
|
|
2999
3447
|
status: "unsupported",
|
|
3000
3448
|
refreshing: false,
|
|
3001
3449
|
revision: 0
|
|
3002
|
-
}
|
|
3450
|
+
}, {
|
|
3451
|
+
member: childSessionId !== void 0,
|
|
3452
|
+
curated: this.deps.modelList?.(home) ?? [],
|
|
3453
|
+
current: info.effective
|
|
3454
|
+
});
|
|
3003
3455
|
}
|
|
3004
3456
|
async *followModelDirectory(childSessionId, signal) {
|
|
3005
3457
|
if (this.deps.followDirectory === void 0) {
|
|
@@ -3019,7 +3471,7 @@ var CodexModelBroker = class {
|
|
|
3019
3471
|
if (override !== void 0) return override;
|
|
3020
3472
|
const recorded = this.deps.ctx.localAgent.getDelegation(childSessionId)?.model?.trim();
|
|
3021
3473
|
if (recorded !== void 0 && recorded !== "") return recorded;
|
|
3022
|
-
return this.deps.settingsModel();
|
|
3474
|
+
return this.deps.settingsModel(this.deps.homeDir(childSessionId));
|
|
3023
3475
|
}
|
|
3024
3476
|
/**
|
|
3025
3477
|
* Read the model surface. The cliDefault and the discovered identifiers come
|
|
@@ -3030,8 +3482,8 @@ var CodexModelBroker = class {
|
|
|
3030
3482
|
* re-fetches on open, so it never blocks on a CLI boot).
|
|
3031
3483
|
*/
|
|
3032
3484
|
async modelInfo(childSessionId, delegationModel) {
|
|
3033
|
-
const settings = this.deps.settingsModel();
|
|
3034
3485
|
const homeDir = this.deps.homeDir(childSessionId);
|
|
3486
|
+
const settings = this.deps.settingsModel(homeDir);
|
|
3035
3487
|
const [cliDefault, discovered] = await Promise.all([readCodexModel(homeDir).catch(() => void 0), listCodexConfigModels(homeDir).catch(() => [])]);
|
|
3036
3488
|
const override = childSessionId === void 0 ? void 0 : this.deps.overrides.get(childSessionId);
|
|
3037
3489
|
const delegation = delegationModel?.trim() === "" ? void 0 : delegationModel;
|
|
@@ -3039,6 +3491,7 @@ var CodexModelBroker = class {
|
|
|
3039
3491
|
const effective = override ?? delegation ?? settings ?? cliDefault ?? (catalogDefaultRaw === void 0 || catalogDefaultRaw === "" ? void 0 : catalogDefaultRaw);
|
|
3040
3492
|
const source = override !== void 0 ? "override" : delegation !== void 0 ? "delegation" : settings !== void 0 ? "settings" : cliDefault !== void 0 ? "cli-config" : "cli-builtin";
|
|
3041
3493
|
const inFlight = childSessionId !== void 0 && this.activeDelegations().includes(childSessionId);
|
|
3494
|
+
const curated = this.deps.modelList?.(homeDir) ?? [];
|
|
3042
3495
|
return {
|
|
3043
3496
|
...effective === void 0 ? {} : { effective },
|
|
3044
3497
|
source,
|
|
@@ -3050,8 +3503,8 @@ var CodexModelBroker = class {
|
|
|
3050
3503
|
settings,
|
|
3051
3504
|
cliDefault,
|
|
3052
3505
|
...discovered
|
|
3053
|
-
], this.deps.recentModels()) },
|
|
3054
|
-
choices: dedupeChoices([
|
|
3506
|
+
], curated.length > 0 ? [] : this.deps.recentModels()) },
|
|
3507
|
+
choices: curated.length > 0 ? [...curated] : dedupeChoices([
|
|
3055
3508
|
settings,
|
|
3056
3509
|
cliDefault,
|
|
3057
3510
|
...discovered,
|
|
@@ -3083,6 +3536,16 @@ var CodexModelBroker = class {
|
|
|
3083
3536
|
};
|
|
3084
3537
|
/** Grace between stdin EOF and terminate when the probe tears the process down. */
|
|
3085
3538
|
const PROBE_EOF_GRACE_MS = 1e3;
|
|
3539
|
+
/** One directory answer that names why it is empty. */
|
|
3540
|
+
function unsupported(reason) {
|
|
3541
|
+
return {
|
|
3542
|
+
entries: [],
|
|
3543
|
+
complete: false,
|
|
3544
|
+
customInput: true,
|
|
3545
|
+
unsupported: true,
|
|
3546
|
+
reason
|
|
3547
|
+
};
|
|
3548
|
+
}
|
|
3086
3549
|
/** Preserve native metadata, including hidden candidates; reject malformed pages. */
|
|
3087
3550
|
function catalogEntries(result) {
|
|
3088
3551
|
const page = result;
|
|
@@ -3138,7 +3601,7 @@ var CodexModelCatalog = class {
|
|
|
3138
3601
|
this.cache = new ModelDirectoryCache({
|
|
3139
3602
|
load: (key, signal) => {
|
|
3140
3603
|
const context = JSON.parse(key);
|
|
3141
|
-
return this.
|
|
3604
|
+
return this.load(context.homeDir, context.cwd, signal);
|
|
3142
3605
|
},
|
|
3143
3606
|
ttlMs: deps.ttlMs ?? 3e5,
|
|
3144
3607
|
...deps.now === void 0 ? {} : { now: deps.now },
|
|
@@ -3146,7 +3609,11 @@ var CodexModelCatalog = class {
|
|
|
3146
3609
|
});
|
|
3147
3610
|
}
|
|
3148
3611
|
key(homeDir, cwd = homeDir) {
|
|
3149
|
-
const files = [
|
|
3612
|
+
const files = [
|
|
3613
|
+
join(homeDir, "config.toml"),
|
|
3614
|
+
join(homeDir, "auth.json"),
|
|
3615
|
+
join(homeDir, ENDPOINT_AUTH_FILE)
|
|
3616
|
+
];
|
|
3150
3617
|
for (let directory = cwd;; directory = dirname(directory)) {
|
|
3151
3618
|
files.push(join(directory, ".codex", "config.toml"));
|
|
3152
3619
|
if (dirname(directory) === directory) break;
|
|
@@ -3181,16 +3648,69 @@ var CodexModelCatalog = class {
|
|
|
3181
3648
|
return (await this.cache.refresh(this.key(homeDir, cwd))).entries.filter((entry) => !entry.hidden).map((entry) => entry.value);
|
|
3182
3649
|
}
|
|
3183
3650
|
/**
|
|
3184
|
-
* One
|
|
3185
|
-
*
|
|
3186
|
-
*
|
|
3187
|
-
*
|
|
3651
|
+
* One directory load: the endpoint's served id list is the vocabulary (the
|
|
3652
|
+
* card's add dialog can only offer what the endpoint answers), folded
|
|
3653
|
+
* CONCURRENTLY with the native app-server probe's metadata. The endpoint
|
|
3654
|
+
* answers are deliberate:
|
|
3655
|
+
*
|
|
3656
|
+
* - no credential resolved → empty with a "save a key first" reason;
|
|
3657
|
+
* - 401/403 → empty with the rejection (definitive — cached as data);
|
|
3658
|
+
* - another HTTP status → empty with it (the relay serves no model route);
|
|
3659
|
+
* - a transport failure THROWS, so the cache keeps the last good list and
|
|
3660
|
+
* retries on schedule instead of blanking a momentarily-down endpoint
|
|
3661
|
+
* (the openproxy relay once 500'd and recovered — 2026-10-22).
|
|
3662
|
+
*
|
|
3663
|
+
* The native probe's failure never fails the load: endpoint data alone is
|
|
3664
|
+
* complete (it warns once via the deps sink). The endpoint fetch gets a
|
|
3665
|
+
* 20s ceiling by default (mirrors claude's catalog timeout); the native
|
|
3666
|
+
* probe keeps its own tighter bound internally.
|
|
3188
3667
|
*/
|
|
3189
|
-
async
|
|
3668
|
+
async load(homeDir, cwd, signal) {
|
|
3669
|
+
const endpoint = this.deps.endpoint === void 0 ? void 0 : await this.deps.endpoint(homeDir).catch(() => void 0);
|
|
3670
|
+
if (!hasEndpointCredential(endpoint)) return unsupported("No endpoint credential configured — the model list loads from the endpoint once an API Key is saved");
|
|
3671
|
+
const combined = AbortSignal.any([signal, AbortSignal.timeout(this.deps.timeoutMs ?? 2e4)]);
|
|
3672
|
+
const [result, native] = await Promise.all([fetchEndpointModels({
|
|
3673
|
+
...endpoint,
|
|
3674
|
+
baseUrl: endpoint.baseUrl ?? "https://api.openai.com"
|
|
3675
|
+
}, this.deps.fetchImpl, combined), this.probeNative(homeDir, cwd, combined).catch((error) => {
|
|
3676
|
+
this.deps.warn?.(`local-agent-codex: the native model/list probe failed (metadata only — the endpoint's list stands): ${error instanceof Error ? error.message : String(error)}`);
|
|
3677
|
+
})]);
|
|
3678
|
+
if (result.kind === "unreachable") throw new Error(`endpoint unreachable: ${result.message}`);
|
|
3679
|
+
if (result.kind === "malformed") return unsupported("The endpoint answered but the model list did not parse (not /v1/models-shaped)");
|
|
3680
|
+
if (result.kind === "http") {
|
|
3681
|
+
if (result.status === 401 || result.status === 403) return unsupported(`The endpoint rejected the credential (HTTP ${String(result.status)}) — the list loads once a valid key is saved`);
|
|
3682
|
+
return unsupported(`The endpoint does not serve a model list (HTTP ${String(result.status)})`);
|
|
3683
|
+
}
|
|
3684
|
+
if (result.models.length === 0) return unsupported("The endpoint serves no models");
|
|
3685
|
+
const nativeByValue = new Map((native?.entries ?? []).map((entry) => [entry.value, entry]));
|
|
3686
|
+
const entries = result.models.map((id) => nativeByValue.get(id) ?? {
|
|
3687
|
+
value: id,
|
|
3688
|
+
label: id,
|
|
3689
|
+
source: "native"
|
|
3690
|
+
});
|
|
3691
|
+
const defaultModel = native?.defaultModel;
|
|
3692
|
+
return {
|
|
3693
|
+
entries,
|
|
3694
|
+
complete: true,
|
|
3695
|
+
customInput: true,
|
|
3696
|
+
...defaultModel === void 0 ? {} : { defaultModel }
|
|
3697
|
+
};
|
|
3698
|
+
}
|
|
3699
|
+
/**
|
|
3700
|
+
* One bounded NATIVE probe — the metadata-enrichment side of {@link load}:
|
|
3701
|
+
* spawn the app-server against the scoped home, answer the initialize
|
|
3702
|
+
* handshake exactly as the live driver shapes it, ask `model/list`, then
|
|
3703
|
+
* tear the process down (stdin EOF → grace → terminate). The overall
|
|
3704
|
+
* timeout kills the process on expiry. Failures propagate to the caller
|
|
3705
|
+
* (load demotes them to a warn).
|
|
3706
|
+
*/
|
|
3707
|
+
async probeNative(homeDir, cwd, signal) {
|
|
3708
|
+
const endpoint = this.deps.endpoint === void 0 ? void 0 : await this.deps.endpoint(homeDir).catch(() => void 0);
|
|
3190
3709
|
const spec = {
|
|
3191
3710
|
argv: [
|
|
3192
3711
|
"codex",
|
|
3193
3712
|
"app-server",
|
|
3713
|
+
...endpointConfigOverrides(endpoint),
|
|
3194
3714
|
"--stdio"
|
|
3195
3715
|
],
|
|
3196
3716
|
cwd,
|
|
@@ -3200,7 +3720,10 @@ var CodexModelCatalog = class {
|
|
|
3200
3720
|
stderr: "pipe"
|
|
3201
3721
|
},
|
|
3202
3722
|
graceMs: DEFAULT_DISPOSE_GRACE_MS,
|
|
3203
|
-
env: delegationEnv({
|
|
3723
|
+
env: delegationEnv({
|
|
3724
|
+
CODEX_HOME: homeDir,
|
|
3725
|
+
...endpointAuthEnv(endpoint)
|
|
3726
|
+
})
|
|
3204
3727
|
};
|
|
3205
3728
|
const child = this.deps.spawn(spec);
|
|
3206
3729
|
const decoder = new StringDecoder("utf8");
|
|
@@ -3337,8 +3860,8 @@ var CodexModelCatalog = class {
|
|
|
3337
3860
|
/**
|
|
3338
3861
|
* Codex harness instantiation of the local-agent family. Registers the
|
|
3339
3862
|
* `codex` harness into the core registry: scoped home under the shared homes
|
|
3340
|
-
* root (`CODEX_HOME`),
|
|
3341
|
-
*
|
|
3863
|
+
* root (`CODEX_HOME`), endpoint-key authentication (endpoint-auth.ts — no web
|
|
3864
|
+
* login anywhere), and rollout-file session records.
|
|
3342
3865
|
* Delegation mounts through the `codex-local` provider (one-shot
|
|
3343
3866
|
* `codex exec` under the scoped home); the bundle patch's
|
|
3344
3867
|
* `tool-subagent-codex-local` row puts the tool on the profile root, so
|
|
@@ -3350,7 +3873,8 @@ var CodexModelCatalog = class {
|
|
|
3350
3873
|
const name = "local-agent-codex";
|
|
3351
3874
|
/**
|
|
3352
3875
|
* Boot delay before the model catalog's eager warmup probe fires. The probe
|
|
3353
|
-
* spawns a `codex app-server`
|
|
3876
|
+
* fetches the endpoint's served model list AND spawns a `codex app-server`
|
|
3877
|
+
* (the native metadata side) — cheap, but not free — so it waits out the
|
|
3354
3878
|
* boot storm instead of competing with it, and the FIRST settings-card open
|
|
3355
3879
|
* usually hits a warm cache. A card opened before the timer fires simply
|
|
3356
3880
|
* triggers the probe itself (the read path's lazy kick), so the delay never
|
|
@@ -3393,6 +3917,8 @@ const Config = z.object({
|
|
|
3393
3917
|
z.const("workspace-write"),
|
|
3394
3918
|
z.const("danger-full-access")
|
|
3395
3919
|
]),
|
|
3920
|
+
baseUrl: z.string(),
|
|
3921
|
+
apiKey: z.string(),
|
|
3396
3922
|
model: volatilize(SETTINGS_FIELDS.model),
|
|
3397
3923
|
live: volatilize(SETTINGS_FIELDS.live),
|
|
3398
3924
|
liveIdleMs: z.number().default(DEFAULT_LIVE_IDLE_MS),
|
|
@@ -3449,14 +3975,22 @@ function apply(ctx, config) {
|
|
|
3449
3975
|
legacyBase,
|
|
3450
3976
|
read: readSettings
|
|
3451
3977
|
});
|
|
3452
|
-
const
|
|
3978
|
+
const resolveModelKey = () => {
|
|
3453
3979
|
const model = face.get().model?.trim();
|
|
3454
3980
|
return model === void 0 || model === "" ? void 0 : model;
|
|
3455
3981
|
};
|
|
3982
|
+
const resolveModelFor = (homeDir) => firstModelOf(readModelListSync(homeDir)) ?? resolveModelKey();
|
|
3983
|
+
const resolveModel = (scope) => resolveModelFor(ctx.localAgent.homeDir("codex", scope));
|
|
3456
3984
|
const memberModelOverrides = /* @__PURE__ */ new Map();
|
|
3985
|
+
const endpointConfig = {
|
|
3986
|
+
...config.baseUrl === void 0 ? {} : { baseUrl: config.baseUrl },
|
|
3987
|
+
...config.apiKey === void 0 ? {} : { apiKey: config.apiKey }
|
|
3988
|
+
};
|
|
3989
|
+
const resolveEndpoint = (scopeHome) => resolveEndpointAuth(scopeHome, endpointConfig);
|
|
3457
3990
|
const modelCatalog = new CodexModelCatalog({
|
|
3458
3991
|
spawn: (spec) => ctx.subprocess.spawn(spec),
|
|
3459
|
-
warn: (message) => ctx.logger.warn(message)
|
|
3992
|
+
warn: (message) => ctx.logger.warn(message),
|
|
3993
|
+
endpoint: resolveEndpoint
|
|
3460
3994
|
});
|
|
3461
3995
|
const warmup = setTimeout(() => {
|
|
3462
3996
|
modelCatalog.read(homeDir);
|
|
@@ -3464,12 +3998,14 @@ function apply(ctx, config) {
|
|
|
3464
3998
|
warmup.unref();
|
|
3465
3999
|
const liveSwitch = new LiveDriverSwitch(ctx, face, {
|
|
3466
4000
|
sandbox,
|
|
3467
|
-
model: (childSessionId) => memberModelOverrides.get(childSessionId) ?? resolveModel(),
|
|
4001
|
+
model: (childSessionId) => memberModelOverrides.get(childSessionId) ?? resolveModel((ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.scope),
|
|
4002
|
+
endpoint: resolveEndpoint,
|
|
3468
4003
|
...config.liveIdleMs === void 0 ? {} : { liveIdleMs: config.liveIdleMs }
|
|
3469
4004
|
});
|
|
3470
4005
|
const modelBroker = new CodexModelBroker({
|
|
3471
4006
|
ctx,
|
|
3472
|
-
settingsModel:
|
|
4007
|
+
settingsModel: resolveModelFor,
|
|
4008
|
+
modelList: (homeDir) => readModelListSync(homeDir),
|
|
3473
4009
|
recentModels: () => face.get().recentModels ?? [],
|
|
3474
4010
|
homeDir: (childSessionId) => ctx.localAgent.homeDir("codex", childSessionId === void 0 ? void 0 : (ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.scope),
|
|
3475
4011
|
cwd: (childSessionId) => childSessionId === void 0 ? void 0 : (ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.cwd,
|
|
@@ -3483,7 +4019,7 @@ function apply(ctx, config) {
|
|
|
3483
4019
|
catalog: (scopedHome, cwd) => modelCatalog.read(scopedHome, cwd),
|
|
3484
4020
|
catalogDefault: (scopedHome, cwd) => modelCatalog.readDefault(scopedHome, cwd)
|
|
3485
4021
|
});
|
|
3486
|
-
const disposeProvider = ctx.subagents.registerProvider(new CodexCliProvider(ctx, sandbox, liveSwitch.resolve, resolveModel, (childSessionId) => memberModelOverrides.get(childSessionId)));
|
|
4022
|
+
const disposeProvider = ctx.subagents.registerProvider(new CodexCliProvider(ctx, sandbox, liveSwitch.resolve, resolveModel, (childSessionId) => memberModelOverrides.get(childSessionId), resolveEndpoint));
|
|
3487
4023
|
const disposeHarness = ctx.localAgent.register({
|
|
3488
4024
|
prepareMember: async ({ binding, childSession, configuration, signal }) => {
|
|
3489
4025
|
const driver = liveSwitch.resolve(binding.childSessionId);
|
|
@@ -3501,23 +4037,53 @@ function apply(ctx, config) {
|
|
|
3501
4037
|
homeEnvVar: "CODEX_HOME",
|
|
3502
4038
|
delegationProvider: "codex-local",
|
|
3503
4039
|
modelBroker,
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
}
|
|
4040
|
+
modelList: {
|
|
4041
|
+
read: (home) => readModelList(home),
|
|
4042
|
+
write: (home, models) => writeModelList(home, models)
|
|
4043
|
+
},
|
|
3508
4044
|
records: { listSessions: (homeDir) => listCodexSessions(homeDir) },
|
|
3509
4045
|
provision: (scopedHome) => provisionCodexConfig(scopedHome).then(() => {}),
|
|
3510
|
-
isAuthenticated:
|
|
3511
|
-
|
|
3512
|
-
|
|
4046
|
+
isAuthenticated: async (home) => hasEndpointCredential(await resolveEndpoint(home)),
|
|
4047
|
+
checkEndpoint: (home) => resolveEndpoint(home).then((endpoint) => checkCodexEndpoint(endpoint)),
|
|
4048
|
+
endpointAuth: {
|
|
4049
|
+
read: async (home) => {
|
|
4050
|
+
const endpoint = await resolveEndpoint(home);
|
|
4051
|
+
return {
|
|
4052
|
+
...endpoint.baseUrl === void 0 ? {} : { baseUrl: endpoint.baseUrl },
|
|
4053
|
+
apiKeySet: endpoint.apiKey !== void 0,
|
|
4054
|
+
authTokenSet: false
|
|
4055
|
+
};
|
|
4056
|
+
},
|
|
4057
|
+
write: async (home, fields) => {
|
|
4058
|
+
const next = { ...await readEndpointAuthFile(home) ?? {} };
|
|
4059
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
4060
|
+
if (field !== "baseUrl" && field !== "apiKey") continue;
|
|
4061
|
+
if (value === void 0) continue;
|
|
4062
|
+
const trimmed = typeof value === "string" ? value.trim() : "";
|
|
4063
|
+
if (value === null || trimmed === "") delete next[field];
|
|
4064
|
+
else next[field] = trimmed;
|
|
4065
|
+
}
|
|
4066
|
+
if (Object.keys(next).length === 0) await clearEndpointAuth(home);
|
|
4067
|
+
else await writeEndpointAuth(home, next);
|
|
4068
|
+
}
|
|
4069
|
+
},
|
|
4070
|
+
credentialStamp: endpointAuthStamp,
|
|
4071
|
+
logout: async (home) => {
|
|
4072
|
+
if (await endpointAuthFileExists(home)) {
|
|
4073
|
+
await clearEndpointAuth(home);
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
throw new Error("codex has no scoped endpoint-auth.json to clear — the credential comes from the settings card or the profile row (baseUrl/apiKey); clear it there instead");
|
|
4077
|
+
},
|
|
3513
4078
|
effectiveSettings: async (scopedHome) => {
|
|
3514
|
-
const [reasoningEffort,
|
|
4079
|
+
const [reasoningEffort, endpoint, scopedModel, cliVersion] = await Promise.all([
|
|
3515
4080
|
readCodexReasoningEffort(scopedHome).catch(() => void 0),
|
|
3516
|
-
|
|
4081
|
+
resolveEndpoint(scopedHome).catch(() => void 0),
|
|
3517
4082
|
readCodexModel(scopedHome).catch(() => void 0),
|
|
3518
4083
|
codexCliVersion(ctx, scopedHome).catch(() => void 0)
|
|
3519
4084
|
]);
|
|
3520
|
-
const
|
|
4085
|
+
const baseUrl = hasEndpointCredential(endpoint) ? endpoint.baseUrl : await readCodexBaseUrl(scopedHome).catch(() => void 0);
|
|
4086
|
+
const model = resolveModelFor(scopedHome) ?? scopedModel;
|
|
3521
4087
|
const baseUrlHost = baseUrl !== void 0 ? endpointHost(baseUrl) : void 0;
|
|
3522
4088
|
return {
|
|
3523
4089
|
drive: face.get().live ? "live" : "exec",
|