@bitkyc08/opencodex 2.81.0 → 2.82.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/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-BqHF5Trd.js} +1 -1
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/{index-RsoaAxXY.js → index-D3FBCNiP.js} +13 -13
- package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DTBC8n7-.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound.ts +77 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +2 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +5 -0
- package/src/cli/doctor.ts +16 -3
- package/src/cli/index.ts +14 -9
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integrations.ts +16 -3
- package/src/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/routes.ts +10 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/build-entries.ts +18 -0
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/effort.ts +26 -5
- package/src/codex/catalog/gather-capture.ts +5 -0
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/provider-models.ts +43 -35
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -220
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +8 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/config-schema.ts +4 -0
- package/src/generated/compatibility-version.json +303 -171
- package/src/generated/model-metadata.ts +2 -2
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +29 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +18 -2
- package/src/integrations/writer.ts +9 -3
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/request-execution-budget.ts +8 -0
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/oauth/anthropic-account-refusal.ts +39 -1
- package/src/oauth/anthropic-routing.ts +1 -1
- package/src/oauth/anthropic-send-ownership.ts +3 -3
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/index.ts +6 -2
- package/src/oauth/store.ts +1 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +21 -0
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota.ts +1 -1
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +15 -2
- package/src/responses/state/metrics.ts +8 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +12 -5
- package/src/responses/state.ts +18 -10
- package/src/responses/task-input.ts +17 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +1 -0
- package/src/server/chat-native.ts +28 -12
- package/src/server/claude-messages.ts +43 -6
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +2 -1
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +1 -0
- package/src/server/management/agent-settings-routes.ts +93 -32
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +59 -8
- package/src/server/management/context.ts +2 -2
- package/src/server/management/shared.ts +3 -3
- package/src/server/messages-native-oauth.ts +9 -8
- package/src/server/messages-native.ts +49 -13
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +31 -2
- package/src/server/responses/adapter-continuation.ts +2 -2
- package/src/server/responses/adapter-dispatch.ts +34 -13
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +20 -3
- package/src/server/responses/core-combo.ts +22 -14
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +73 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +24 -2
- package/src/server/responses/passthrough-dispatch.ts +19 -2
- package/src/server/responses/request-prepare.ts +4 -0
- package/src/server/responses/request-send-budget.ts +112 -49
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +2 -0
- package/src/server/responses/sidecar-execution.ts +25 -16
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +3 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/config.ts +21 -0
- package/src/types/request.ts +5 -0
- package/src/types/wire.ts +1 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-7yhoys8a.js +0 -54
package/src/codex/features.ts
CHANGED
|
@@ -20,11 +20,12 @@
|
|
|
20
20
|
* Used by the catalog v2-gated-ultra policy (devlog/260709_v2_gated_ultra) and the
|
|
21
21
|
* `ocx v2` toggle surface. The FLAG itself is never written here — toggling goes
|
|
22
22
|
* through the official `codex features enable|disable` CLI (format-preserving).
|
|
23
|
-
* The
|
|
24
|
-
* `features.multi_agent_v2.max_concurrent_threads_per_session`
|
|
25
|
-
*
|
|
26
|
-
* feature config (`-c` is per-invocation only),
|
|
27
|
-
* EOL-preserving line
|
|
23
|
+
* The writes this module owns are the scoped scalars —
|
|
24
|
+
* `features.multi_agent_v2.max_concurrent_threads_per_session` and the
|
|
25
|
+
* `[agents]`/`[features]` fields the management API exposes: the codex CLI has
|
|
26
|
+
* no persisted setter for nested feature config (`-c` is per-invocation only),
|
|
27
|
+
* so ocx does scoped, EOL-preserving line edits — each one a read → edit →
|
|
28
|
+
* rename under the shared config write lock (see config-write-lock.ts).
|
|
28
29
|
*
|
|
29
30
|
* CODEX_HOME is resolved at CALL time (activeCodexConfigPath pattern, mirrors
|
|
30
31
|
* catalog.ts:40-54) so tests can point fixtures via env or the explicit
|
|
@@ -34,9 +35,12 @@ import { existsSync, readFileSync, statSync, unlinkSync } from "node:fs";
|
|
|
34
35
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
35
36
|
import { createRequire } from "node:module";
|
|
36
37
|
import { realpathSync } from "node:fs";
|
|
37
|
-
import {
|
|
38
|
+
import { homedir } from "node:os";
|
|
39
|
+
import { AtomicWriteResidualTempError, AtomicWriteSecretResidualError, atomicWriteFile, getConfigDir } from "../config";
|
|
40
|
+
import { ConfigWriteDestinationChanged, assertNativeConfigWriteDestination, runConfigWriteChild, withConfigWriteRecovery, publishConfigWrite, configWriteLockFailureMessage, withConfigWriteLock, withConfigWriteLockHeld, type LockHandle } from "./config-write-lock";
|
|
41
|
+
import { codexHomeIsAbsent } from "./codex-home-owner";
|
|
38
42
|
import { forgetEphemeralSecretPath } from "../lib/windows-secret-acl";
|
|
39
|
-
import {
|
|
43
|
+
import { resolveCodexHomeDir } from "./home";
|
|
40
44
|
import { resolveAndPersistCodexRuntime } from "./runtime";
|
|
41
45
|
import { decodeOverlayState } from "./shim-state-file";
|
|
42
46
|
import { canonicalizeOpenCodexModeHint } from "./multi-agent-mode-policy";
|
|
@@ -67,14 +71,14 @@ function mergeTrailingComments(existing?: string, migrated?: string): string {
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
export function activeCodexConfigPath(): string {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
// Keep home aliases observable by the held destination validator.
|
|
75
|
+
const home = resolveCodexHomeDir();
|
|
76
|
+
if (!process.env.CODEX_HOME?.trim()) {
|
|
77
|
+
const alias = join(homedir(), ".codex");
|
|
78
|
+
try { if (realpathSync.native(alias) === home) return join(alias, "config.toml"); }
|
|
79
|
+
catch { /* An absent local home can select the WSL Windows-home fallback. */ }
|
|
80
|
+
}
|
|
81
|
+
return join(home, "config.toml");
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
function readConfigText(configPath?: string): string | null {
|
|
@@ -87,6 +91,46 @@ function readConfigText(configPath?: string): string | null {
|
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
94
|
+
/**
|
|
95
|
+
* One edit's answer: either the replacement content or a refusal. `next` equal
|
|
96
|
+
* to the input is a no-change result — the helper compares before writing, so
|
|
97
|
+
* an edit never has to spell "unchanged" differently.
|
|
98
|
+
*/
|
|
99
|
+
type TomlEditOutcome = { next: string } | { error: string };
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* One read → edit → write pass over config.toml under the shared write lock.
|
|
103
|
+
*
|
|
104
|
+
* The edit callback sees bytes re-read INSIDE the lock, so it is always
|
|
105
|
+
* computed against what is actually on disk and no cooperating writer can land
|
|
106
|
+
* while the section runs. Previously every writer in this file read outside
|
|
107
|
+
* any lock and renamed over the whole file — a foreign rewrite landing between
|
|
108
|
+
* read and rename was silently discarded (a prompt-layer projection, another
|
|
109
|
+
* scalar edit, an injection's routing keys).
|
|
110
|
+
*
|
|
111
|
+
* A busy lock maps to the shared locked message: the sections under it are
|
|
112
|
+
* millisecond-scale, so refusing fast and letting the caller retry beats
|
|
113
|
+
* queuing behind whatever holds it.
|
|
114
|
+
*/
|
|
115
|
+
function editCodexConfigToml(
|
|
116
|
+
path: string,
|
|
117
|
+
edit: (content: string) => TomlEditOutcome,
|
|
118
|
+
heldConfigWriteLock?: LockHandle,
|
|
119
|
+
): ConfigEditResult {
|
|
120
|
+
if (codexHomeIsAbsent(dirname(path))) return { ok: false, error: `config.toml not readable at ${path}` };
|
|
121
|
+
const locked = withConfigWriteLockHeld(path, heldConfigWriteLock, (held): ConfigEditResult => {
|
|
122
|
+
const content = readConfigText(path);
|
|
123
|
+
if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
|
|
124
|
+
const outcome = edit(content);
|
|
125
|
+
if ("error" in outcome) return { ok: false, error: outcome.error };
|
|
126
|
+
if (outcome.next === content) return { ok: true, changed: false };
|
|
127
|
+
publishConfigWrite(path, held, (destination, hooks) => atomicWriteFile(destination, outcome.next, undefined, hooks));
|
|
128
|
+
return { ok: true, changed: true };
|
|
129
|
+
});
|
|
130
|
+
if (!locked.ok) return { ok: false, error: configWriteLockFailureMessage(locked), ...(locked.error === "unsafe" ? { retryable: false } : {}) };
|
|
131
|
+
return locked.value;
|
|
132
|
+
}
|
|
133
|
+
|
|
90
134
|
/**
|
|
91
135
|
* Body lines of a TOML table `[header]` up to (not including) the next table header.
|
|
92
136
|
*
|
|
@@ -409,26 +453,22 @@ export function v2TotalLimitToV1ChildLimit(totalLimit: number): number {
|
|
|
409
453
|
}
|
|
410
454
|
|
|
411
455
|
/**
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
* attached to the feature without a TOML key conflict. Idempotent on equal value.
|
|
456
|
+
* The content-level half of `setMaxConcurrentThreads`, reused by
|
|
457
|
+
* `ensureDisabledV2Config` so a nested edit never re-takes the write lock on
|
|
458
|
+
* the file its caller already holds.
|
|
416
459
|
*/
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const content = readConfigText(path);
|
|
423
|
-
if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
|
|
424
|
-
|
|
460
|
+
function maxConcurrentThreadsEdit(
|
|
461
|
+
content: string,
|
|
462
|
+
value: number,
|
|
463
|
+
migratedComment?: string,
|
|
464
|
+
): TomlEditOutcome {
|
|
425
465
|
const eol = dominantEol(content);
|
|
426
466
|
const lines = content.split(/\r?\n/);
|
|
427
467
|
const headerRe = /^\s*\[features\.multi_agent_v2\]\s*(?:#.*)?$/;
|
|
428
468
|
const headerIdx = lines.findIndex(l => headerRe.test(l));
|
|
429
469
|
if (headerIdx === -1) {
|
|
430
470
|
const featuresHeader = lines.findIndex(l => /^\s*\[features\]\s*(?:#.*)?$/.test(l));
|
|
431
|
-
if (featuresHeader === -1) return {
|
|
471
|
+
if (featuresHeader === -1) return { error: "multi_agent_v2 feature config not found — enable v2 first (ocx v2 on)" };
|
|
432
472
|
let featuresEnd = lines.length;
|
|
433
473
|
for (let i = featuresHeader + 1; i < lines.length; i++) {
|
|
434
474
|
if (/^\s*\[/.test(lines[i])) { featuresEnd = i; break; }
|
|
@@ -439,21 +479,19 @@ export function setMaxConcurrentThreads(value: number, configPath?: string, migr
|
|
|
439
479
|
const bool = lines[i].match(boolRe);
|
|
440
480
|
if (bool) {
|
|
441
481
|
lines[i] = `${bool[1]}multi_agent_v2 = { enabled = ${bool[2]}, max_concurrent_threads_per_session = ${value} }${mergeTrailingComments(bool[3], migratedComment)}`;
|
|
442
|
-
|
|
443
|
-
return { ok: true, changed: true };
|
|
482
|
+
return { next: applyEol(lines.join("\n"), eol) };
|
|
444
483
|
}
|
|
445
484
|
const inline = lines[i].match(inlineRe);
|
|
446
485
|
if (!inline) continue;
|
|
447
486
|
const existing = inline[2].match(/(?:^|,)\s*max_concurrent_threads_per_session\s*=\s*(\d+)\s*(?=,|$)/);
|
|
448
|
-
if (existing && Number(existing[1]) === value && (!migratedComment || migratedComment === inline[3])) return {
|
|
487
|
+
if (existing && Number(existing[1]) === value && (!migratedComment || migratedComment === inline[3])) return { next: content };
|
|
449
488
|
const body = existing
|
|
450
489
|
? inline[2].replace(/(^|,)\s*max_concurrent_threads_per_session\s*=\s*\d+\s*(?=,|$)/, `$1 max_concurrent_threads_per_session = ${value}`)
|
|
451
490
|
: `${inline[2].trim()}${inline[2].trim() ? ", " : ""}max_concurrent_threads_per_session = ${value}`;
|
|
452
491
|
lines[i] = `${inline[1]}multi_agent_v2 = { ${body.trim()} }${mergeTrailingComments(inline[3], migratedComment)}`;
|
|
453
|
-
|
|
454
|
-
return { ok: true, changed: true };
|
|
492
|
+
return { next: applyEol(lines.join("\n"), eol) };
|
|
455
493
|
}
|
|
456
|
-
return {
|
|
494
|
+
return { error: "multi_agent_v2 feature config not found — enable v2 first (ocx v2 on)" };
|
|
457
495
|
}
|
|
458
496
|
let end = lines.length;
|
|
459
497
|
for (let i = headerIdx + 1; i < lines.length; i++) {
|
|
@@ -463,17 +501,29 @@ export function setMaxConcurrentThreads(value: number, configPath?: string, migr
|
|
|
463
501
|
for (let i = headerIdx + 1; i < end; i++) {
|
|
464
502
|
const m = lines[i].match(keyRe);
|
|
465
503
|
if (!m) continue;
|
|
466
|
-
if (Number(m[2]) === value && (!migratedComment || migratedComment === m[3])) return {
|
|
504
|
+
if (Number(m[2]) === value && (!migratedComment || migratedComment === m[3])) return { next: content };
|
|
467
505
|
lines[i] = `${m[1]}max_concurrent_threads_per_session = ${value}${mergeTrailingComments(m[3], migratedComment)}`;
|
|
468
|
-
|
|
469
|
-
return { ok: true, changed: true };
|
|
506
|
+
return { next: applyEol(lines.join("\n"), eol) };
|
|
470
507
|
}
|
|
471
508
|
lines.splice(headerIdx + 1, 0, `max_concurrent_threads_per_session = ${value}${migratedComment ?? ""}`);
|
|
472
|
-
|
|
473
|
-
|
|
509
|
+
return { next: applyEol(lines.join("\n"), eol) };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Persist `features.multi_agent_v2.max_concurrent_threads_per_session = value`.
|
|
514
|
+
* Scoped edit in either the dedicated table or `[features]` boolean/inline form.
|
|
515
|
+
* Boolean form is upgraded to an inline config so the numeric value remains
|
|
516
|
+
* attached to the feature without a TOML key conflict. Idempotent on equal value.
|
|
517
|
+
*/
|
|
518
|
+
export function setMaxConcurrentThreads(value: number, configPath?: string, migratedComment?: string): { ok: true; changed: boolean } | { ok: false; error: string } {
|
|
519
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
520
|
+
return { ok: false, error: "max_concurrent_threads_per_session must be an integer >= 1" };
|
|
521
|
+
}
|
|
522
|
+
const path = configPath ?? activeCodexConfigPath();
|
|
523
|
+
return editCodexConfigToml(path, content => maxConcurrentThreadsEdit(content, value, migratedComment));
|
|
474
524
|
}
|
|
475
525
|
|
|
476
|
-
type ConfigEditResult = { ok: true; changed: boolean } | { ok: false; error: string };
|
|
526
|
+
type ConfigEditResult = { ok: true; changed: boolean } | { ok: false; error: string; retryable?: boolean };
|
|
477
527
|
|
|
478
528
|
/**
|
|
479
529
|
* Encode a string as a TOML single-line basic string.
|
|
@@ -807,14 +857,11 @@ function editScalarInTable(content: string, table: string, key: string, encoded:
|
|
|
807
857
|
}
|
|
808
858
|
|
|
809
859
|
/** Persist `[agents] enabled = value`, or remove the key when `value` is null. */
|
|
810
|
-
export function setAgentsEnabled(value: boolean | null, configPath?: string): ConfigEditResult {
|
|
860
|
+
export function setAgentsEnabled(value: boolean | null, configPath?: string, heldConfigWriteLock?: LockHandle): ConfigEditResult {
|
|
811
861
|
const path = configPath ?? activeCodexConfigPath();
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
if (next === content) return { ok: true, changed: false };
|
|
816
|
-
atomicWriteFile(path, next);
|
|
817
|
-
return { ok: true, changed: true };
|
|
862
|
+
return editCodexConfigToml(path, content => ({
|
|
863
|
+
next: editScalarInTable(content, "agents", "enabled", value === null ? null : String(value)),
|
|
864
|
+
}), heldConfigWriteLock);
|
|
818
865
|
}
|
|
819
866
|
|
|
820
867
|
/**
|
|
@@ -824,17 +871,14 @@ export function setAgentsEnabled(value: boolean | null, configPath?: string): Co
|
|
|
824
871
|
* produce a config upstream cannot deserialize, a hard parse failure for the
|
|
825
872
|
* user's Codex.
|
|
826
873
|
*/
|
|
827
|
-
export function setAgentsMaxDepth(value: number | null, configPath?: string): ConfigEditResult {
|
|
874
|
+
export function setAgentsMaxDepth(value: number | null, configPath?: string, heldConfigWriteLock?: LockHandle): ConfigEditResult {
|
|
828
875
|
if (value !== null && (!Number.isInteger(value) || value < -2_147_483_648 || value > 2_147_483_647)) {
|
|
829
876
|
return { ok: false, error: "max_depth must be an integer within signed i32 range" };
|
|
830
877
|
}
|
|
831
878
|
const path = configPath ?? activeCodexConfigPath();
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
if (next === content) return { ok: true, changed: false };
|
|
836
|
-
atomicWriteFile(path, next);
|
|
837
|
-
return { ok: true, changed: true };
|
|
879
|
+
return editCodexConfigToml(path, content => ({
|
|
880
|
+
next: editScalarInTable(content, "agents", "max_depth", value === null ? null : String(value)),
|
|
881
|
+
}), heldConfigWriteLock);
|
|
838
882
|
}
|
|
839
883
|
|
|
840
884
|
/**
|
|
@@ -950,11 +994,10 @@ export function getMultiAgentModeHintText(configPath?: string): string | null {
|
|
|
950
994
|
* carries `#[serde(deny_unknown_fields)]`, so a misspelling is not ignored — it is
|
|
951
995
|
* a hard config-parse failure for the user's Codex.
|
|
952
996
|
*/
|
|
953
|
-
function setV2StringField(key: string, value: string | null, configPath?: string): ConfigEditResult {
|
|
997
|
+
function setV2StringField(key: string, value: string | null, configPath?: string, heldConfigWriteLock?: LockHandle): ConfigEditResult {
|
|
954
998
|
const path = configPath ?? activeCodexConfigPath();
|
|
955
|
-
const content = readConfigText(path);
|
|
956
|
-
if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
|
|
957
999
|
const encoded = value === null ? null : encodeTomlBasicString(value);
|
|
1000
|
+
return editCodexConfigToml(path, (content): TomlEditOutcome => {
|
|
958
1001
|
|
|
959
1002
|
// A parsed V2 object without one of the supported source forms came from
|
|
960
1003
|
// dotted/quoted path segments. Appending a dedicated table would redefine it
|
|
@@ -966,7 +1009,7 @@ function setV2StringField(key: string, value: string | null, configPath?: string
|
|
|
966
1009
|
const featuresV2Entry = featuresBody === null ? null : findTomlAssignment(featuresBody, "multi_agent_v2");
|
|
967
1010
|
const supportedFeaturesEntry = featuresV2Entry !== null;
|
|
968
1011
|
if (parsedV2 !== null && !dedicatedV2 && !supportedFeaturesEntry) {
|
|
969
|
-
return {
|
|
1012
|
+
return { error: "dotted or quoted multi_agent_v2 config is not supported for managed string fields" };
|
|
970
1013
|
}
|
|
971
1014
|
|
|
972
1015
|
const dedicatedStringBody = tomlTableBodyForStringFields(content, "features.multi_agent_v2");
|
|
@@ -977,20 +1020,17 @@ function setV2StringField(key: string, value: string | null, configPath?: string
|
|
|
977
1020
|
// table carrying the same key does not block an otherwise safe edit.
|
|
978
1021
|
const dedicatedEntry = findTomlAssignment(dedicatedStringBody, key);
|
|
979
1022
|
if (dedicatedEntry !== null && isMultilineTomlString(dedicatedStringBody, dedicatedEntry.valueStart)) {
|
|
980
|
-
return {
|
|
1023
|
+
return { error: `multi-line TOML string for ${key} is not editable; convert it to a single-line string first` };
|
|
981
1024
|
}
|
|
982
1025
|
const legacyDedicatedBody = tomlTableBody(content, "features.multi_agent_v2") ?? "";
|
|
983
1026
|
if (dedicatedEntry !== null && findTomlAssignment(legacyDedicatedBody, key) === null) {
|
|
984
|
-
return {
|
|
1027
|
+
return { error: `cannot edit ${key} after a header-shaped multiline value safely` };
|
|
985
1028
|
}
|
|
986
|
-
|
|
987
|
-
if (next === content) return { ok: true, changed: false };
|
|
988
|
-
atomicWriteFile(path, next);
|
|
989
|
-
return { ok: true, changed: true };
|
|
1029
|
+
return { next: editScalarInTable(content, "features.multi_agent_v2", key, encoded) };
|
|
990
1030
|
}
|
|
991
1031
|
|
|
992
1032
|
if (featuresBody !== null && featuresV2Entry !== null && hasInlineMultilineTomlString(featuresBody, featuresV2Entry, key)) {
|
|
993
|
-
return {
|
|
1033
|
+
return { error: `multi-line TOML string for ${key} is not editable; convert it to a single-line string first` };
|
|
994
1034
|
}
|
|
995
1035
|
|
|
996
1036
|
const eol = dominantEol(content);
|
|
@@ -1007,10 +1047,10 @@ function setV2StringField(key: string, value: string | null, configPath?: string
|
|
|
1007
1047
|
if (inlineMatch) {
|
|
1008
1048
|
const openIdx = inlineMatch[0].length - 1;
|
|
1009
1049
|
const closeIdx = findInlineTableEnd(line, openIdx);
|
|
1010
|
-
if (closeIdx === -1) return {
|
|
1050
|
+
if (closeIdx === -1) return { error: "malformed multi_agent_v2 inline table" };
|
|
1011
1051
|
const entry = findInlineEntry(line, openIdx + 1, closeIdx, key);
|
|
1012
1052
|
if (encoded === null) {
|
|
1013
|
-
if (!entry) return {
|
|
1053
|
+
if (!entry) return { next: content };
|
|
1014
1054
|
let start = entry.keyStart;
|
|
1015
1055
|
let stop = entry.valueEnd;
|
|
1016
1056
|
let j = stop;
|
|
@@ -1024,11 +1064,10 @@ function setV2StringField(key: string, value: string | null, configPath?: string
|
|
|
1024
1064
|
if (line[k - 1] === ",") start = k - 1;
|
|
1025
1065
|
}
|
|
1026
1066
|
lines[i] = line.slice(0, start) + line.slice(stop);
|
|
1027
|
-
|
|
1028
|
-
return { ok: true, changed: true };
|
|
1067
|
+
return { next: applyEol(lines.join("\n"), eol) };
|
|
1029
1068
|
}
|
|
1030
1069
|
if (entry) {
|
|
1031
|
-
if (line.slice(entry.valueStart, entry.valueEnd).trim() === encoded) return {
|
|
1070
|
+
if (line.slice(entry.valueStart, entry.valueEnd).trim() === encoded) return { next: content };
|
|
1032
1071
|
lines[i] = line.slice(0, entry.valueStart) + encoded + line.slice(entry.valueEnd);
|
|
1033
1072
|
} else {
|
|
1034
1073
|
let insertPos = closeIdx;
|
|
@@ -1039,32 +1078,30 @@ function setV2StringField(key: string, value: string | null, configPath?: string
|
|
|
1039
1078
|
: ` ${key} = ${encoded} `;
|
|
1040
1079
|
lines[i] = line.slice(0, insertPos) + insertion + line.slice(closeIdx);
|
|
1041
1080
|
}
|
|
1042
|
-
|
|
1043
|
-
return { ok: true, changed: true };
|
|
1081
|
+
return { next: applyEol(lines.join("\n"), eol) };
|
|
1044
1082
|
}
|
|
1045
1083
|
const boolMatch = line.match(/^(\s*)multi_agent_v2\s*=\s*(true|false)(\s*(?:#.*)?)$/);
|
|
1046
1084
|
if (boolMatch) {
|
|
1047
|
-
if (encoded === null) return {
|
|
1085
|
+
if (encoded === null) return { next: content };
|
|
1048
1086
|
lines[i] = `${boolMatch[1]}multi_agent_v2 = { enabled = ${boolMatch[2]}, ${key} = ${encoded} }${boolMatch[3]}`;
|
|
1049
|
-
|
|
1050
|
-
return { ok: true, changed: true };
|
|
1087
|
+
return { next: applyEol(lines.join("\n"), eol) };
|
|
1051
1088
|
}
|
|
1052
1089
|
}
|
|
1053
1090
|
if (featuresV2Entry !== null) {
|
|
1054
|
-
return {
|
|
1091
|
+
return { error: "multi_agent_v2 inside a multiline [features] table is not editable safely" };
|
|
1055
1092
|
}
|
|
1056
1093
|
}
|
|
1057
1094
|
|
|
1058
|
-
if (encoded === null) return {
|
|
1095
|
+
if (encoded === null) return { next: content };
|
|
1059
1096
|
const suffix = content.endsWith("\n") || content.length === 0 ? "" : eol;
|
|
1060
1097
|
const separator = content.length > 0 && !content.endsWith(`${eol}${eol}`) ? eol : "";
|
|
1061
1098
|
const tableText = `[features.multi_agent_v2]${eol}${key} = ${encoded}${eol}`;
|
|
1062
|
-
|
|
1063
|
-
|
|
1099
|
+
return { next: `${content}${suffix}${separator}${tableText}` };
|
|
1100
|
+
}, heldConfigWriteLock);
|
|
1064
1101
|
}
|
|
1065
1102
|
|
|
1066
|
-
export function setSubagentDeveloperInstructions(value: string | null, configPath?: string): ConfigEditResult {
|
|
1067
|
-
return setV2StringField("subagent_developer_instructions", value, configPath);
|
|
1103
|
+
export function setSubagentDeveloperInstructions(value: string | null, configPath?: string, heldConfigWriteLock?: LockHandle): ConfigEditResult {
|
|
1104
|
+
return setV2StringField("subagent_developer_instructions", value, configPath, heldConfigWriteLock);
|
|
1068
1105
|
}
|
|
1069
1106
|
|
|
1070
1107
|
export const MODE_HINT_UNSUPPORTED_ERROR =
|
|
@@ -1076,7 +1113,7 @@ export const MODE_HINT_UNSUPPORTED_ERROR =
|
|
|
1076
1113
|
* `setSubagentDeveloperInstructions`; the upstream struct rejects unknown fields,
|
|
1077
1114
|
* so the key spelling must match codex-rs exactly.
|
|
1078
1115
|
*/
|
|
1079
|
-
export function setMultiAgentModeHintText(value: string | null, configPath?: string): ConfigEditResult {
|
|
1116
|
+
export function setMultiAgentModeHintText(value: string | null, configPath?: string, heldConfigWriteLock?: LockHandle): ConfigEditResult {
|
|
1080
1117
|
// The upstream `multi_agent_mode_hint_text` key is newer than the v2 config
|
|
1081
1118
|
// surface opencodex already manages; an older Codex build rejects the unknown
|
|
1082
1119
|
// member (`#[serde(deny_unknown_fields)]`) and fails to start. Probe the
|
|
@@ -1094,7 +1131,7 @@ export function setMultiAgentModeHintText(value: string | null, configPath?: str
|
|
|
1094
1131
|
}
|
|
1095
1132
|
}
|
|
1096
1133
|
const canonicalValue = value === null ? null : canonicalizeOpenCodexModeHint(value);
|
|
1097
|
-
return setV2StringField("multi_agent_mode_hint_text", canonicalValue, configPath);
|
|
1134
|
+
return setV2StringField("multi_agent_mode_hint_text", canonicalValue, configPath, heldConfigWriteLock);
|
|
1098
1135
|
}
|
|
1099
1136
|
|
|
1100
1137
|
export const MODE_HINT_CAPABILITY_CACHE_MAX_ENTRIES = 8;
|
|
@@ -1120,7 +1157,7 @@ export function probeCodexSupportsModeHint(): boolean | null {
|
|
|
1120
1157
|
const runtime = resolveAndPersistCodexRuntime({ env: process.env }).runtime;
|
|
1121
1158
|
const selectedPath = resolveSelectedCommandPath(runtime.command);
|
|
1122
1159
|
let selectedIdentity = selectedPath ?? "";
|
|
1123
|
-
try { if (selectedPath) selectedIdentity = realpathSync(selectedPath); } catch { /* keep lexical path */ }
|
|
1160
|
+
try { if (selectedPath) selectedIdentity = realpathSync.native(selectedPath); } catch { /* keep lexical path */ }
|
|
1124
1161
|
const candidates = codexNativeBinaryCandidates(runtime.command);
|
|
1125
1162
|
const binaryStatFingerprint = candidates.map(candidate => {
|
|
1126
1163
|
try {
|
|
@@ -1189,7 +1226,7 @@ function codexNativeBinaryCandidates(command: string): string[] {
|
|
|
1189
1226
|
|
|
1190
1227
|
const addSelectedTarget = (target: string) => {
|
|
1191
1228
|
try {
|
|
1192
|
-
const real = realpathSync(target);
|
|
1229
|
+
const real = realpathSync.native(target);
|
|
1193
1230
|
out.add(target);
|
|
1194
1231
|
out.add(real);
|
|
1195
1232
|
if (/[\\/]@openai[\\/]codex[\\/]bin[\\/]/.test(real)) resolverBases.add(real);
|
|
@@ -1271,91 +1308,87 @@ function resolveSelectedCommandPath(command: string): string | null {
|
|
|
1271
1308
|
|
|
1272
1309
|
function editAgentsMaxThreads(value: number | null, configPath?: string, migratedComment?: string): ConfigEditResult {
|
|
1273
1310
|
const path = configPath ?? activeCodexConfigPath();
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
lines.splice(headerIdx + 1, 0, `max_threads = ${value}${migratedComment ?? ""}`);
|
|
1302
|
-
atomicWriteFile(path, applyEol(lines.join("\n"), eol));
|
|
1303
|
-
return { ok: true, changed: true };
|
|
1311
|
+
return editCodexConfigToml(path, (content): TomlEditOutcome => {
|
|
1312
|
+
const eol = dominantEol(content);
|
|
1313
|
+
const lines = content.split(/\r?\n/);
|
|
1314
|
+
const headerIdx = lines.findIndex(l => /^\s*\[agents\]\s*(?:#.*)?$/.test(l));
|
|
1315
|
+
if (headerIdx === -1) {
|
|
1316
|
+
if (value === null) return { next: content };
|
|
1317
|
+
const separator = lines.length > 0 && lines[lines.length - 1] !== "" ? [""] : [];
|
|
1318
|
+
lines.push(...separator, "[agents]", `max_threads = ${value}${migratedComment ?? ""}`);
|
|
1319
|
+
return { next: applyEol(lines.join("\n"), eol) };
|
|
1320
|
+
}
|
|
1321
|
+
let end = lines.length;
|
|
1322
|
+
for (let i = headerIdx + 1; i < lines.length; i++) {
|
|
1323
|
+
if (/^\s*\[/.test(lines[i])) { end = i; break; }
|
|
1324
|
+
}
|
|
1325
|
+
const keyRe = /^(\s*)max_threads\s*=\s*(\d+)(\s*#.*)?$/;
|
|
1326
|
+
for (let i = headerIdx + 1; i < end; i++) {
|
|
1327
|
+
const m = lines[i].match(keyRe);
|
|
1328
|
+
if (!m) continue;
|
|
1329
|
+
if (value === null) lines.splice(i, 1);
|
|
1330
|
+
else if (Number(m[2]) === value && (!migratedComment || migratedComment === m[3])) return { next: content };
|
|
1331
|
+
else lines[i] = `${m[1]}max_threads = ${value}${mergeTrailingComments(m[3], migratedComment)}`;
|
|
1332
|
+
return { next: applyEol(lines.join("\n"), eol) };
|
|
1333
|
+
}
|
|
1334
|
+
if (value === null) return { next: content };
|
|
1335
|
+
lines.splice(headerIdx + 1, 0, `max_threads = ${value}${migratedComment ?? ""}`);
|
|
1336
|
+
return { next: applyEol(lines.join("\n"), eol) };
|
|
1337
|
+
});
|
|
1304
1338
|
}
|
|
1305
1339
|
|
|
1306
1340
|
function removeMaxConcurrentThreads(configPath?: string): ConfigEditResult {
|
|
1307
1341
|
const path = configPath ?? activeCodexConfigPath();
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1342
|
+
return editCodexConfigToml(path, (content): TomlEditOutcome => {
|
|
1343
|
+
const eol = dominantEol(content);
|
|
1344
|
+
const lines = content.split(/\r?\n/);
|
|
1345
|
+
const headerIdx = lines.findIndex(l => /^\s*\[features\.multi_agent_v2\]\s*(?:#.*)?$/.test(l));
|
|
1346
|
+
if (headerIdx !== -1) {
|
|
1347
|
+
let end = lines.length;
|
|
1348
|
+
for (let i = headerIdx + 1; i < lines.length; i++) {
|
|
1349
|
+
if (/^\s*\[/.test(lines[i])) { end = i; break; }
|
|
1350
|
+
}
|
|
1351
|
+
const keyIdx = lines.findIndex((line, i) => i > headerIdx && i < end && /^\s*max_concurrent_threads_per_session\s*=/.test(line));
|
|
1352
|
+
if (keyIdx !== -1) {
|
|
1353
|
+
lines.splice(keyIdx, 1);
|
|
1354
|
+
return { next: applyEol(lines.join("\n"), eol) };
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
const featuresHeader = lines.findIndex(l => /^\s*\[features\]\s*(?:#.*)?$/.test(l));
|
|
1358
|
+
if (featuresHeader === -1) return { next: content };
|
|
1359
|
+
let featuresEnd = lines.length;
|
|
1360
|
+
for (let i = featuresHeader + 1; i < lines.length; i++) {
|
|
1361
|
+
if (/^\s*\[/.test(lines[i])) { featuresEnd = i; break; }
|
|
1317
1362
|
}
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
lines.
|
|
1321
|
-
|
|
1322
|
-
|
|
1363
|
+
const inlineRe = /^(\s*)multi_agent_v2\s*=\s*\{([^}]*)\}(\s*#.*)?$/;
|
|
1364
|
+
for (let i = featuresHeader + 1; i < featuresEnd; i++) {
|
|
1365
|
+
const inline = lines[i].match(inlineRe);
|
|
1366
|
+
if (!inline || !/(?:^|,)\s*max_concurrent_threads_per_session\s*=/.test(inline[2])) continue;
|
|
1367
|
+
const body = inline[2]
|
|
1368
|
+
.replace(/^\s*max_concurrent_threads_per_session\s*=\s*\d+\s*,?\s*/, "")
|
|
1369
|
+
.replace(/,\s*max_concurrent_threads_per_session\s*=\s*\d+\s*(?=,|$)/, "")
|
|
1370
|
+
.trim();
|
|
1371
|
+
lines[i] = `${inline[1]}multi_agent_v2 = { ${body} }${inline[3] ?? ""}`;
|
|
1372
|
+
return { next: applyEol(lines.join("\n"), eol) };
|
|
1323
1373
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
if (featuresHeader === -1) return { ok: true, changed: false };
|
|
1327
|
-
let featuresEnd = lines.length;
|
|
1328
|
-
for (let i = featuresHeader + 1; i < lines.length; i++) {
|
|
1329
|
-
if (/^\s*\[/.test(lines[i])) { featuresEnd = i; break; }
|
|
1330
|
-
}
|
|
1331
|
-
const inlineRe = /^(\s*)multi_agent_v2\s*=\s*\{([^}]*)\}(\s*#.*)?$/;
|
|
1332
|
-
for (let i = featuresHeader + 1; i < featuresEnd; i++) {
|
|
1333
|
-
const inline = lines[i].match(inlineRe);
|
|
1334
|
-
if (!inline || !/(?:^|,)\s*max_concurrent_threads_per_session\s*=/.test(inline[2])) continue;
|
|
1335
|
-
const body = inline[2]
|
|
1336
|
-
.replace(/^\s*max_concurrent_threads_per_session\s*=\s*\d+\s*,?\s*/, "")
|
|
1337
|
-
.replace(/,\s*max_concurrent_threads_per_session\s*=\s*\d+\s*(?=,|$)/, "")
|
|
1338
|
-
.trim();
|
|
1339
|
-
lines[i] = `${inline[1]}multi_agent_v2 = { ${body} }${inline[3] ?? ""}`;
|
|
1340
|
-
atomicWriteFile(path, applyEol(lines.join("\n"), eol));
|
|
1341
|
-
return { ok: true, changed: true };
|
|
1342
|
-
}
|
|
1343
|
-
return { ok: true, changed: false };
|
|
1374
|
+
return { next: content };
|
|
1375
|
+
});
|
|
1344
1376
|
}
|
|
1345
1377
|
|
|
1346
1378
|
function ensureDisabledV2Config(value: number | null, configPath?: string, migratedComment?: string): ConfigEditResult {
|
|
1347
1379
|
const path = configPath ?? activeCodexConfigPath();
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1380
|
+
return editCodexConfigToml(path, (content): TomlEditOutcome => {
|
|
1381
|
+
if (tomlTableBody(content, "features.multi_agent_v2") !== null || tomlTableBody(content, "features")?.match(/^\s*multi_agent_v2\s*=/m)) {
|
|
1382
|
+
if (value === null) return { next: content };
|
|
1383
|
+
// The content-level edit, not the exported writer: this section already
|
|
1384
|
+
// holds `path`'s write lock and a nested acquire would refuse itself.
|
|
1385
|
+
return maxConcurrentThreadsEdit(content, value, migratedComment);
|
|
1386
|
+
}
|
|
1387
|
+
const eol = dominantEol(content);
|
|
1388
|
+
const suffix = content.endsWith("\n") || content.length === 0 ? "" : eol;
|
|
1389
|
+
const table = `[features.multi_agent_v2]${eol}enabled = false${value === null ? "" : `${eol}max_concurrent_threads_per_session = ${value}${migratedComment ?? ""}`}${eol}`;
|
|
1390
|
+
return { next: `${content}${suffix}${content.length > 0 && !content.endsWith(`${eol}${eol}`) ? eol : ""}${table}` };
|
|
1391
|
+
});
|
|
1359
1392
|
}
|
|
1360
1393
|
|
|
1361
1394
|
/**
|
|
@@ -1425,7 +1458,16 @@ export function isAtomicResidualError(error: unknown): boolean {
|
|
|
1425
1458
|
return error instanceof AtomicWriteResidualTempError || error instanceof AtomicWriteSecretResidualError;
|
|
1426
1459
|
}
|
|
1427
1460
|
|
|
1428
|
-
|
|
1461
|
+
/**
|
|
1462
|
+
* Stage the temp-copy edits then swap `path` in one pass.
|
|
1463
|
+
*
|
|
1464
|
+
* Caller must already hold `path`'s config write lock —
|
|
1465
|
+
* `transitionMultiAgentV2` is that caller, and it holds the lock across the
|
|
1466
|
+
* whole migration so a foreign write cannot land between its stages or be
|
|
1467
|
+
* discarded by the byte-restore rollback. The per-call edits inside run on the
|
|
1468
|
+
* temp path, which takes its own lock harmlessly.
|
|
1469
|
+
*/
|
|
1470
|
+
function applyConfigEditsAtomically(path: string, held: LockHandle, edit: (tempPath: string) => ConfigEditResult): ConfigEditResult {
|
|
1429
1471
|
const content = readConfigText(path);
|
|
1430
1472
|
if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
|
|
1431
1473
|
const tempPath = `${path}.ocx-migration.${process.pid}.${++migrationEditSeq}`;
|
|
@@ -1439,7 +1481,7 @@ function applyConfigEditsAtomically(path: string, edit: (tempPath: string) => Co
|
|
|
1439
1481
|
const edited = readConfigText(tempPath);
|
|
1440
1482
|
if (edited === null) return { ok: false, error: "temporary config migration output is unreadable" };
|
|
1441
1483
|
if (edited === content) return { ok: true, changed: false };
|
|
1442
|
-
|
|
1484
|
+
publishConfigWrite(path, held, (destination, hooks) => atomicWriteFile(destination, edited, undefined, hooks));
|
|
1443
1485
|
return { ok: true, changed: true };
|
|
1444
1486
|
} catch (error) {
|
|
1445
1487
|
if (isAtomicResidualError(error)) innerResidual = true;
|
|
@@ -1459,7 +1501,7 @@ function applyConfigEditsAtomically(path: string, edit: (tempPath: string) => Co
|
|
|
1459
1501
|
|
|
1460
1502
|
export type MultiAgentV2TransitionResult =
|
|
1461
1503
|
| { ok: true; changed: boolean; threadLimit: number | null }
|
|
1462
|
-
| { ok: false; error: string };
|
|
1504
|
+
| { ok: false; error: string; retryable?: boolean };
|
|
1463
1505
|
|
|
1464
1506
|
function transitionConfigError(content: string): string | null {
|
|
1465
1507
|
if (/^\s*(?:features\.multi_agent_v2(?:\.[A-Za-z0-9_]+)?|agents\.max_threads)\s*=/m.test(content)) {
|
|
@@ -1493,17 +1535,19 @@ function transitionConfigError(content: string): string | null {
|
|
|
1493
1535
|
/**
|
|
1494
1536
|
* Toggle native multi_agent_v2 while moving the active thread limit to the key
|
|
1495
1537
|
* valid for the destination version. Any failed command/postcondition restores
|
|
1496
|
-
* the exact original config bytes.
|
|
1538
|
+
* the exact original config bytes; alias drift restores only the locked canonical target.
|
|
1497
1539
|
*/
|
|
1498
1540
|
export function transitionMultiAgentV2(
|
|
1499
1541
|
enabled: boolean,
|
|
1500
|
-
toggleFeature: (enabled: boolean) => void,
|
|
1501
|
-
options: { configPath?: string; threadLimit?: number } = {},
|
|
1542
|
+
toggleFeature: (enabled: boolean, env: NodeJS.ProcessEnv, validateBeforeSpawn: () => void) => void,
|
|
1543
|
+
options: { configPath?: string; threadLimit?: number; heldConfigWriteLock?: LockHandle } = {},
|
|
1502
1544
|
): MultiAgentV2TransitionResult {
|
|
1503
1545
|
if (options.threadLimit !== undefined && (!Number.isInteger(options.threadLimit) || options.threadLimit < 1)) {
|
|
1504
1546
|
return { ok: false, error: "thread limit must be an integer >= 1" };
|
|
1505
1547
|
}
|
|
1506
1548
|
const path = options.configPath ?? activeCodexConfigPath();
|
|
1549
|
+
if (codexHomeIsAbsent(dirname(path))) return { ok: false, error: `config.toml not readable at ${path}` };
|
|
1550
|
+
const runTransition = (held: LockHandle): MultiAgentV2TransitionResult => {
|
|
1507
1551
|
const original = readConfigText(path);
|
|
1508
1552
|
if (original === null) return { ok: false, error: `config.toml not readable at ${path}` };
|
|
1509
1553
|
const preflightError = transitionConfigError(original);
|
|
@@ -1531,45 +1575,58 @@ export function transitionMultiAgentV2(
|
|
|
1531
1575
|
}
|
|
1532
1576
|
const migratedComment = activeThreadComment(original, beforeEnabled);
|
|
1533
1577
|
try {
|
|
1534
|
-
|
|
1535
|
-
if (
|
|
1536
|
-
|
|
1537
|
-
|
|
1578
|
+
return withConfigWriteRecovery(path, held, () => {
|
|
1579
|
+
if (beforeEnabled !== enabled) assertNativeConfigWriteDestination(path, held);
|
|
1580
|
+
if (enabled) {
|
|
1581
|
+
if (!beforeEnabled) {
|
|
1582
|
+
const staged = applyConfigEditsAtomically(path, held, tempPath => {
|
|
1583
|
+
const v2 = ensureDisabledV2Config(threadLimit, tempPath, migratedComment);
|
|
1584
|
+
if (!v2.ok) return v2;
|
|
1585
|
+
return editAgentsMaxThreads(null, tempPath);
|
|
1586
|
+
});
|
|
1587
|
+
if (!staged.ok) throw new Error(staged.error);
|
|
1588
|
+
runConfigWriteChild(path, held, (env, validate) => toggleFeature(true, env, validate));
|
|
1589
|
+
}
|
|
1590
|
+
if (!isMultiAgentV2Enabled(path)) throw new Error("codex feature command did not enable multi_agent_v2");
|
|
1591
|
+
const target = applyConfigEditsAtomically(path, held, tempPath => {
|
|
1592
|
+
const v2 = threadLimit === null
|
|
1593
|
+
? removeMaxConcurrentThreads(tempPath)
|
|
1594
|
+
: setMaxConcurrentThreads(threadLimit, tempPath, migratedComment);
|
|
1538
1595
|
if (!v2.ok) return v2;
|
|
1539
1596
|
return editAgentsMaxThreads(null, tempPath);
|
|
1540
1597
|
});
|
|
1541
|
-
if (!
|
|
1542
|
-
|
|
1598
|
+
if (!target.ok) throw new Error(target.error);
|
|
1599
|
+
if (hasAgentsMaxThreads(path) || getMaxConcurrentThreads(path) !== threadLimit) throw new Error("v2 thread-limit migration postcondition failed");
|
|
1600
|
+
} else {
|
|
1601
|
+
if (beforeEnabled) runConfigWriteChild(path, held, (env, validate) => toggleFeature(false, env, validate));
|
|
1602
|
+
if (isMultiAgentV2Enabled(path)) throw new Error("codex feature command did not disable multi_agent_v2");
|
|
1603
|
+
const target = applyConfigEditsAtomically(path, held, tempPath => {
|
|
1604
|
+
const v2 = removeMaxConcurrentThreads(tempPath);
|
|
1605
|
+
if (!v2.ok) return v2;
|
|
1606
|
+
return editAgentsMaxThreads(threadLimit, tempPath, migratedComment);
|
|
1607
|
+
});
|
|
1608
|
+
if (!target.ok) throw new Error(target.error);
|
|
1609
|
+
if (getMaxConcurrentThreads(path) !== null || getAgentsMaxThreads(path) !== threadLimit) throw new Error("v1 thread-limit migration postcondition failed");
|
|
1543
1610
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
const v2 = threadLimit === null
|
|
1547
|
-
? removeMaxConcurrentThreads(tempPath)
|
|
1548
|
-
: setMaxConcurrentThreads(threadLimit, tempPath, migratedComment);
|
|
1549
|
-
if (!v2.ok) return v2;
|
|
1550
|
-
return editAgentsMaxThreads(null, tempPath);
|
|
1551
|
-
});
|
|
1552
|
-
if (!target.ok) throw new Error(target.error);
|
|
1553
|
-
if (hasAgentsMaxThreads(path) || getMaxConcurrentThreads(path) !== threadLimit) throw new Error("v2 thread-limit migration postcondition failed");
|
|
1554
|
-
} else {
|
|
1555
|
-
if (beforeEnabled) toggleFeature(false);
|
|
1556
|
-
if (isMultiAgentV2Enabled(path)) throw new Error("codex feature command did not disable multi_agent_v2");
|
|
1557
|
-
const target = applyConfigEditsAtomically(path, tempPath => {
|
|
1558
|
-
const v2 = removeMaxConcurrentThreads(tempPath);
|
|
1559
|
-
if (!v2.ok) return v2;
|
|
1560
|
-
return editAgentsMaxThreads(threadLimit, tempPath, migratedComment);
|
|
1561
|
-
});
|
|
1562
|
-
if (!target.ok) throw new Error(target.error);
|
|
1563
|
-
if (getMaxConcurrentThreads(path) !== null || getAgentsMaxThreads(path) !== threadLimit) throw new Error("v1 thread-limit migration postcondition failed");
|
|
1564
|
-
}
|
|
1565
|
-
return { ok: true, changed: readConfigText(path) !== original, threadLimit };
|
|
1611
|
+
return { ok: true, changed: readConfigText(path) !== original, threadLimit };
|
|
1612
|
+
});
|
|
1566
1613
|
} catch (err) {
|
|
1567
1614
|
const message = err instanceof Error ? err.message : String(err);
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
return { ok: false, error: message };
|
|
1571
|
-
} catch (rollbackErr) {
|
|
1572
|
-
return { ok: false, error: `${message}; rollback failed: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}` };
|
|
1573
|
-
}
|
|
1615
|
+
if (err instanceof ConfigWriteDestinationChanged) return { ok: false, error: message, retryable: false };
|
|
1616
|
+
return { ok: false, error: message };
|
|
1574
1617
|
}
|
|
1618
|
+
};
|
|
1619
|
+
/*
|
|
1620
|
+
* The whole migration — staged edits, the codex-features subprocess, the
|
|
1621
|
+
* postcondition checks, and a possible byte-restore rollback — runs under
|
|
1622
|
+
* `path`'s write lock. A foreign write that could once land between stages
|
|
1623
|
+
* is now refused instead of silently surviving into the rolled-back bytes.
|
|
1624
|
+
*
|
|
1625
|
+
* A caller that already holds the lock (the injector, whose wider section
|
|
1626
|
+
* must also cover the artifact commit) passes its handle: re-acquiring the
|
|
1627
|
+
* same lock file would refuse itself.
|
|
1628
|
+
*/
|
|
1629
|
+
const locked = withConfigWriteLockHeld(path, options.heldConfigWriteLock, runTransition);
|
|
1630
|
+
if (!locked.ok) return { ok: false, error: configWriteLockFailureMessage(locked), ...(locked.error === "unsafe" ? { retryable: false } : {}) };
|
|
1631
|
+
return locked.value;
|
|
1575
1632
|
}
|