codex-multi-auth 2.1.2 → 2.1.4
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/README.md +4 -2
- package/dist/lib/auto-update-checker.d.ts.map +1 -1
- package/dist/lib/auto-update-checker.js +6 -1
- package/dist/lib/auto-update-checker.js.map +1 -1
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +13 -5
- package/dist/lib/request/fetch-helpers.js.map +1 -1
- package/dist/lib/runtime/app-bind.d.ts.map +1 -1
- package/dist/lib/runtime/app-bind.js +16 -1
- package/dist/lib/runtime/app-bind.js.map +1 -1
- package/dist/lib/runtime/config-toml.d.ts +2 -0
- package/dist/lib/runtime/config-toml.d.ts.map +1 -1
- package/dist/lib/runtime/config-toml.js +57 -4
- package/dist/lib/runtime/config-toml.js.map +1 -1
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +199 -8
- package/dist/lib/runtime-rotation-proxy.js.map +1 -1
- package/dist/lib/usage/ledger.d.ts.map +1 -1
- package/dist/lib/usage/ledger.js +1 -0
- package/dist/lib/usage/ledger.js.map +1 -1
- package/dist/lib/usage/redaction.d.ts.map +1 -1
- package/dist/lib/usage/redaction.js +1 -0
- package/dist/lib/usage/redaction.js.map +1 -1
- package/dist/lib/usage/types.d.ts +1 -1
- package/dist/lib/usage/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/scripts/codex-multi-auth.js +4 -0
- package/scripts/codex-routing.js +8 -0
- package/scripts/codex.js +292 -6
- package/scripts/test-model-matrix.js +97 -13
package/scripts/codex.js
CHANGED
|
@@ -89,6 +89,7 @@ const shadowHomeCleanupRetryMarkerDir =
|
|
|
89
89
|
(process.env.CODEX_MULTI_AUTH_TEST_SHADOW_RETRY_MARKER_DIR ?? "").trim();
|
|
90
90
|
let warnedInvalidRuntimeRotationProxyEnv = false;
|
|
91
91
|
let warnedPendingAccountReadIdOverflow = false;
|
|
92
|
+
let warnedShadowHomeSqliteLinkFailure = false;
|
|
92
93
|
|
|
93
94
|
async function loadRuntimeConstants() {
|
|
94
95
|
const fallback = {
|
|
@@ -579,6 +580,9 @@ function shouldCaptureForwardedCodexOutput(env = process.env) {
|
|
|
579
580
|
if (override === "0") {
|
|
580
581
|
return false;
|
|
581
582
|
}
|
|
583
|
+
if ((env.CODEX_CI ?? "").trim() === "1") {
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
582
586
|
// Windows child processes can report undefined isTTY; treat that as non-TTY so retry capture remains available.
|
|
583
587
|
return process.stdout.isTTY !== true || process.stderr.isTTY !== true;
|
|
584
588
|
}
|
|
@@ -764,6 +768,57 @@ function rewriteAppServerAccountReadResponseLine(line, pendingAuthRequestMethods
|
|
|
764
768
|
})}${lineEnding}`;
|
|
765
769
|
}
|
|
766
770
|
|
|
771
|
+
function filterKnownForwardedCodexStderr(text) {
|
|
772
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
773
|
+
return "";
|
|
774
|
+
}
|
|
775
|
+
const lines = text.split(/\r?\n/);
|
|
776
|
+
const filtered = [];
|
|
777
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
778
|
+
const line = lines[index];
|
|
779
|
+
if (
|
|
780
|
+
/ERROR codex_core::session: failed to record rollout items: thread\s*$/.test(
|
|
781
|
+
line,
|
|
782
|
+
) &&
|
|
783
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} not found$/i.test(
|
|
784
|
+
lines[index + 1] ?? "",
|
|
785
|
+
)
|
|
786
|
+
) {
|
|
787
|
+
index += 1;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (
|
|
791
|
+
/ERROR rmcp::transport::streamable_http_client: fail to delete session:\s*$/.test(
|
|
792
|
+
line,
|
|
793
|
+
) &&
|
|
794
|
+
/^unexpected server response: DELETE returned HTTP 404 session_id="/.test(
|
|
795
|
+
lines[index + 1] ?? "",
|
|
796
|
+
)
|
|
797
|
+
) {
|
|
798
|
+
index += 1;
|
|
799
|
+
while (index + 1 < lines.length && !/"\s*$/.test(lines[index])) {
|
|
800
|
+
index += 1;
|
|
801
|
+
}
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
filtered.push(line);
|
|
805
|
+
}
|
|
806
|
+
return filtered.join("\n");
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function withCiKnownForwardedCodexLogSilencers(env) {
|
|
810
|
+
if ((env.CODEX_CI ?? "").trim() !== "1") {
|
|
811
|
+
return env;
|
|
812
|
+
}
|
|
813
|
+
if ((env.RUST_LOG ?? "").trim().length > 0) {
|
|
814
|
+
return env;
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
...env,
|
|
818
|
+
RUST_LOG: "codex_core::session=off",
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
767
822
|
function forwardToRealCodexOnce(
|
|
768
823
|
codexBin,
|
|
769
824
|
args,
|
|
@@ -794,6 +849,21 @@ function forwardToRealCodexOnce(
|
|
|
794
849
|
} catch {
|
|
795
850
|
// Best-effort cleanup only.
|
|
796
851
|
}
|
|
852
|
+
if (captureOutput && stdout.length > 0) {
|
|
853
|
+
const filteredStdout = filterKnownForwardedCodexStderr(stdout);
|
|
854
|
+
if (filteredStdout.length > 0) {
|
|
855
|
+
process.stdout.write(filteredStdout);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (captureOutput && stderr.length > 0) {
|
|
859
|
+
const filteredStderr = filterKnownForwardedCodexStderr(stderr);
|
|
860
|
+
if (filteredStderr.trim().length > 0) {
|
|
861
|
+
process.stderr.write(filteredStderr);
|
|
862
|
+
if (!filteredStderr.endsWith("\n")) {
|
|
863
|
+
process.stderr.write("\n");
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
797
867
|
resolve({
|
|
798
868
|
exitCode,
|
|
799
869
|
// No-capture output stays empty by design so retry parsing cannot
|
|
@@ -812,13 +882,16 @@ function forwardToRealCodexOnce(
|
|
|
812
882
|
finalize(1);
|
|
813
883
|
};
|
|
814
884
|
try {
|
|
885
|
+
const childEnv = captureOutput
|
|
886
|
+
? withCiKnownForwardedCodexLogSilencers(env)
|
|
887
|
+
: env;
|
|
815
888
|
child = spawn(command, commandArgs, {
|
|
816
889
|
stdio: proxyAppServerAccountRead
|
|
817
890
|
? ["pipe", "pipe", "pipe"]
|
|
818
891
|
: captureOutput
|
|
819
892
|
? ["inherit", "pipe", "pipe"]
|
|
820
893
|
: "inherit",
|
|
821
|
-
env,
|
|
894
|
+
env: childEnv,
|
|
822
895
|
});
|
|
823
896
|
} catch (error) {
|
|
824
897
|
failLaunch(error);
|
|
@@ -883,12 +956,10 @@ function forwardToRealCodexOnce(
|
|
|
883
956
|
child.stdout?.on("data", (chunk) => {
|
|
884
957
|
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
885
958
|
stdout += text;
|
|
886
|
-
process.stdout.write(chunk);
|
|
887
959
|
});
|
|
888
960
|
child.stderr?.on("data", (chunk) => {
|
|
889
961
|
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
890
962
|
stderr += text;
|
|
891
|
-
process.stderr.write(chunk);
|
|
892
963
|
});
|
|
893
964
|
}
|
|
894
965
|
|
|
@@ -948,6 +1019,7 @@ async function forwardToRealCodex(codexBin, rawArgs, baseEnv = process.env) {
|
|
|
948
1019
|
);
|
|
949
1020
|
lastExitCode = result.exitCode;
|
|
950
1021
|
if (result.exitCode === 0) {
|
|
1022
|
+
repairCodexSessionIndex(resolveCodexHomeDir(baseEnv));
|
|
951
1023
|
return result.exitCode;
|
|
952
1024
|
}
|
|
953
1025
|
|
|
@@ -1831,6 +1903,25 @@ function mirrorFileIntoShadowHome(sourcePath, destinationPath, tightenFile) {
|
|
|
1831
1903
|
tightenFile(destinationPath);
|
|
1832
1904
|
}
|
|
1833
1905
|
|
|
1906
|
+
function shouldMaterializeFileIntoShadowHome(name) {
|
|
1907
|
+
return /\.sqlite(?:-(?:shm|wal))?$/i.test(name);
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
function materializeFileIntoShadowHome(sourcePath, destinationPath) {
|
|
1911
|
+
try {
|
|
1912
|
+
linkSync(sourcePath, destinationPath);
|
|
1913
|
+
return true;
|
|
1914
|
+
} catch {
|
|
1915
|
+
if (!warnedShadowHomeSqliteLinkFailure) {
|
|
1916
|
+
warnedShadowHomeSqliteLinkFailure = true;
|
|
1917
|
+
console.error(
|
|
1918
|
+
"codex-multi-auth: skipped SQLite shadow-home materialization because hard-linking failed; refusing to copy active SQLite state.",
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
return false;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1834
1925
|
function collectShadowHomeSyncFileNames(shadowCodexHome, syncFileNames) {
|
|
1835
1926
|
try {
|
|
1836
1927
|
for (const entry of readdirSync(shadowCodexHome, { withFileTypes: true })) {
|
|
@@ -2004,6 +2095,7 @@ function createShadowHomeMirror(
|
|
|
2004
2095
|
continue;
|
|
2005
2096
|
}
|
|
2006
2097
|
const isKnownStateFile = SHADOW_HOME_STATE_FILE_SET.has(name);
|
|
2098
|
+
const shouldMaterializeFile = shouldMaterializeFileIntoShadowHome(name);
|
|
2007
2099
|
const sourcePath = join(originalCodexHome, name);
|
|
2008
2100
|
const destinationPath = join(shadowCodexHome, name);
|
|
2009
2101
|
if (existsSync(destinationPath)) {
|
|
@@ -2032,6 +2124,8 @@ function createShadowHomeMirror(
|
|
|
2032
2124
|
if (isKnownStateFile) {
|
|
2033
2125
|
copyFileSync(sourcePath, destinationPath);
|
|
2034
2126
|
tightenFile(destinationPath);
|
|
2127
|
+
} else if (shouldMaterializeFile) {
|
|
2128
|
+
materializeFileIntoShadowHome(sourcePath, destinationPath);
|
|
2035
2129
|
} else {
|
|
2036
2130
|
mirrorFileIntoShadowHome(sourcePath, destinationPath, tightenFile);
|
|
2037
2131
|
}
|
|
@@ -2195,6 +2289,16 @@ function resolveRuntimeRotationProxyOriginalCodexHome(baseEnv) {
|
|
|
2195
2289
|
return override || resolveCodexHomeDir(baseEnv);
|
|
2196
2290
|
}
|
|
2197
2291
|
|
|
2292
|
+
function createRuntimeRotationShadowHome(originalCodexHome) {
|
|
2293
|
+
const shadowRoot = join(
|
|
2294
|
+
originalCodexHome,
|
|
2295
|
+
"multi-auth",
|
|
2296
|
+
"runtime-shadow-homes",
|
|
2297
|
+
);
|
|
2298
|
+
mkdirSync(shadowRoot, { recursive: true });
|
|
2299
|
+
return mkdtempSync(join(shadowRoot, "codex-multi-auth-runtime-home-"));
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2198
2302
|
function createRuntimeRotationProxyCodexHome(
|
|
2199
2303
|
baseEnv,
|
|
2200
2304
|
proxyBaseUrl,
|
|
@@ -2202,7 +2306,7 @@ function createRuntimeRotationProxyCodexHome(
|
|
|
2202
2306
|
configTomlModule,
|
|
2203
2307
|
) {
|
|
2204
2308
|
const originalCodexHome = resolveRuntimeRotationProxyOriginalCodexHome(baseEnv);
|
|
2205
|
-
const shadowCodexHome =
|
|
2309
|
+
const shadowCodexHome = createRuntimeRotationShadowHome(originalCodexHome);
|
|
2206
2310
|
let syncShadowHomeStateBack = () => {};
|
|
2207
2311
|
const cleanup = () => {
|
|
2208
2312
|
try {
|
|
@@ -2770,7 +2874,11 @@ function startRuntimeRotationAppHelper(baseContext) {
|
|
|
2770
2874
|
});
|
|
2771
2875
|
}
|
|
2772
2876
|
|
|
2773
|
-
async function createRuntimeRotationAppHelperContext(
|
|
2877
|
+
async function createRuntimeRotationAppHelperContext(
|
|
2878
|
+
baseContext,
|
|
2879
|
+
configTomlModule,
|
|
2880
|
+
options = {},
|
|
2881
|
+
) {
|
|
2774
2882
|
const startedAt = Date.now();
|
|
2775
2883
|
const { helper, message } = await startRuntimeRotationAppHelper(baseContext);
|
|
2776
2884
|
const helperEnv = message.env ?? {};
|
|
@@ -2778,7 +2886,7 @@ async function createRuntimeRotationAppHelperContext(baseContext, configTomlModu
|
|
|
2778
2886
|
|
|
2779
2887
|
const cleanup = async ({ exitCode } = {}) => {
|
|
2780
2888
|
const livedMs = Date.now() - startedAt;
|
|
2781
|
-
if (exitCode === 0 && livedMs < detachGraceMs) {
|
|
2889
|
+
if (exitCode === 0 && (options.detachOnExit === true || livedMs < detachGraceMs)) {
|
|
2782
2890
|
helper.stdout?.destroy();
|
|
2783
2891
|
helper.stderr?.destroy();
|
|
2784
2892
|
helper.unref();
|
|
@@ -2827,6 +2935,11 @@ async function createRuntimeRotationProxyContextIfEnabled(
|
|
|
2827
2935
|
if (isCodexAppCommand(rawArgs)) {
|
|
2828
2936
|
return createRuntimeRotationAppHelperContext(baseContext, configTomlModule);
|
|
2829
2937
|
}
|
|
2938
|
+
if (isCodexInteractiveTuiCommand(rawArgs)) {
|
|
2939
|
+
return createRuntimeRotationAppHelperContext(baseContext, configTomlModule, {
|
|
2940
|
+
detachOnExit: true,
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2830
2943
|
|
|
2831
2944
|
const proxyModule = await loadRuntimeRotationProxyModule();
|
|
2832
2945
|
if (!proxyModule) {
|
|
@@ -3021,6 +3134,10 @@ function isCodexAppServerCommand(rawArgs) {
|
|
|
3021
3134
|
return findForwardedCommand(rawArgs)?.command === "app-server";
|
|
3022
3135
|
}
|
|
3023
3136
|
|
|
3137
|
+
function isCodexInteractiveTuiCommand(rawArgs) {
|
|
3138
|
+
return findForwardedCommand(rawArgs) === null;
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3024
3141
|
function shouldUseRuntimeRoutingForForwardedArgs(rawArgs) {
|
|
3025
3142
|
if (!Array.isArray(rawArgs) || rawArgs.length === 0) {
|
|
3026
3143
|
return true;
|
|
@@ -3273,6 +3390,175 @@ function normalizeExitCode(value) {
|
|
|
3273
3390
|
return 1;
|
|
3274
3391
|
}
|
|
3275
3392
|
|
|
3393
|
+
function extractRolloutIdFromFilename(fileName) {
|
|
3394
|
+
const match = fileName.match(
|
|
3395
|
+
/^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i,
|
|
3396
|
+
);
|
|
3397
|
+
return match?.[1] ?? null;
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
function collectRolloutFiles(rootDir, results = []) {
|
|
3401
|
+
let entries = [];
|
|
3402
|
+
try {
|
|
3403
|
+
entries = readdirSync(rootDir, { withFileTypes: true });
|
|
3404
|
+
} catch {
|
|
3405
|
+
return results;
|
|
3406
|
+
}
|
|
3407
|
+
for (const entry of entries) {
|
|
3408
|
+
const entryPath = join(rootDir, entry.name);
|
|
3409
|
+
if (entry.isDirectory()) {
|
|
3410
|
+
collectRolloutFiles(entryPath, results);
|
|
3411
|
+
continue;
|
|
3412
|
+
}
|
|
3413
|
+
if (entry.isFile() && extractRolloutIdFromFilename(entry.name)) {
|
|
3414
|
+
results.push(entryPath);
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
return results;
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
function parseRolloutIndexEntry(rolloutPath) {
|
|
3421
|
+
const fileName = basename(rolloutPath);
|
|
3422
|
+
const idFromName = extractRolloutIdFromFilename(fileName);
|
|
3423
|
+
if (!idFromName) return null;
|
|
3424
|
+
let content = "";
|
|
3425
|
+
try {
|
|
3426
|
+
content = readFileSync(rolloutPath, "utf8");
|
|
3427
|
+
} catch {
|
|
3428
|
+
return null;
|
|
3429
|
+
}
|
|
3430
|
+
const lines = content.split(/\r?\n/).filter(Boolean);
|
|
3431
|
+
let id = idFromName;
|
|
3432
|
+
let threadName = "";
|
|
3433
|
+
let updatedAt = null;
|
|
3434
|
+
let hasSessionMeta = false;
|
|
3435
|
+
for (const line of lines) {
|
|
3436
|
+
let record;
|
|
3437
|
+
try {
|
|
3438
|
+
record = JSON.parse(line);
|
|
3439
|
+
} catch {
|
|
3440
|
+
continue;
|
|
3441
|
+
}
|
|
3442
|
+
if (typeof record?.timestamp === "string") {
|
|
3443
|
+
updatedAt = record.timestamp;
|
|
3444
|
+
}
|
|
3445
|
+
if (record?.type === "session_meta" && typeof record.payload?.id === "string") {
|
|
3446
|
+
id = record.payload.id;
|
|
3447
|
+
hasSessionMeta = true;
|
|
3448
|
+
}
|
|
3449
|
+
if (!threadName && record?.type === "event_msg") {
|
|
3450
|
+
const message = record.payload?.message;
|
|
3451
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
3452
|
+
threadName = message.trim();
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
if (!updatedAt) {
|
|
3457
|
+
try {
|
|
3458
|
+
updatedAt = statSync(rolloutPath).mtime.toISOString();
|
|
3459
|
+
} catch {
|
|
3460
|
+
updatedAt = new Date().toISOString();
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
if (!threadName) {
|
|
3464
|
+
threadName = "Codex session";
|
|
3465
|
+
}
|
|
3466
|
+
if (!hasSessionMeta) {
|
|
3467
|
+
return null;
|
|
3468
|
+
}
|
|
3469
|
+
if (threadName.length > 80) {
|
|
3470
|
+
threadName = `${threadName.slice(0, 77)}...`;
|
|
3471
|
+
}
|
|
3472
|
+
return { id, thread_name: threadName, updated_at: updatedAt };
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
function writeSessionIndexAtomicSync(indexPath, lines) {
|
|
3476
|
+
const indexDir = dirname(indexPath);
|
|
3477
|
+
mkdirSync(indexDir, { recursive: true });
|
|
3478
|
+
for (let attempt = 0; attempt <= SHADOW_HOME_CLEANUP_BACKOFF_MS.length; attempt += 1) {
|
|
3479
|
+
const tempPath = join(
|
|
3480
|
+
indexDir,
|
|
3481
|
+
[
|
|
3482
|
+
`.${basename(indexPath)}`,
|
|
3483
|
+
String(process.pid),
|
|
3484
|
+
String(Date.now()),
|
|
3485
|
+
randomBytes(4).toString("hex"),
|
|
3486
|
+
"tmp",
|
|
3487
|
+
].join("."),
|
|
3488
|
+
);
|
|
3489
|
+
try {
|
|
3490
|
+
writeFileSync(tempPath, `${lines.join("\n")}\n`, {
|
|
3491
|
+
encoding: "utf8",
|
|
3492
|
+
mode: 0o600,
|
|
3493
|
+
});
|
|
3494
|
+
chmodSync(tempPath, 0o600);
|
|
3495
|
+
renameSync(tempPath, indexPath);
|
|
3496
|
+
chmodSync(indexPath, 0o600);
|
|
3497
|
+
return;
|
|
3498
|
+
} catch (error) {
|
|
3499
|
+
try {
|
|
3500
|
+
rmSync(tempPath, { force: true });
|
|
3501
|
+
} catch {
|
|
3502
|
+
// Preserve the original write failure.
|
|
3503
|
+
}
|
|
3504
|
+
if (
|
|
3505
|
+
isRetryableShadowHomeCleanupError(error) &&
|
|
3506
|
+
attempt < SHADOW_HOME_CLEANUP_BACKOFF_MS.length
|
|
3507
|
+
) {
|
|
3508
|
+
sleepSync(SHADOW_HOME_CLEANUP_BACKOFF_MS[attempt]);
|
|
3509
|
+
continue;
|
|
3510
|
+
}
|
|
3511
|
+
throw error;
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
function repairCodexSessionIndex(codexHome) {
|
|
3517
|
+
if (!codexHome || typeof codexHome !== "string") return;
|
|
3518
|
+
const sessionsDir = join(codexHome, "sessions");
|
|
3519
|
+
if (!existsSync(sessionsDir)) return;
|
|
3520
|
+
const indexPath = join(codexHome, "session_index.jsonl");
|
|
3521
|
+
let releaseLock = null;
|
|
3522
|
+
try {
|
|
3523
|
+
releaseLock = acquireShadowHomeSyncLock(codexHome);
|
|
3524
|
+
const seen = new Set();
|
|
3525
|
+
let existingLines = [];
|
|
3526
|
+
if (existsSync(indexPath)) {
|
|
3527
|
+
existingLines = readFileSync(indexPath, "utf8")
|
|
3528
|
+
.split(/\r?\n/)
|
|
3529
|
+
.filter(Boolean);
|
|
3530
|
+
for (const line of existingLines) {
|
|
3531
|
+
try {
|
|
3532
|
+
const entry = JSON.parse(line);
|
|
3533
|
+
if (typeof entry?.id === "string") {
|
|
3534
|
+
seen.add(entry.id);
|
|
3535
|
+
}
|
|
3536
|
+
} catch {
|
|
3537
|
+
// Preserve unparsable existing lines.
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
const additions = [];
|
|
3543
|
+
for (const rolloutPath of collectRolloutFiles(sessionsDir)) {
|
|
3544
|
+
const entry = parseRolloutIndexEntry(rolloutPath);
|
|
3545
|
+
if (!entry || seen.has(entry.id)) continue;
|
|
3546
|
+
seen.add(entry.id);
|
|
3547
|
+
additions.push(entry);
|
|
3548
|
+
}
|
|
3549
|
+
if (additions.length === 0) return;
|
|
3550
|
+
additions.sort((a, b) => a.updated_at.localeCompare(b.updated_at));
|
|
3551
|
+
writeSessionIndexAtomicSync(indexPath, [
|
|
3552
|
+
...existingLines,
|
|
3553
|
+
...additions.map((entry) => JSON.stringify(entry)),
|
|
3554
|
+
]);
|
|
3555
|
+
} catch {
|
|
3556
|
+
// Best-effort repair only; forwarding must not fail because indexing did.
|
|
3557
|
+
} finally {
|
|
3558
|
+
releaseLock?.();
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3276
3562
|
const WINDOWS_SHIM_MARKER = "codex-multi-auth windows shim guardian v1";
|
|
3277
3563
|
const POWERSHELL_PROFILE_MARKER_START = "# >>> codex-multi-auth shell guard >>>";
|
|
3278
3564
|
const POWERSHELL_PROFILE_MARKER_END = "# <<< codex-multi-auth shell guard <<<";
|
|
@@ -110,6 +110,7 @@ function printUsage() {
|
|
|
110
110
|
" --plugin=dist|package Load plugin from local dist URI or package name (default: dist)",
|
|
111
111
|
" --max-cases=N Hard cap number of cases per scenario",
|
|
112
112
|
" --report-json=PATH Write JSON report to PATH (relative to repo root)",
|
|
113
|
+
" --strict-capabilities Fail unsupported account/model capabilities instead of skipping them",
|
|
113
114
|
" --no-restore Keep generated local config files after run",
|
|
114
115
|
" -h, --help Show help",
|
|
115
116
|
].join("\n"),
|
|
@@ -148,6 +149,15 @@ function runQuiet(command, commandArgs) {
|
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
function runQuietWindowsTaskkill(pid) {
|
|
153
|
+
runQuiet("cmd.exe", [
|
|
154
|
+
"/d",
|
|
155
|
+
"/s",
|
|
156
|
+
"/c",
|
|
157
|
+
`taskkill /F /T /PID ${pid}`,
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
|
|
151
161
|
export function registerSpawnedCodex(pid) {
|
|
152
162
|
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
153
163
|
spawnedCodexPids.add(pid);
|
|
@@ -171,18 +181,67 @@ export function resolveMatrixTimeoutMs(smoke = false) {
|
|
|
171
181
|
return parsedTimeout;
|
|
172
182
|
}
|
|
173
183
|
|
|
174
|
-
function
|
|
184
|
+
function parseNdjsonEvents(output) {
|
|
185
|
+
const events = [];
|
|
186
|
+
for (const line of output.split(/\r?\n/)) {
|
|
187
|
+
const trimmed = line.trim();
|
|
188
|
+
if (!trimmed.startsWith("{")) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
events.push(JSON.parse(trimmed));
|
|
193
|
+
} catch {
|
|
194
|
+
// Ignore wrapper noise and partial lines.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return events;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function findLastIndex(items, predicate) {
|
|
201
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
202
|
+
if (predicate(items[index], index)) {
|
|
203
|
+
return index;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return -1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function hasTerminalFailure(events) {
|
|
210
|
+
const lastCompletedIndex = findLastIndex(
|
|
211
|
+
events,
|
|
212
|
+
(event) =>
|
|
213
|
+
event?.type === "turn.completed" || event?.type === "response.completed",
|
|
214
|
+
);
|
|
175
215
|
return (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
216
|
+
findLastIndex(
|
|
217
|
+
events,
|
|
218
|
+
(event, index) =>
|
|
219
|
+
index > lastCompletedIndex &&
|
|
220
|
+
(event?.type === "error" ||
|
|
221
|
+
event?.type === "turn.failed" ||
|
|
222
|
+
event?.type === "response.failed" ||
|
|
223
|
+
event?.type === "response.error" ||
|
|
224
|
+
event?.type === "response.incomplete"),
|
|
225
|
+
) >= 0
|
|
179
226
|
);
|
|
180
227
|
}
|
|
181
228
|
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
229
|
+
function hasCompletedSuccessfully(output, token) {
|
|
230
|
+
const events = parseNdjsonEvents(output);
|
|
231
|
+
if (events.length > 0) {
|
|
232
|
+
if (hasTerminalFailure(events)) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
return events.some(
|
|
236
|
+
(event) =>
|
|
237
|
+
event?.type === "turn.completed" ||
|
|
238
|
+
event?.type === "response.completed",
|
|
239
|
+
);
|
|
185
240
|
}
|
|
241
|
+
return output.includes(token);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getCapabilitySkipReason(exitCode, output, smoke) {
|
|
186
245
|
if (/not supported when using codex with a chatgpt account/i.test(output)) {
|
|
187
246
|
return "unsupported-model";
|
|
188
247
|
}
|
|
@@ -192,14 +251,26 @@ function getSmokeSkipReason(exitCode, output) {
|
|
|
192
251
|
) {
|
|
193
252
|
return "unsupported-reasoning";
|
|
194
253
|
}
|
|
254
|
+
if (smoke && exitCode === 124) {
|
|
255
|
+
return "timed-out";
|
|
256
|
+
}
|
|
195
257
|
return null;
|
|
196
258
|
}
|
|
197
259
|
|
|
198
|
-
function finalizeModelCaseResult(
|
|
260
|
+
function finalizeModelCaseResult(
|
|
261
|
+
caseInfo,
|
|
262
|
+
exitCode,
|
|
263
|
+
output,
|
|
264
|
+
token,
|
|
265
|
+
{ smoke, strictCapabilities } = {},
|
|
266
|
+
) {
|
|
199
267
|
const hasToken = output.includes(token);
|
|
200
268
|
const completed = hasCompletedSuccessfully(output, token);
|
|
201
269
|
const ok = exitCode === 0 && completed;
|
|
202
|
-
const skipReason =
|
|
270
|
+
const skipReason =
|
|
271
|
+
!ok && strictCapabilities !== true
|
|
272
|
+
? getCapabilitySkipReason(exitCode, output, smoke === true)
|
|
273
|
+
: null;
|
|
203
274
|
|
|
204
275
|
return {
|
|
205
276
|
...caseInfo,
|
|
@@ -219,8 +290,12 @@ export function __finalizeModelCaseResultForTests(
|
|
|
219
290
|
output,
|
|
220
291
|
token,
|
|
221
292
|
smoke = false,
|
|
293
|
+
strictCapabilities = false,
|
|
222
294
|
) {
|
|
223
|
-
return finalizeModelCaseResult(caseInfo, exitCode, output, token,
|
|
295
|
+
return finalizeModelCaseResult(caseInfo, exitCode, output, token, {
|
|
296
|
+
smoke,
|
|
297
|
+
strictCapabilities,
|
|
298
|
+
});
|
|
224
299
|
}
|
|
225
300
|
|
|
226
301
|
function stopCodexServersInternal() {
|
|
@@ -228,7 +303,7 @@ function stopCodexServersInternal() {
|
|
|
228
303
|
spawnedCodexPids.clear();
|
|
229
304
|
for (const pid of tracked) {
|
|
230
305
|
if (process.platform === "win32") {
|
|
231
|
-
|
|
306
|
+
runQuietWindowsTaskkill(pid);
|
|
232
307
|
continue;
|
|
233
308
|
}
|
|
234
309
|
runQuiet("kill", ["-9", String(pid)]);
|
|
@@ -352,7 +427,10 @@ function executeModelCase(caseInfo, index) {
|
|
|
352
427
|
124,
|
|
353
428
|
`Timed out after ${timeoutMs}ms`,
|
|
354
429
|
token,
|
|
355
|
-
|
|
430
|
+
{
|
|
431
|
+
smoke: caseInfo.smoke === true,
|
|
432
|
+
strictCapabilities: caseInfo.strictCapabilities === true,
|
|
433
|
+
},
|
|
356
434
|
);
|
|
357
435
|
}
|
|
358
436
|
|
|
@@ -364,7 +442,10 @@ function executeModelCase(caseInfo, index) {
|
|
|
364
442
|
exitCode,
|
|
365
443
|
combinedOutput,
|
|
366
444
|
token,
|
|
367
|
-
|
|
445
|
+
{
|
|
446
|
+
smoke: caseInfo.smoke === true,
|
|
447
|
+
strictCapabilities: caseInfo.strictCapabilities === true,
|
|
448
|
+
},
|
|
368
449
|
);
|
|
369
450
|
}
|
|
370
451
|
|
|
@@ -442,6 +523,7 @@ async function runScenario(scenario, options) {
|
|
|
442
523
|
(caseInfo) => ({
|
|
443
524
|
...caseInfo,
|
|
444
525
|
smoke: options.smoke,
|
|
526
|
+
strictCapabilities: options.strictCapabilities,
|
|
445
527
|
}),
|
|
446
528
|
);
|
|
447
529
|
console.log(`\n=== ${scenario.toUpperCase()} (${cases.length} cases) ===`);
|
|
@@ -483,6 +565,7 @@ async function main() {
|
|
|
483
565
|
const scenarioValue =
|
|
484
566
|
parseArgValue(args, "--scenario") ?? (smoke ? "modern" : "all");
|
|
485
567
|
const pluginMode = parseArgValue(args, "--plugin") ?? "dist";
|
|
568
|
+
const strictCapabilities = args.includes("--strict-capabilities");
|
|
486
569
|
const noRestore = args.includes("--no-restore");
|
|
487
570
|
const maxCasesRaw = parseArgValue(args, "--max-cases");
|
|
488
571
|
const maxCases = maxCasesRaw ? Number.parseInt(maxCasesRaw, 10) : 0;
|
|
@@ -530,6 +613,7 @@ async function main() {
|
|
|
530
613
|
smoke,
|
|
531
614
|
maxCases,
|
|
532
615
|
pluginRef,
|
|
616
|
+
strictCapabilities,
|
|
533
617
|
});
|
|
534
618
|
allResults.push(
|
|
535
619
|
...scenarioResults.map((item) => ({ ...item, scenario })),
|