@bitkyc08/opencodex 2.12.0 → 2.13.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/README.md +1 -1
- package/gui/dist/assets/index-BHldBl6_.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +15 -0
- package/src/adapters/google-antigravity-replay.ts +269 -5
- package/src/adapters/openai-chat.ts +25 -18
- package/src/adapters/openai-responses.ts +63 -17
- package/src/bridge.ts +11 -5
- package/src/claude/inbound.ts +24 -3
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +262 -1
- package/src/cli/account.ts +3 -1
- package/src/cli/dispatch.ts +532 -0
- package/src/cli/doctor.ts +26 -21
- package/src/cli/help.ts +9 -278
- package/src/cli/index.ts +45 -506
- package/src/cli/lab.ts +171 -65
- package/src/cli/registry.ts +384 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/status.ts +9 -2
- package/src/cli/v2.ts +37 -2
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-store.ts +12 -1
- package/src/codex/admission.ts +2 -1
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/metadata.ts +6 -4
- package/src/codex/catalog/parsing.ts +31 -7
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/catalog/sync.ts +65 -11
- package/src/codex/features.ts +437 -31
- package/src/codex/history-job.ts +6 -11
- package/src/codex/history-provider.ts +10 -9
- package/src/codex/inject.ts +14 -1
- package/src/codex/native-profile-startup.ts +49 -3
- package/src/codex/native-residue.ts +14 -2
- package/src/codex/paths.ts +75 -1
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/shim.ts +876 -16
- package/src/codex/sync.ts +27 -2
- package/src/config.ts +148 -10
- package/src/generated/compatibility-version.json +2572 -0
- package/src/images/loop.ts +1 -1
- package/src/lab/artifacts/sanitize.ts +3 -3
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +480 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/types.ts +23 -0
- package/src/lab/events/types.ts +2 -1
- package/src/lab/events/validate.ts +2 -1
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +454 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +2 -0
- package/src/lab/ledger/store.ts +173 -0
- package/src/lab/live/executor.ts +29 -4
- package/src/lab/live/transport.ts +3 -1
- package/src/lab/observe/from-conformance.ts +12 -57
- package/src/lab/observe/from-live.ts +9 -1
- package/src/lab/paths.ts +74 -9
- package/src/lab/projection/schema.ts +14 -1
- package/src/lab/projection/verdicts.ts +89 -53
- package/src/lab/projection/verification.ts +83 -3
- package/src/lab/query/catalog.ts +34 -5
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +11 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/subject/installation-salt.ts +24 -4
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +2 -1
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/windows-elevation.ts +8 -1
- package/src/lib/windows-user-principal.ts +62 -2
- package/src/lib/winsw.ts +8 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/google-antigravity.ts +27 -0
- package/src/oauth/health.ts +12 -48
- package/src/oauth/index.ts +122 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/nous.ts +749 -0
- package/src/oauth/store.ts +56 -3
- package/src/providers/codex-capacity.ts +1 -0
- package/src/providers/registry.ts +161 -16
- package/src/responses/custom-tool-compat.ts +182 -0
- package/src/responses/parser.ts +22 -0
- package/src/responses/reasoning-replay-cache.ts +168 -11
- package/src/responses/schema.ts +1 -1
- package/src/router.ts +5 -20
- package/src/routing/capability.ts +9 -3
- package/src/routing/compatibility/assemble.ts +159 -0
- package/src/routing/compatibility/behavior.ts +224 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/evaluator.ts +27 -32
- package/src/routing/profile.ts +118 -4
- package/src/routing/trace.ts +82 -28
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +20 -7
- package/src/server/chat-completions.ts +2 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/direct-local-http.ts +343 -0
- package/src/server/index.ts +80 -18
- package/src/server/lifecycle.ts +12 -4
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/management/agent-settings-routes.ts +27 -3
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +13 -0
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +57 -1
- package/src/server/management/provider-routes.ts +16 -0
- package/src/server/management/routing-profile-routes.ts +12 -24
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +68 -3
- package/src/server/proxy-liveness.ts +3 -2
- package/src/server/request-decompress.ts +9 -3
- package/src/server/request-log.ts +53 -4
- package/src/server/responses/collaboration.ts +93 -17
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +202 -15
- package/src/server/responses/policy-fallback.ts +1 -0
- package/src/server/responses-custom-tool-repair.ts +280 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/startup-action-control.ts +8 -1
- package/src/service.ts +278 -12
- package/src/types.ts +49 -3
- package/src/usage/expected-prices.ts +30 -0
- package/src/usage/log.ts +30 -4
- package/src/usage/summary.ts +153 -2
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
- package/src/cli/internal-dispatch.ts +0 -20
package/src/codex/features.ts
CHANGED
|
@@ -30,12 +30,14 @@
|
|
|
30
30
|
* catalog.ts:40-54) so tests can point fixtures via env or the explicit
|
|
31
31
|
* `configPath` parameter without fighting the module-load-time const in paths.ts.
|
|
32
32
|
*/
|
|
33
|
-
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
34
|
-
import { join, resolve } from "node:path";
|
|
33
|
+
import { existsSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
34
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
35
|
+
import { createRequire } from "node:module";
|
|
35
36
|
import { realpathSync } from "node:fs";
|
|
36
|
-
import { AtomicWriteResidualTempError, AtomicWriteSecretResidualError, atomicWriteFile, expandUserPath } from "../config";
|
|
37
|
+
import { AtomicWriteResidualTempError, AtomicWriteSecretResidualError, atomicWriteFile, expandUserPath, getConfigDir } from "../config";
|
|
37
38
|
import { forgetEphemeralSecretPath } from "../lib/windows-secret-acl";
|
|
38
39
|
import { CODEX_CONFIG_PATH } from "./paths";
|
|
40
|
+
import { resolveAndPersistCodexRuntime } from "./runtime";
|
|
39
41
|
|
|
40
42
|
/** Upstream codex-rs feature key: allow `request_user_input` in Default mode. */
|
|
41
43
|
export const DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY = "default_mode_request_user_input";
|
|
@@ -451,16 +453,46 @@ function encodeTomlBasicString(value: string): string {
|
|
|
451
453
|
*/
|
|
452
454
|
function decodeTomlStringToken(token: string): string | null {
|
|
453
455
|
if (token.length < 2) return null;
|
|
456
|
+
// Multi-line literal string: `'''...'''` verbatim (backslashes not special).
|
|
457
|
+
if (token.startsWith("'''")) {
|
|
458
|
+
return token.endsWith("'''") && token.length >= 6
|
|
459
|
+
? normalizeTomlMultilineBody(token.slice(3, -3))
|
|
460
|
+
: null;
|
|
461
|
+
}
|
|
462
|
+
// Multi-line basic string: `"""..."""` with escapes.
|
|
463
|
+
if (token.startsWith('"""')) {
|
|
464
|
+
if (!token.endsWith('"""') || token.length < 6) return null;
|
|
465
|
+
return decodeBasicStringBody(normalizeTomlMultilineBody(token.slice(3, -3)), true);
|
|
466
|
+
}
|
|
454
467
|
if (token.startsWith("'")) {
|
|
455
468
|
return token.endsWith("'") ? token.slice(1, -1) : null;
|
|
456
469
|
}
|
|
457
470
|
if (!token.startsWith('"') || !token.endsWith('"')) return null;
|
|
458
|
-
|
|
471
|
+
return decodeBasicStringBody(token.slice(1, -1));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Apply TOML's multi-line newline rules before string-body decoding. */
|
|
475
|
+
function normalizeTomlMultilineBody(body: string): string {
|
|
476
|
+
const normalized = body.replace(/\r\n/g, "\n");
|
|
477
|
+
return normalized.startsWith("\n") ? normalized.slice(1) : normalized;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Unescape the body of a TOML basic string (single- or multi-line). */
|
|
481
|
+
function decodeBasicStringBody(body: string, multiline = false): string | null {
|
|
459
482
|
let out = "";
|
|
460
483
|
for (let i = 0; i < body.length; i++) {
|
|
461
484
|
const ch = body[i];
|
|
462
485
|
if (ch !== "\\") { out += ch; continue; }
|
|
463
486
|
const esc = body[++i];
|
|
487
|
+
if (multiline) {
|
|
488
|
+
let newline = i;
|
|
489
|
+
while (body[newline] === " " || body[newline] === "\t") newline++;
|
|
490
|
+
if (body[newline] === "\n") {
|
|
491
|
+
i = newline;
|
|
492
|
+
while (body[i + 1] === " " || body[i + 1] === "\t" || body[i + 1] === "\n") i++;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
464
496
|
switch (esc) {
|
|
465
497
|
case "\\": out += "\\"; break;
|
|
466
498
|
case '"': out += '"'; break;
|
|
@@ -499,6 +531,28 @@ function scanTomlValueEnd(text: string, start: number): number {
|
|
|
499
531
|
while (i < text.length && (text[i] === " " || text[i] === "\t")) i++;
|
|
500
532
|
const first = text[i];
|
|
501
533
|
if (first === '"') {
|
|
534
|
+
// Multi-line basic string: `"""..."""`. The closing delimiter is a triple
|
|
535
|
+
// quote; a single or double quote inside the body does not end the token.
|
|
536
|
+
if (text[i + 1] === '"' && text[i + 2] === '"') {
|
|
537
|
+
i += 3;
|
|
538
|
+
while (i < text.length) {
|
|
539
|
+
if (text[i] === "\\") { i += 2; continue; }
|
|
540
|
+
if (text[i] === '"' && text[i + 1] === '"' && text[i + 2] === '"') {
|
|
541
|
+
// TOML permits up to two quotes immediately inside the closing
|
|
542
|
+
// delimiter, so a 4- or 5-quote run means the extra one or two
|
|
543
|
+
// quotes are part of the value. Consume the full valid run and
|
|
544
|
+
// keep the surplus quotes in the body.
|
|
545
|
+
let end = i + 3;
|
|
546
|
+
if (text[end] === '"') {
|
|
547
|
+
end++;
|
|
548
|
+
if (text[end] === '"') end++;
|
|
549
|
+
}
|
|
550
|
+
return end;
|
|
551
|
+
}
|
|
552
|
+
i++;
|
|
553
|
+
}
|
|
554
|
+
return text.length;
|
|
555
|
+
}
|
|
502
556
|
i++;
|
|
503
557
|
while (i < text.length) {
|
|
504
558
|
if (text[i] === "\\") { i += 2; continue; }
|
|
@@ -508,6 +562,24 @@ function scanTomlValueEnd(text: string, start: number): number {
|
|
|
508
562
|
return text.length;
|
|
509
563
|
}
|
|
510
564
|
if (first === "'") {
|
|
565
|
+
// Multi-line literal string: `'''...'''`.
|
|
566
|
+
if (text[i + 1] === "'" && text[i + 2] === "'") {
|
|
567
|
+
i += 3;
|
|
568
|
+
while (i < text.length) {
|
|
569
|
+
if (text[i] === "'" && text[i + 1] === "'" && text[i + 2] === "'") {
|
|
570
|
+
// Same as multi-line basic: up to two surplus single quotes can
|
|
571
|
+
// precede the closing delimiter and remain part of the value.
|
|
572
|
+
let end = i + 3;
|
|
573
|
+
if (text[end] === "'") {
|
|
574
|
+
end++;
|
|
575
|
+
if (text[end] === "'") end++;
|
|
576
|
+
}
|
|
577
|
+
return end;
|
|
578
|
+
}
|
|
579
|
+
i++;
|
|
580
|
+
}
|
|
581
|
+
return text.length;
|
|
582
|
+
}
|
|
511
583
|
const close = text.indexOf("'", i + 1);
|
|
512
584
|
return close === -1 ? text.length : close + 1;
|
|
513
585
|
}
|
|
@@ -546,6 +618,40 @@ function findInlineTableEnd(text: string, openIdx: number): number {
|
|
|
546
618
|
|
|
547
619
|
interface InlineEntry { keyStart: number; valueStart: number; valueEnd: number }
|
|
548
620
|
|
|
621
|
+
/** Locate a top-level assignment while skipping complete (possibly multiline) values. */
|
|
622
|
+
function findTomlAssignment(text: string, key: string): InlineEntry | null {
|
|
623
|
+
let lineStart = 0;
|
|
624
|
+
while (lineStart < text.length) {
|
|
625
|
+
let i = lineStart;
|
|
626
|
+
while (text[i] === " " || text[i] === "\t") i++;
|
|
627
|
+
const keyStart = i;
|
|
628
|
+
let keyText = "";
|
|
629
|
+
if (text[i] === '"' || text[i] === "'") {
|
|
630
|
+
const keyEnd = scanTomlValueEnd(text, i);
|
|
631
|
+
keyText = decodeTomlStringToken(text.slice(i, keyEnd)) ?? "";
|
|
632
|
+
i = keyEnd;
|
|
633
|
+
} else {
|
|
634
|
+
const match = /^[A-Za-z0-9_-]+/.exec(text.slice(i));
|
|
635
|
+
if (match) {
|
|
636
|
+
keyText = match[0];
|
|
637
|
+
i += match[0].length;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
while (text[i] === " " || text[i] === "\t") i++;
|
|
641
|
+
if (keyText && text[i] === "=") {
|
|
642
|
+
const valueStart = i + 1;
|
|
643
|
+
const valueEnd = scanTomlValueEnd(text, valueStart);
|
|
644
|
+
if (keyText === key) return { keyStart, valueStart, valueEnd };
|
|
645
|
+
const nextLine = text.indexOf("\n", valueEnd);
|
|
646
|
+
lineStart = nextLine === -1 ? text.length : nextLine + 1;
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const nextLine = text.indexOf("\n", lineStart);
|
|
650
|
+
lineStart = nextLine === -1 ? text.length : nextLine + 1;
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
|
|
549
655
|
/**
|
|
550
656
|
* Locate `key = value` inside the inline-table body spanning [bodyStart, bodyEnd)
|
|
551
657
|
* (exclusive of the braces), string-aware on both keys and values, or null.
|
|
@@ -579,6 +685,24 @@ function findInlineEntry(text: string, bodyStart: number, bodyEnd: number, key:
|
|
|
579
685
|
return null;
|
|
580
686
|
}
|
|
581
687
|
|
|
688
|
+
/** Whether a TOML assignment's value starts with a triple-quoted string. */
|
|
689
|
+
function isMultilineTomlString(text: string, valueStart: number): boolean {
|
|
690
|
+
let start = valueStart;
|
|
691
|
+
while (text[start] === " " || text[start] === "\t") start++;
|
|
692
|
+
return text.startsWith('"""', start) || text.startsWith("'''", start);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** Whether one field in a supported inline `multi_agent_v2` table is triple-quoted. */
|
|
696
|
+
function hasInlineMultilineTomlString(text: string, assignment: InlineEntry, key: string): boolean {
|
|
697
|
+
let openIdx = assignment.valueStart;
|
|
698
|
+
while (text[openIdx] === " " || text[openIdx] === "\t") openIdx++;
|
|
699
|
+
if (text[openIdx] !== "{") return false;
|
|
700
|
+
const closeIdx = findInlineTableEnd(text, openIdx);
|
|
701
|
+
if (closeIdx === -1) return false;
|
|
702
|
+
const entry = findInlineEntry(text, openIdx + 1, closeIdx, key);
|
|
703
|
+
return entry !== null && isMultilineTomlString(text, entry.valueStart);
|
|
704
|
+
}
|
|
705
|
+
|
|
582
706
|
/**
|
|
583
707
|
* Set or remove one scalar key inside a top-level TOML table, preserving every
|
|
584
708
|
* other line byte-for-byte — including the existing value's trailing comment,
|
|
@@ -602,22 +726,19 @@ function editScalarInTable(content: string, table: string, key: string, encoded:
|
|
|
602
726
|
for (let i = headerIdx + 1; i < lines.length; i++) {
|
|
603
727
|
if (/^\s*\[/.test(lines[i])) { end = i; break; }
|
|
604
728
|
}
|
|
605
|
-
const keyRe = new RegExp(`^(\\s*)${key}\\s*=\\s*`);
|
|
606
729
|
for (let i = headerIdx + 1; i < end; i++) {
|
|
607
|
-
const
|
|
608
|
-
if (!
|
|
730
|
+
const entry = findTomlAssignment(lines[i], key);
|
|
731
|
+
if (!entry) continue;
|
|
609
732
|
// The existing value may itself contain '#', so scan the value token
|
|
610
733
|
// string-aware instead of splitting on '#'.
|
|
611
734
|
const line = lines[i];
|
|
612
|
-
const
|
|
613
|
-
const valueEnd = scanTomlValueEnd(line, valueStart);
|
|
614
|
-
const trailing = line.slice(valueEnd);
|
|
735
|
+
const trailing = line.slice(entry.valueEnd);
|
|
615
736
|
if (encoded === null) {
|
|
616
737
|
lines.splice(i, 1);
|
|
617
738
|
return applyEol(lines.join("\n"), eol);
|
|
618
739
|
}
|
|
619
|
-
if (line.slice(valueStart, valueEnd).trim() === encoded) return content;
|
|
620
|
-
lines[i] = `${
|
|
740
|
+
if (line.slice(entry.valueStart, entry.valueEnd).trim() === encoded) return content;
|
|
741
|
+
lines[i] = `${line.slice(0, entry.keyStart)}${key} = ${encoded}${trailing}`;
|
|
621
742
|
return applyEol(lines.join("\n"), eol);
|
|
622
743
|
}
|
|
623
744
|
if (encoded === null) return content;
|
|
@@ -669,31 +790,95 @@ export function setAgentsMaxDepth(value: number | null, configPath?: string): Co
|
|
|
669
790
|
* Reads both the dedicated-table and inline forms; dedicated wins, mirroring
|
|
670
791
|
* `getMaxConcurrentThreads` precedence.
|
|
671
792
|
*/
|
|
672
|
-
|
|
793
|
+
/**
|
|
794
|
+
* Read a string-valued `features.multi_agent_v2` scalar (dedicated table or inline
|
|
795
|
+
* form; dedicated wins, mirroring `getMaxConcurrentThreads` precedence). Returns
|
|
796
|
+
* `null` when the key is absent or the config is unreadable. A present empty string
|
|
797
|
+
* round-trips faithfully — some upstream keys treat `""` and `null` differently.
|
|
798
|
+
*/
|
|
799
|
+
function getV2StringField(key: string, configPath?: string): string | null {
|
|
673
800
|
const content = readConfigText(configPath);
|
|
674
801
|
if (content === null) return null;
|
|
675
|
-
const table =
|
|
802
|
+
const table = tomlTableBodyForStringFields(content, "features.multi_agent_v2");
|
|
676
803
|
if (table !== null) {
|
|
677
|
-
const
|
|
678
|
-
if (
|
|
679
|
-
const
|
|
680
|
-
const token = table.slice(valueStart, scanTomlValueEnd(table, valueStart)).trim();
|
|
804
|
+
const entry = findTomlAssignment(table, key);
|
|
805
|
+
if (entry) {
|
|
806
|
+
const token = table.slice(entry.valueStart, entry.valueEnd).trim();
|
|
681
807
|
return decodeTomlStringToken(token);
|
|
682
808
|
}
|
|
683
809
|
return null;
|
|
684
810
|
}
|
|
685
|
-
const features =
|
|
811
|
+
const features = tomlTableBodyForStringFields(content, "features");
|
|
686
812
|
if (features === null) return null;
|
|
687
|
-
const
|
|
688
|
-
if (!
|
|
689
|
-
|
|
813
|
+
const v2Entry = findTomlAssignment(features, "multi_agent_v2");
|
|
814
|
+
if (!v2Entry) return null;
|
|
815
|
+
let openIdx = v2Entry.valueStart;
|
|
816
|
+
while (features[openIdx] === " " || features[openIdx] === "\t") openIdx++;
|
|
817
|
+
if (features[openIdx] !== "{") return null;
|
|
690
818
|
const closeIdx = findInlineTableEnd(features, openIdx);
|
|
691
819
|
if (closeIdx === -1) return null;
|
|
692
|
-
const entry = findInlineEntry(features, openIdx + 1, closeIdx,
|
|
820
|
+
const entry = findInlineEntry(features, openIdx + 1, closeIdx, key);
|
|
693
821
|
if (!entry) return null;
|
|
694
822
|
return decodeTomlStringToken(features.slice(entry.valueStart, entry.valueEnd).trim());
|
|
695
823
|
}
|
|
696
824
|
|
|
825
|
+
/**
|
|
826
|
+
* A table body scanner that skips complete TOML values before recognizing the
|
|
827
|
+
* next header. Unlike the legacy line scanner, bracket-shaped prose inside a
|
|
828
|
+
* multi-line string cannot truncate the table.
|
|
829
|
+
*/
|
|
830
|
+
function tomlTableBodyForStringFields(content: string, header: string): string | null {
|
|
831
|
+
const escaped = escapeRegExp(header);
|
|
832
|
+
const match = new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`, "m").exec(content);
|
|
833
|
+
if (!match) return null;
|
|
834
|
+
const newline = content.indexOf("\n", match.index + match[0].length);
|
|
835
|
+
if (newline === -1) return "";
|
|
836
|
+
const bodyStart = newline + 1;
|
|
837
|
+
let lineStart = bodyStart;
|
|
838
|
+
while (lineStart < content.length) {
|
|
839
|
+
let cursor = lineStart;
|
|
840
|
+
while (content[cursor] === " " || content[cursor] === "\t") cursor++;
|
|
841
|
+
if (content[cursor] === "[") return content.slice(bodyStart, lineStart);
|
|
842
|
+
const lineEnd = content.indexOf("\n", cursor);
|
|
843
|
+
const boundedEnd = lineEnd === -1 ? content.length : lineEnd;
|
|
844
|
+
let keyEnd = cursor;
|
|
845
|
+
if (content[keyEnd] === '"' || content[keyEnd] === "'") {
|
|
846
|
+
keyEnd = scanTomlValueEnd(content, keyEnd);
|
|
847
|
+
} else {
|
|
848
|
+
const keyMatch = /^[A-Za-z0-9_.-]+/.exec(content.slice(keyEnd, boundedEnd));
|
|
849
|
+
if (keyMatch) keyEnd += keyMatch[0].length;
|
|
850
|
+
}
|
|
851
|
+
while (content[keyEnd] === " " || content[keyEnd] === "\t") keyEnd++;
|
|
852
|
+
if (content[keyEnd] === "=" && keyEnd < boundedEnd) {
|
|
853
|
+
const valueEnd = scanTomlValueEnd(content, keyEnd + 1);
|
|
854
|
+
const nextLine = content.indexOf("\n", valueEnd);
|
|
855
|
+
lineStart = nextLine === -1 ? content.length : nextLine + 1;
|
|
856
|
+
} else {
|
|
857
|
+
lineStart = lineEnd === -1 ? content.length : lineEnd + 1;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return content.slice(bodyStart);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function escapeRegExp(value: string): string {
|
|
864
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/** Bare and quoted TOML forms of one known-safe key. */
|
|
868
|
+
function tomlKeyPattern(key: string): string {
|
|
869
|
+
const escaped = escapeRegExp(key);
|
|
870
|
+
return `(?:${escaped}|"${escaped}"|'${escaped}')`;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
export function getSubagentDeveloperInstructions(configPath?: string): string | null {
|
|
874
|
+
return getV2StringField("subagent_developer_instructions", configPath);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** Current `features.multi_agent_v2.multi_agent_mode_hint_text`, or null when unset. */
|
|
878
|
+
export function getMultiAgentModeHintText(configPath?: string): string | null {
|
|
879
|
+
return getV2StringField("multi_agent_mode_hint_text", configPath);
|
|
880
|
+
}
|
|
881
|
+
|
|
697
882
|
/**
|
|
698
883
|
* Persist `features.multi_agent_v2.subagent_developer_instructions`, or remove the
|
|
699
884
|
* key when `value` is null. Handles all three existing encodings: the dedicated
|
|
@@ -705,19 +890,49 @@ export function getSubagentDeveloperInstructions(configPath?: string): string |
|
|
|
705
890
|
* carries `#[serde(deny_unknown_fields)]`, so a misspelling is not ignored — it is
|
|
706
891
|
* a hard config-parse failure for the user's Codex.
|
|
707
892
|
*/
|
|
708
|
-
|
|
893
|
+
function setV2StringField(key: string, value: string | null, configPath?: string): ConfigEditResult {
|
|
709
894
|
const path = configPath ?? activeCodexConfigPath();
|
|
710
895
|
const content = readConfigText(path);
|
|
711
896
|
if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
|
|
712
897
|
const encoded = value === null ? null : encodeTomlBasicString(value);
|
|
713
898
|
|
|
714
|
-
|
|
715
|
-
|
|
899
|
+
// A parsed V2 object without one of the supported source forms came from
|
|
900
|
+
// dotted/quoted path segments. Appending a dedicated table would redefine it
|
|
901
|
+
// and make Codex reject the file, so fail closed without changing any bytes.
|
|
902
|
+
const parsedFeatures = parsedTomlTable(content, "features");
|
|
903
|
+
const parsedV2 = parsedFeatures === null ? null : plainTomlRecord(parsedFeatures.multi_agent_v2);
|
|
904
|
+
const dedicatedV2 = tomlTableBodyForStringFields(content, "features.multi_agent_v2") !== null;
|
|
905
|
+
const featuresBody = tomlTableBodyForStringFields(content, "features");
|
|
906
|
+
const featuresV2Entry = featuresBody === null ? null : findTomlAssignment(featuresBody, "multi_agent_v2");
|
|
907
|
+
const supportedFeaturesEntry = featuresV2Entry !== null;
|
|
908
|
+
if (parsedV2 !== null && !dedicatedV2 && !supportedFeaturesEntry) {
|
|
909
|
+
return { ok: false, error: "dotted or quoted multi_agent_v2 config is not supported for managed string fields" };
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const dedicatedStringBody = tomlTableBodyForStringFields(content, "features.multi_agent_v2");
|
|
913
|
+
if (dedicatedStringBody !== null) {
|
|
914
|
+
// Multiline TOML strings ("""...""" / '''...''') span multiple lines and the
|
|
915
|
+
// single-line table editor cannot rewrite or remove them without corrupting
|
|
916
|
+
// the document. Scope this guard to the target V2 table so an unrelated
|
|
917
|
+
// table carrying the same key does not block an otherwise safe edit.
|
|
918
|
+
const dedicatedEntry = findTomlAssignment(dedicatedStringBody, key);
|
|
919
|
+
if (dedicatedEntry !== null && isMultilineTomlString(dedicatedStringBody, dedicatedEntry.valueStart)) {
|
|
920
|
+
return { ok: false, error: `multi-line TOML string for ${key} is not editable; convert it to a single-line string first` };
|
|
921
|
+
}
|
|
922
|
+
const legacyDedicatedBody = tomlTableBody(content, "features.multi_agent_v2") ?? "";
|
|
923
|
+
if (dedicatedEntry !== null && findTomlAssignment(legacyDedicatedBody, key) === null) {
|
|
924
|
+
return { ok: false, error: `cannot edit ${key} after a header-shaped multiline value safely` };
|
|
925
|
+
}
|
|
926
|
+
const next = editScalarInTable(content, "features.multi_agent_v2", key, encoded);
|
|
716
927
|
if (next === content) return { ok: true, changed: false };
|
|
717
928
|
atomicWriteFile(path, next);
|
|
718
929
|
return { ok: true, changed: true };
|
|
719
930
|
}
|
|
720
931
|
|
|
932
|
+
if (featuresBody !== null && featuresV2Entry !== null && hasInlineMultilineTomlString(featuresBody, featuresV2Entry, key)) {
|
|
933
|
+
return { ok: false, error: `multi-line TOML string for ${key} is not editable; convert it to a single-line string first` };
|
|
934
|
+
}
|
|
935
|
+
|
|
721
936
|
const eol = dominantEol(content);
|
|
722
937
|
const lines = content.split(/\r?\n/);
|
|
723
938
|
const featuresHeader = lines.findIndex(l => /^\s*\[features\]\s*(?:#.*)?$/.test(l));
|
|
@@ -733,7 +948,7 @@ export function setSubagentDeveloperInstructions(value: string | null, configPat
|
|
|
733
948
|
const openIdx = inlineMatch[0].length - 1;
|
|
734
949
|
const closeIdx = findInlineTableEnd(line, openIdx);
|
|
735
950
|
if (closeIdx === -1) return { ok: false, error: "malformed multi_agent_v2 inline table" };
|
|
736
|
-
const entry = findInlineEntry(line, openIdx + 1, closeIdx,
|
|
951
|
+
const entry = findInlineEntry(line, openIdx + 1, closeIdx, key);
|
|
737
952
|
if (encoded === null) {
|
|
738
953
|
if (!entry) return { ok: true, changed: false };
|
|
739
954
|
let start = entry.keyStart;
|
|
@@ -760,8 +975,8 @@ export function setSubagentDeveloperInstructions(value: string | null, configPat
|
|
|
760
975
|
while (insertPos > openIdx + 1 && line[insertPos - 1] === " ") insertPos--;
|
|
761
976
|
const hasEntries = line.slice(openIdx + 1, insertPos).trim().length > 0;
|
|
762
977
|
const insertion = hasEntries
|
|
763
|
-
? `,
|
|
764
|
-
: `
|
|
978
|
+
? `, ${key} = ${encoded} `
|
|
979
|
+
: ` ${key} = ${encoded} `;
|
|
765
980
|
lines[i] = line.slice(0, insertPos) + insertion + line.slice(closeIdx);
|
|
766
981
|
}
|
|
767
982
|
atomicWriteFile(path, applyEol(lines.join("\n"), eol));
|
|
@@ -770,21 +985,212 @@ export function setSubagentDeveloperInstructions(value: string | null, configPat
|
|
|
770
985
|
const boolMatch = line.match(/^(\s*)multi_agent_v2\s*=\s*(true|false)(\s*(?:#.*)?)$/);
|
|
771
986
|
if (boolMatch) {
|
|
772
987
|
if (encoded === null) return { ok: true, changed: false };
|
|
773
|
-
lines[i] = `${boolMatch[1]}multi_agent_v2 = { enabled = ${boolMatch[2]},
|
|
988
|
+
lines[i] = `${boolMatch[1]}multi_agent_v2 = { enabled = ${boolMatch[2]}, ${key} = ${encoded} }${boolMatch[3]}`;
|
|
774
989
|
atomicWriteFile(path, applyEol(lines.join("\n"), eol));
|
|
775
990
|
return { ok: true, changed: true };
|
|
776
991
|
}
|
|
777
992
|
}
|
|
993
|
+
if (featuresV2Entry !== null) {
|
|
994
|
+
return { ok: false, error: "multi_agent_v2 inside a multiline [features] table is not editable safely" };
|
|
995
|
+
}
|
|
778
996
|
}
|
|
779
997
|
|
|
780
998
|
if (encoded === null) return { ok: true, changed: false };
|
|
781
999
|
const suffix = content.endsWith("\n") || content.length === 0 ? "" : eol;
|
|
782
1000
|
const separator = content.length > 0 && !content.endsWith(`${eol}${eol}`) ? eol : "";
|
|
783
|
-
const tableText = `[features.multi_agent_v2]${eol}
|
|
1001
|
+
const tableText = `[features.multi_agent_v2]${eol}${key} = ${encoded}${eol}`;
|
|
784
1002
|
atomicWriteFile(path, `${content}${suffix}${separator}${tableText}`);
|
|
785
1003
|
return { ok: true, changed: true };
|
|
786
1004
|
}
|
|
787
1005
|
|
|
1006
|
+
export function setSubagentDeveloperInstructions(value: string | null, configPath?: string): ConfigEditResult {
|
|
1007
|
+
return setV2StringField("subagent_developer_instructions", value, configPath);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
export const MODE_HINT_UNSUPPORTED_ERROR =
|
|
1011
|
+
"installed Codex does not support multi_agent_mode_hint_text; update Codex first";
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Persist `features.multi_agent_v2.multi_agent_mode_hint_text`, or remove the key
|
|
1015
|
+
* when `value` is null. Same encoding coverage and upstream-name discipline as
|
|
1016
|
+
* `setSubagentDeveloperInstructions`; the upstream struct rejects unknown fields,
|
|
1017
|
+
* so the key spelling must match codex-rs exactly.
|
|
1018
|
+
*/
|
|
1019
|
+
export function setMultiAgentModeHintText(value: string | null, configPath?: string): ConfigEditResult {
|
|
1020
|
+
// The upstream `multi_agent_mode_hint_text` key is newer than the v2 config
|
|
1021
|
+
// surface opencodex already manages; an older Codex build rejects the unknown
|
|
1022
|
+
// member (`#[serde(deny_unknown_fields)]`) and fails to start. Probe the
|
|
1023
|
+
// installed runtime binary for the key string and refuse the write when the
|
|
1024
|
+
// binary provably lacks it. A probe that cannot run (missing binary,
|
|
1025
|
+
// unreadable file) does not block: that is the test/hermetic path and the
|
|
1026
|
+
// headless runtime fallback.
|
|
1027
|
+
if (value !== null) {
|
|
1028
|
+
const probe = probeCodexSupportsModeHint();
|
|
1029
|
+
if (probe === false) {
|
|
1030
|
+
return {
|
|
1031
|
+
ok: false,
|
|
1032
|
+
error: MODE_HINT_UNSUPPORTED_ERROR,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return setV2StringField("multi_agent_mode_hint_text", value, configPath);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const modeHintCapabilityCache = new Map<string, boolean | null>();
|
|
1040
|
+
|
|
1041
|
+
/**
|
|
1042
|
+
* True when the installed Codex runtime binary contains the
|
|
1043
|
+
* `multi_agent_mode_hint_text` config key, false when it provably does not, and
|
|
1044
|
+
* null when the probe could not run (missing binary, unreadable file).
|
|
1045
|
+
*/
|
|
1046
|
+
export function probeCodexSupportsModeHint(): boolean | null {
|
|
1047
|
+
try {
|
|
1048
|
+
const runtime = resolveAndPersistCodexRuntime({ env: process.env }).runtime;
|
|
1049
|
+
const selectedPath = resolveSelectedCommandPath(runtime.command);
|
|
1050
|
+
let selectedIdentity = selectedPath ?? "";
|
|
1051
|
+
try { if (selectedPath) selectedIdentity = realpathSync(selectedPath); } catch { /* keep lexical path */ }
|
|
1052
|
+
const candidates = codexNativeBinaryCandidates(runtime.command);
|
|
1053
|
+
const binaryStatFingerprint = candidates.map(candidate => {
|
|
1054
|
+
try {
|
|
1055
|
+
const stat = statSync(candidate);
|
|
1056
|
+
return `${candidate}\0${stat.dev}\0${stat.ino}\0${stat.size}\0${stat.mtimeMs}\0${stat.ctimeMs}`;
|
|
1057
|
+
} catch {
|
|
1058
|
+
return `${candidate}\0missing`;
|
|
1059
|
+
}
|
|
1060
|
+
}).join("\0");
|
|
1061
|
+
const cacheKey = `${runtime.command}\0${runtime.version ?? ""}\0${selectedIdentity}\0${binaryStatFingerprint}`;
|
|
1062
|
+
const cached = modeHintCapabilityCache.get(cacheKey);
|
|
1063
|
+
if (cached !== undefined) return cached;
|
|
1064
|
+
let sawBinary = false;
|
|
1065
|
+
for (const candidate of candidates) {
|
|
1066
|
+
try {
|
|
1067
|
+
if (!existsSync(candidate)) continue;
|
|
1068
|
+
const buf = readFileSync(candidate);
|
|
1069
|
+
if (!isNativeExecutable(buf)) continue;
|
|
1070
|
+
sawBinary = true;
|
|
1071
|
+
if (buf.includes(Buffer.from("multi_agent_mode_hint_text", "utf8"))) {
|
|
1072
|
+
modeHintCapabilityCache.set(cacheKey, true);
|
|
1073
|
+
return true;
|
|
1074
|
+
}
|
|
1075
|
+
} catch {
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
// At least one real binary was inspected and none contained the key.
|
|
1080
|
+
const result = sawBinary ? false : null;
|
|
1081
|
+
modeHintCapabilityCache.set(cacheKey, result);
|
|
1082
|
+
return result;
|
|
1083
|
+
} catch {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const CODEX_PLATFORM_PACKAGES = [
|
|
1089
|
+
["codex-darwin-arm64", "aarch64-apple-darwin", "codex"],
|
|
1090
|
+
["codex-darwin-x64", "x86_64-apple-darwin", "codex"],
|
|
1091
|
+
["codex-linux-x64", "x86_64-unknown-linux-musl", "codex"],
|
|
1092
|
+
["codex-linux-arm64", "aarch64-unknown-linux-musl", "codex"],
|
|
1093
|
+
["codex-win32-x64", "x86_64-pc-windows-msvc", "codex.exe"],
|
|
1094
|
+
["codex-win32-arm64", "aarch64-pc-windows-msvc", "codex.exe"],
|
|
1095
|
+
] as const;
|
|
1096
|
+
|
|
1097
|
+
function isNativeExecutable(buf: Buffer): boolean {
|
|
1098
|
+
if (buf.length < 4) return false;
|
|
1099
|
+
if (buf[0] === 0x4d && buf[1] === 0x5a) return true; // PE/COFF
|
|
1100
|
+
const magic = buf.readUInt32BE(0);
|
|
1101
|
+
return magic === 0x7f454c46 // ELF
|
|
1102
|
+
|| magic === 0xfeedface || magic === 0xfeedfacf
|
|
1103
|
+
|| magic === 0xcefaedfe || magic === 0xcffaedfe
|
|
1104
|
+
|| magic === 0xcafebabe || magic === 0xbebafeca; // Mach-O/fat Mach-O
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Candidate native codex binaries to probe. The resolved `command` may be the
|
|
1109
|
+
* opencodex shim or the npm JS wrapper (`codex.opencodex-real`), neither of
|
|
1110
|
+
* which embeds the Rust config schema. The real binary ships under the
|
|
1111
|
+
* platform package's `vendor/<triple>/bin/codex`; also try adjacent wrappers.
|
|
1112
|
+
*/
|
|
1113
|
+
function codexNativeBinaryCandidates(command: string): string[] {
|
|
1114
|
+
const out = new Set<string>();
|
|
1115
|
+
const resolverBases = new Set<string>();
|
|
1116
|
+
const selectedPath = resolveSelectedCommandPath(command);
|
|
1117
|
+
|
|
1118
|
+
const addSelectedTarget = (target: string) => {
|
|
1119
|
+
try {
|
|
1120
|
+
const real = realpathSync(target);
|
|
1121
|
+
out.add(target);
|
|
1122
|
+
out.add(real);
|
|
1123
|
+
if (/[\\/]@openai[\\/]codex[\\/]bin[\\/]/.test(real)) resolverBases.add(real);
|
|
1124
|
+
} catch {
|
|
1125
|
+
// Missing backing paths cannot establish capability either way.
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
// Follow the resolved command to its real location. The opencodex shim and
|
|
1130
|
+
// the npm JS wrapper resolve to `@openai/codex/bin/codex.js`; the native
|
|
1131
|
+
// binary lives in the sibling platform package's vendor directory.
|
|
1132
|
+
if (selectedPath) {
|
|
1133
|
+
addSelectedTarget(selectedPath);
|
|
1134
|
+
for (const target of selectedShimBackingPaths(selectedPath)) addSelectedTarget(target);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// npm's Windows wrappers are ordinary scripts. Resolve from both the global
|
|
1138
|
+
// prefix layout and a project-local node_modules/.bin layout; createRequire
|
|
1139
|
+
// then follows the selected installation's own dependency tree (including
|
|
1140
|
+
// pnpm/nested optional dependencies) without consulting unrelated PATH bins.
|
|
1141
|
+
const wrapperPath = selectedPath ?? command;
|
|
1142
|
+
if (/\.(?:cmd|ps1)$/i.test(wrapperPath)) {
|
|
1143
|
+
resolverBases.add(resolve(dirname(wrapperPath), "node_modules", "@openai", "codex", "bin", "codex.js"));
|
|
1144
|
+
resolverBases.add(resolve(dirname(wrapperPath), "..", "@openai", "codex", "bin", "codex.js"));
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
for (const base of resolverBases) {
|
|
1148
|
+
const requireFromSelected = createRequire(base);
|
|
1149
|
+
for (const [pkg, triple, exe] of CODEX_PLATFORM_PACKAGES) {
|
|
1150
|
+
try {
|
|
1151
|
+
const manifest = requireFromSelected.resolve(`@openai/${pkg}/package.json`);
|
|
1152
|
+
out.add(join(dirname(manifest), "vendor", triple, "bin", exe));
|
|
1153
|
+
} catch {
|
|
1154
|
+
// Optional platform packages for other targets are normally absent.
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return [...out];
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/** Backing paths tied to this exact OCX shim entry in codex-shim.json. */
|
|
1162
|
+
function selectedShimBackingPaths(commandPath: string): string[] {
|
|
1163
|
+
try {
|
|
1164
|
+
const state = JSON.parse(readFileSync(join(getConfigDir(), "codex-shim.json"), "utf8")) as {
|
|
1165
|
+
wrapperPath?: unknown;
|
|
1166
|
+
originalPath?: unknown;
|
|
1167
|
+
backupPath?: unknown;
|
|
1168
|
+
realPath?: unknown;
|
|
1169
|
+
wrappers?: Array<Record<string, unknown>>;
|
|
1170
|
+
};
|
|
1171
|
+
const entries = Array.isArray(state.wrappers) && state.wrappers.length > 0 ? state.wrappers : [state];
|
|
1172
|
+
const selected = resolve(commandPath);
|
|
1173
|
+
for (const entry of entries) {
|
|
1174
|
+
if (typeof entry.wrapperPath !== "string" || resolve(entry.wrapperPath) !== selected) continue;
|
|
1175
|
+
return [entry.backupPath, entry.realPath, entry.originalPath]
|
|
1176
|
+
.filter((value): value is string => typeof value === "string" && value.length > 0 && resolve(value) !== selected);
|
|
1177
|
+
}
|
|
1178
|
+
} catch {
|
|
1179
|
+
// Not an OCX-owned shim, or no readable state.
|
|
1180
|
+
}
|
|
1181
|
+
return [];
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/** Resolve only the selected bare command against PATH; never enumerate peers. */
|
|
1185
|
+
function resolveSelectedCommandPath(command: string): string | null {
|
|
1186
|
+
if (command.includes("/") || command.includes("\\")) return existsSync(command) ? resolve(command) : null;
|
|
1187
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
1188
|
+
const candidate = join(dir, command);
|
|
1189
|
+
if (existsSync(candidate)) return candidate;
|
|
1190
|
+
}
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
788
1194
|
function editAgentsMaxThreads(value: number | null, configPath?: string, migratedComment?: string): ConfigEditResult {
|
|
789
1195
|
const path = configPath ?? activeCodexConfigPath();
|
|
790
1196
|
const content = readConfigText(path);
|
package/src/codex/history-job.ts
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { randomUUID } from "node:crypto";
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
|
-
import { join } from "node:path";
|
|
22
21
|
|
|
23
22
|
import type {
|
|
24
23
|
CodexHistoryWorkerOperation,
|
|
@@ -26,19 +25,15 @@ import type {
|
|
|
26
25
|
} from "./history-worker";
|
|
27
26
|
import { historyBackupPathFor } from "./history-provider";
|
|
28
27
|
import type { CodexHistoryFailureReason, CodexHistoryVerifiedNoopProof } from "./history-provider";
|
|
29
|
-
import { getCodexHome } from "./paths";
|
|
30
|
-
|
|
31
|
-
/** Where Codex keeps its resume history, and the manifest that shadows it. */
|
|
32
|
-
const STATE_DB_FILE = "state_5.sqlite";
|
|
28
|
+
import { getCodexHome, resolveCodexStateDbPath } from "./paths";
|
|
33
29
|
|
|
34
30
|
/**
|
|
35
31
|
* Resolve the paths a history job needs, at CALL time.
|
|
36
32
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* different
|
|
41
|
-
* moves `CODEX_HOME` is honoured rather than ignored.
|
|
33
|
+
* The SQLite root can differ from CODEX_HOME and both environment/config inputs
|
|
34
|
+
* can change between invocations. The parent resolves one exact target and hands
|
|
35
|
+
* those canonical paths to the Worker rather than asking the Worker to infer a
|
|
36
|
+
* possibly different environment.
|
|
42
37
|
*/
|
|
43
38
|
export function resolveCodexHistoryJobTarget(): {
|
|
44
39
|
readonly canonicalCodexHome: string;
|
|
@@ -46,7 +41,7 @@ export function resolveCodexHistoryJobTarget(): {
|
|
|
46
41
|
readonly canonicalBackupPath: string;
|
|
47
42
|
} {
|
|
48
43
|
const home = getCodexHome();
|
|
49
|
-
const stateDb =
|
|
44
|
+
const stateDb = resolveCodexStateDbPath({ codexHome: home });
|
|
50
45
|
return {
|
|
51
46
|
canonicalCodexHome: home,
|
|
52
47
|
canonicalStateDbPath: stateDb,
|