@kbediako/codex-orchestrator 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +6 -1
  2. package/dist/bin/codex-orchestrator.js +38 -0
  3. package/dist/orchestrator/src/cli/config/delegationConfig.js +485 -0
  4. package/dist/orchestrator/src/cli/control/confirmations.js +262 -0
  5. package/dist/orchestrator/src/cli/control/controlServer.js +1476 -0
  6. package/dist/orchestrator/src/cli/control/controlState.js +46 -0
  7. package/dist/orchestrator/src/cli/control/controlWatcher.js +222 -0
  8. package/dist/orchestrator/src/cli/control/delegationTokens.js +62 -0
  9. package/dist/orchestrator/src/cli/control/questions.js +106 -0
  10. package/dist/orchestrator/src/cli/delegationServer.js +1368 -0
  11. package/dist/orchestrator/src/cli/events/runEventStream.js +246 -0
  12. package/dist/orchestrator/src/cli/exec/context.js +4 -1
  13. package/dist/orchestrator/src/cli/exec/stageRunner.js +30 -5
  14. package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +377 -147
  15. package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +3 -5
  16. package/dist/orchestrator/src/cli/orchestrator.js +217 -40
  17. package/dist/orchestrator/src/cli/rlmRunner.js +26 -3
  18. package/dist/orchestrator/src/cli/run/manifestPersister.js +33 -3
  19. package/dist/orchestrator/src/cli/run/runPaths.js +14 -0
  20. package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
  21. package/dist/orchestrator/src/cli/utils/devtools.js +33 -2
  22. package/dist/orchestrator/src/persistence/ExperienceStore.js +113 -46
  23. package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +8 -8
  24. package/dist/orchestrator/src/persistence/TaskStateStore.js +2 -1
  25. package/dist/orchestrator/src/persistence/lockFile.js +26 -1
  26. package/dist/orchestrator/src/sync/CloudSyncWorker.js +17 -4
  27. package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +21 -0
  28. package/package.json +3 -1
@@ -5,7 +5,7 @@ import { ensureGuardrailStatus, appendSummary, upsertGuardrailSummary } from '..
5
5
  import { isoTimestamp } from '../utils/time.js';
6
6
  import { persistManifest } from '../run/manifestPersister.js';
7
7
  import { logger } from '../../logger.js';
8
- import { mergePendingMetricsEntries, updateMetricsAggregates, withMetricsLock } from './metricsAggregator.js';
8
+ import { ensureMetricsTrailingNewline, mergePendingMetricsEntries, updateMetricsAggregates, withMetricsLock } from './metricsAggregator.js';
9
9
  import { EnvUtils } from '../../../../packages/shared/config/index.js';
10
10
  const TERMINAL_STATES = new Set(['succeeded', 'failed', 'cancelled']);
11
11
  const METRICS_PENDING_DIRNAME = 'metrics.pending';
@@ -89,6 +89,7 @@ export async function appendMetricsEntry(env, paths, manifest, persister) {
89
89
  };
90
90
  await mkdir(metricsRoot, { recursive: true });
91
91
  const appendEntry = async () => {
92
+ await ensureMetricsTrailingNewline(metricsPath);
92
93
  await appendFile(metricsPath, `${JSON.stringify(entry)}\n`, 'utf8');
93
94
  };
94
95
  const appendPendingEntry = async () => {
@@ -124,11 +125,8 @@ export async function appendMetricsEntry(env, paths, manifest, persister) {
124
125
  await mergePendingMetricsEntries(env);
125
126
  await appendEntry();
126
127
  await finalizeManifest(true);
128
+ await mergePendingMetricsEntries(env);
127
129
  await updateMetricsAggregates(env);
128
- const mergedAfter = await mergePendingMetricsEntries(env);
129
- if (mergedAfter > 0) {
130
- await updateMetricsAggregates(env);
131
- }
132
130
  });
133
131
  if (!acquired) {
134
132
  const pendingPath = await appendPendingEntry();
@@ -18,13 +18,40 @@ import { logger } from '../logger.js';
18
18
  import { getPrivacyGuard } from './services/execRuntime.js';
19
19
  import { PipelineResolver } from './services/pipelineResolver.js';
20
20
  import { ControlPlaneService } from './services/controlPlaneService.js';
21
+ import { ControlWatcher } from './control/controlWatcher.js';
21
22
  import { SchedulerService } from './services/schedulerService.js';
22
23
  import { applyHandlesToRunSummary, applyPrivacyToRunSummary, persistRunSummary } from './services/runSummaryWriter.js';
23
24
  import { prepareRun, resolvePipelineForResume, overrideTaskEnvironment } from './services/runPreparation.js';
24
25
  import { loadPackageConfig, loadUserConfig } from './config/userConfig.js';
25
- import { RunEventPublisher, snapshotStages } from './events/runEvents.js';
26
+ import { loadDelegationConfigFiles, computeEffectiveDelegationConfig, parseDelegationConfigOverride, splitDelegationConfigOverrides } from './config/delegationConfig.js';
27
+ import { ControlServer } from './control/controlServer.js';
28
+ import { RunEventEmitter, RunEventPublisher, snapshotStages } from './events/runEvents.js';
29
+ import { RunEventStream, attachRunEventAdapter } from './events/runEventStream.js';
26
30
  import { CLI_EXECUTION_MODE_PARSER, resolveRequiresCloudPolicy } from '../utils/executionMode.js';
27
31
  const resolveBaseEnvironment = () => normalizeEnvironmentPaths(resolveEnvironmentPaths());
32
+ const CONFIG_OVERRIDE_ENV_KEYS = ['CODEX_CONFIG_OVERRIDES', 'CODEX_MCP_CONFIG_OVERRIDES'];
33
+ function collectDelegationEnvOverrides(env = process.env) {
34
+ const layers = [];
35
+ for (const key of CONFIG_OVERRIDE_ENV_KEYS) {
36
+ const raw = env[key];
37
+ if (!raw) {
38
+ continue;
39
+ }
40
+ const values = splitDelegationConfigOverrides(raw);
41
+ for (const value of values) {
42
+ try {
43
+ const layer = parseDelegationConfigOverride(value, 'env');
44
+ if (layer) {
45
+ layers.push(layer);
46
+ }
47
+ }
48
+ catch (error) {
49
+ logger.warn(`Invalid delegation config override (env): ${error?.message ?? String(error)}`);
50
+ }
51
+ }
52
+ }
53
+ return layers;
54
+ }
28
55
  export class CodexOrchestrator {
29
56
  baseEnv;
30
57
  controlPlane = new ControlPlaneService();
@@ -54,25 +81,90 @@ export class CodexOrchestrator {
54
81
  paths,
55
82
  persistIntervalMs: Math.max(1000, manifest.heartbeat_interval_seconds * 1000)
56
83
  });
57
- const runEvents = this.createRunEventPublisher({
58
- runId,
59
- pipeline: preparation.pipeline,
60
- manifest,
61
- paths,
62
- emitter: options.runEvents
63
- });
64
- return await this.performRunLifecycle({
65
- env: preparation.env,
66
- pipeline: preparation.pipeline,
67
- manifest,
68
- paths,
69
- planner: preparation.planner,
70
- taskContext: preparation.taskContext,
71
- runId,
72
- runEvents,
73
- persister,
74
- envOverrides: preparation.envOverrides
75
- });
84
+ const emitter = options.runEvents ?? new RunEventEmitter();
85
+ let eventStream = null;
86
+ let controlServer = null;
87
+ let detachStream = null;
88
+ let onEventEntry;
89
+ try {
90
+ const stream = await RunEventStream.create({
91
+ paths,
92
+ taskId: manifest.task_id,
93
+ runId,
94
+ pipelineId: preparation.pipeline.id,
95
+ pipelineTitle: preparation.pipeline.title
96
+ });
97
+ eventStream = stream;
98
+ const configFiles = await loadDelegationConfigFiles({ repoRoot: preparation.env.repoRoot });
99
+ const envOverrideLayers = collectDelegationEnvOverrides();
100
+ const layers = [configFiles.global, configFiles.repo, ...envOverrideLayers].filter(Boolean);
101
+ const effectiveConfig = computeEffectiveDelegationConfig({
102
+ repoRoot: preparation.env.repoRoot,
103
+ layers
104
+ });
105
+ controlServer = effectiveConfig.ui.controlEnabled
106
+ ? await ControlServer.start({
107
+ paths,
108
+ config: effectiveConfig,
109
+ eventStream: stream,
110
+ runId
111
+ })
112
+ : null;
113
+ onEventEntry = (entry) => {
114
+ controlServer?.broadcast(entry);
115
+ };
116
+ const onStreamError = (error, payload) => {
117
+ logger.warn(`Failed to append run event ${payload.event}: ${error.message}`);
118
+ };
119
+ detachStream = attachRunEventAdapter(emitter, stream, onEventEntry, onStreamError);
120
+ const runEvents = this.createRunEventPublisher({
121
+ runId,
122
+ pipeline: preparation.pipeline,
123
+ manifest,
124
+ paths,
125
+ emitter
126
+ });
127
+ return await this.performRunLifecycle({
128
+ env: preparation.env,
129
+ pipeline: preparation.pipeline,
130
+ manifest,
131
+ paths,
132
+ planner: preparation.planner,
133
+ taskContext: preparation.taskContext,
134
+ runId,
135
+ runEvents,
136
+ eventStream: stream,
137
+ onEventEntry,
138
+ persister,
139
+ envOverrides: preparation.envOverrides
140
+ });
141
+ }
142
+ finally {
143
+ if (detachStream) {
144
+ try {
145
+ detachStream();
146
+ }
147
+ catch (error) {
148
+ logger.warn(`Failed to detach run event stream: ${error?.message ?? String(error)}`);
149
+ }
150
+ }
151
+ if (controlServer) {
152
+ try {
153
+ await controlServer.close();
154
+ }
155
+ catch (error) {
156
+ logger.warn(`Failed to close control server: ${error?.message ?? String(error)}`);
157
+ }
158
+ }
159
+ if (eventStream) {
160
+ try {
161
+ await eventStream.close();
162
+ }
163
+ catch (error) {
164
+ logger.warn(`Failed to close run event stream: ${error?.message ?? String(error)}`);
165
+ }
166
+ }
167
+ }
76
168
  }
77
169
  async resume(options) {
78
170
  const env = this.baseEnv;
@@ -110,25 +202,90 @@ export class CodexOrchestrator {
110
202
  persistIntervalMs: Math.max(1000, manifest.heartbeat_interval_seconds * 1000)
111
203
  });
112
204
  await persister.schedule({ manifest: true, heartbeat: true, force: true });
113
- const runEvents = this.createRunEventPublisher({
114
- runId: manifest.run_id,
115
- pipeline,
116
- manifest,
117
- paths,
118
- emitter: options.runEvents
119
- });
120
- return await this.performRunLifecycle({
121
- env: preparation.env,
122
- pipeline,
123
- manifest,
124
- paths,
125
- planner: preparation.planner,
126
- taskContext: preparation.taskContext,
127
- runId: manifest.run_id,
128
- runEvents,
129
- persister,
130
- envOverrides: preparation.envOverrides
131
- });
205
+ const emitter = options.runEvents ?? new RunEventEmitter();
206
+ let eventStream = null;
207
+ let controlServer = null;
208
+ let detachStream = null;
209
+ let onEventEntry;
210
+ try {
211
+ const stream = await RunEventStream.create({
212
+ paths,
213
+ taskId: manifest.task_id,
214
+ runId: manifest.run_id,
215
+ pipelineId: pipeline.id,
216
+ pipelineTitle: pipeline.title
217
+ });
218
+ eventStream = stream;
219
+ const configFiles = await loadDelegationConfigFiles({ repoRoot: preparation.env.repoRoot });
220
+ const envOverrideLayers = collectDelegationEnvOverrides();
221
+ const layers = [configFiles.global, configFiles.repo, ...envOverrideLayers].filter(Boolean);
222
+ const effectiveConfig = computeEffectiveDelegationConfig({
223
+ repoRoot: preparation.env.repoRoot,
224
+ layers
225
+ });
226
+ controlServer = effectiveConfig.ui.controlEnabled
227
+ ? await ControlServer.start({
228
+ paths,
229
+ config: effectiveConfig,
230
+ eventStream: stream,
231
+ runId: manifest.run_id
232
+ })
233
+ : null;
234
+ onEventEntry = (entry) => {
235
+ controlServer?.broadcast(entry);
236
+ };
237
+ const onStreamError = (error, payload) => {
238
+ logger.warn(`Failed to append run event ${payload.event}: ${error.message}`);
239
+ };
240
+ detachStream = attachRunEventAdapter(emitter, stream, onEventEntry, onStreamError);
241
+ const runEvents = this.createRunEventPublisher({
242
+ runId: manifest.run_id,
243
+ pipeline,
244
+ manifest,
245
+ paths,
246
+ emitter
247
+ });
248
+ return await this.performRunLifecycle({
249
+ env: preparation.env,
250
+ pipeline,
251
+ manifest,
252
+ paths,
253
+ planner: preparation.planner,
254
+ taskContext: preparation.taskContext,
255
+ runId: manifest.run_id,
256
+ runEvents,
257
+ eventStream: stream,
258
+ onEventEntry,
259
+ persister,
260
+ envOverrides: preparation.envOverrides
261
+ });
262
+ }
263
+ finally {
264
+ if (detachStream) {
265
+ try {
266
+ detachStream();
267
+ }
268
+ catch (error) {
269
+ logger.warn(`Failed to detach run event stream: ${error?.message ?? String(error)}`);
270
+ }
271
+ }
272
+ if (controlServer) {
273
+ try {
274
+ await controlServer.close();
275
+ }
276
+ catch (error) {
277
+ logger.warn(`Failed to close control server: ${error?.message ?? String(error)}`);
278
+ }
279
+ }
280
+ if (eventStream) {
281
+ try {
282
+ await eventStream.close();
283
+ }
284
+ catch (error) {
285
+ logger.warn(`Failed to close run event stream: ${error?.message ?? String(error)}`);
286
+ }
287
+ }
288
+ }
132
289
  }
133
290
  async status(options) {
134
291
  const env = this.baseEnv;
@@ -260,6 +417,13 @@ export class CodexOrchestrator {
260
417
  updateHeartbeat(manifest);
261
418
  return schedulePersist({ manifest: forceManifest, heartbeat: true, force: forceManifest });
262
419
  };
420
+ const controlWatcher = new ControlWatcher({
421
+ paths,
422
+ manifest,
423
+ eventStream: options.eventStream,
424
+ onEntry: options.onEventEntry,
425
+ persist: () => schedulePersist({ manifest: true, force: true })
426
+ });
263
427
  manifest.status = 'in_progress';
264
428
  updateHeartbeat(manifest);
265
429
  await schedulePersist({ manifest: true, heartbeat: true, force: true });
@@ -271,6 +435,13 @@ export class CodexOrchestrator {
271
435
  }, manifest.heartbeat_interval_seconds * 1000);
272
436
  try {
273
437
  for (let i = 0; i < pipeline.stages.length; i += 1) {
438
+ await controlWatcher.sync();
439
+ await controlWatcher.waitForResume();
440
+ if (controlWatcher.isCanceled()) {
441
+ manifest.status_detail = 'run-canceled';
442
+ success = false;
443
+ break;
444
+ }
274
445
  const stage = pipeline.stages[i];
275
446
  const entry = manifest.commands[i];
276
447
  if (!entry) {
@@ -408,7 +579,11 @@ export class CodexOrchestrator {
408
579
  clearInterval(heartbeatInterval);
409
580
  await schedulePersist({ force: true });
410
581
  }
411
- if (success) {
582
+ await controlWatcher.sync();
583
+ if (controlWatcher.isCanceled()) {
584
+ finalizeStatus(manifest, 'cancelled', manifest.status_detail ?? 'run-canceled');
585
+ }
586
+ else if (success) {
412
587
  finalizeStatus(manifest, 'succeeded');
413
588
  }
414
589
  else {
@@ -444,6 +619,8 @@ export class CodexOrchestrator {
444
619
  manifest,
445
620
  paths,
446
621
  runEvents: context.runEvents,
622
+ eventStream: context.eventStream,
623
+ onEventEntry: context.onEventEntry,
447
624
  persister,
448
625
  envOverrides
449
626
  }).then((result) => {
@@ -13,6 +13,8 @@ import { buildRlmPrompt } from './rlm/prompt.js';
13
13
  import { runRlmLoop } from './rlm/runner.js';
14
14
  const execAsync = promisify(exec);
15
15
  const DEFAULT_MAX_ITERATIONS = 88;
16
+ const DEFAULT_MAX_MINUTES = 48 * 60;
17
+ const UNBOUNDED_ITERATION_ALIASES = new Set(['unbounded', 'unlimited', 'infinite', 'infinity']);
16
18
  function parseArgs(argv) {
17
19
  const parsed = {};
18
20
  for (let i = 0; i < argv.length; i += 1) {
@@ -120,6 +122,16 @@ function parsePositiveInt(value, fallback) {
120
122
  }
121
123
  return parsed;
122
124
  }
125
+ function parseMaxIterations(value, fallback) {
126
+ if (!value) {
127
+ return fallback;
128
+ }
129
+ const normalized = value.trim().toLowerCase();
130
+ if (UNBOUNDED_ITERATION_ALIASES.has(normalized)) {
131
+ return 0;
132
+ }
133
+ return parsePositiveInt(value, fallback);
134
+ }
123
135
  function parseRoles(value, fallback) {
124
136
  if (!value) {
125
137
  return fallback;
@@ -257,8 +269,8 @@ async function main() {
257
269
  const parsedArgs = parseArgs(process.argv.slice(2));
258
270
  const goal = (parsedArgs.goal ?? env.RLM_GOAL)?.trim();
259
271
  const roles = parseRoles(parsedArgs.roles ?? env.RLM_ROLES, 'single');
260
- const maxIterations = parsePositiveInt(parsedArgs.maxIterations ?? env.RLM_MAX_ITERATIONS, DEFAULT_MAX_ITERATIONS);
261
- const maxMinutes = parsePositiveInt(parsedArgs.maxMinutes ?? env.RLM_MAX_MINUTES, 0);
272
+ const maxIterations = parseMaxIterations(parsedArgs.maxIterations ?? env.RLM_MAX_ITERATIONS, DEFAULT_MAX_ITERATIONS);
273
+ const maxMinutes = parsePositiveInt(parsedArgs.maxMinutes ?? env.RLM_MAX_MINUTES, DEFAULT_MAX_MINUTES);
262
274
  if (!goal) {
263
275
  const state = {
264
276
  goal: '',
@@ -300,7 +312,7 @@ async function main() {
300
312
  final: { status: 'invalid_config', exitCode: 5 }
301
313
  };
302
314
  await writeTerminalState(runDir, state);
303
- console.error('Invalid max iterations value.');
315
+ console.error('Invalid max iterations value. Use a non-negative integer or one of "unlimited", "unbounded", "infinite", "infinity".');
304
316
  process.exitCode = 5;
305
317
  return;
306
318
  }
@@ -405,6 +417,11 @@ async function main() {
405
417
  const finalStatus = result.state.final?.status ?? 'unknown';
406
418
  const iterationCount = result.state.iterations.length;
407
419
  console.log(`RLM completed: status=${finalStatus} iterations=${iterationCount} exit=${result.exitCode}`);
420
+ const hasTimeCap = maxMinutes !== null && maxMinutes > 0;
421
+ const unboundedBudgetInvalid = validatorCommand === null && maxIterations === 0 && !hasTimeCap;
422
+ if (finalStatus === 'invalid_config' && unboundedBudgetInvalid) {
423
+ console.error('Invalid configuration: --validator none with unbounded iterations and --max-minutes 0 would run forever. Fix: set --max-minutes / RLM_MAX_MINUTES to a positive value (default 2880), set --max-iterations to a positive value, or provide a validator.');
424
+ }
408
425
  process.exitCode = result.exitCode;
409
426
  }
410
427
  const entry = process.argv[1] ? resolve(process.argv[1]) : null;
@@ -415,3 +432,9 @@ if (entry && entry === self) {
415
432
  process.exitCode = 10;
416
433
  });
417
434
  }
435
+ export const __test__ = {
436
+ parseMaxIterations,
437
+ parsePositiveInt,
438
+ DEFAULT_MAX_ITERATIONS,
439
+ DEFAULT_MAX_MINUTES
440
+ };
@@ -21,6 +21,7 @@ export class ManifestPersister {
21
21
  this.writeHeartbeat = options.writeHeartbeat ?? writeHeartbeatFile;
22
22
  }
23
23
  schedule(options = {}) {
24
+ this.pendingPersist = this.pendingPersist.catch(() => undefined);
24
25
  const { manifest: includeManifest = false, heartbeat: includeHeartbeat = false, force = false } = options;
25
26
  this.dirtyManifest = this.dirtyManifest || includeManifest;
26
27
  this.dirtyHeartbeat = this.dirtyHeartbeat || includeHeartbeat;
@@ -70,13 +71,42 @@ export class ManifestPersister {
70
71
  const writeHeartbeat = this.dirtyHeartbeat;
71
72
  this.dirtyManifest = false;
72
73
  this.dirtyHeartbeat = false;
73
- this.lastPersistAt = this.now();
74
+ const tasks = [];
74
75
  if (writeManifest) {
75
- await this.writeManifest(this.paths, this.manifest);
76
+ tasks.push({
77
+ kind: 'manifest',
78
+ promise: Promise.resolve().then(() => this.writeManifest(this.paths, this.manifest))
79
+ });
76
80
  }
77
81
  if (writeHeartbeat) {
78
- await this.writeHeartbeat(this.paths, this.manifest);
82
+ tasks.push({
83
+ kind: 'heartbeat',
84
+ promise: Promise.resolve().then(() => this.writeHeartbeat(this.paths, this.manifest))
85
+ });
86
+ }
87
+ const results = await Promise.allSettled(tasks.map((task) => task.promise));
88
+ let manifestError = null;
89
+ let heartbeatError = null;
90
+ let manifestFailed = false;
91
+ let heartbeatFailed = false;
92
+ results.forEach((result, index) => {
93
+ if (result.status === 'rejected') {
94
+ if (tasks[index].kind === 'manifest') {
95
+ this.dirtyManifest = true;
96
+ manifestFailed = true;
97
+ manifestError = result.reason;
98
+ }
99
+ else {
100
+ this.dirtyHeartbeat = true;
101
+ heartbeatFailed = true;
102
+ heartbeatError = result.reason;
103
+ }
104
+ }
105
+ });
106
+ if (manifestFailed || heartbeatFailed) {
107
+ throw manifestFailed ? manifestError : heartbeatError;
79
108
  }
109
+ this.lastPersistAt = this.now();
80
110
  }
81
111
  }
82
112
  export async function persistManifest(paths, manifest, persister, options = {}) {
@@ -7,6 +7,13 @@ export function resolveRunPaths(env, runId) {
7
7
  const heartbeatPath = join(runDir, '.heartbeat');
8
8
  const resumeTokenPath = join(runDir, '.resume-token');
9
9
  const logPath = join(runDir, 'runner.ndjson');
10
+ const eventsPath = join(runDir, 'events.jsonl');
11
+ const controlPath = join(runDir, 'control.json');
12
+ const controlAuthPath = join(runDir, 'control_auth.json');
13
+ const controlEndpointPath = join(runDir, 'control_endpoint.json');
14
+ const confirmationsPath = join(runDir, 'confirmations.json');
15
+ const questionsPath = join(runDir, 'questions.json');
16
+ const delegationTokensPath = join(runDir, 'delegation_tokens.json');
10
17
  const commandsDir = join(runDir, 'commands');
11
18
  const errorsDir = join(runDir, 'errors');
12
19
  const compatDir = join(env.runsRoot, env.taskId, 'mcp', safeRunId);
@@ -18,6 +25,13 @@ export function resolveRunPaths(env, runId) {
18
25
  heartbeatPath,
19
26
  resumeTokenPath,
20
27
  logPath,
28
+ eventsPath,
29
+ controlPath,
30
+ controlAuthPath,
31
+ controlEndpointPath,
32
+ confirmationsPath,
33
+ questionsPath,
34
+ delegationTokensPath,
21
35
  commandsDir,
22
36
  errorsDir,
23
37
  compatDir,
@@ -12,7 +12,7 @@ import { EnvUtils } from '../../../../packages/shared/config/index.js';
12
12
  import { findPackageRoot } from '../utils/packageInfo.js';
13
13
  const MAX_BUFFERED_OUTPUT_BYTES = 64 * 1024;
14
14
  const EMIT_COMMAND_STREAM_MIRRORS = EnvUtils.getBoolean('CODEX_ORCHESTRATOR_EMIT_COMMAND_STREAMS', false);
15
- const MAX_CAPTURED_CHUNK_EVENTS = EnvUtils.getInt('CODEX_ORCHESTRATOR_EXEC_EVENT_MAX_CHUNKS', 0);
15
+ export const MAX_CAPTURED_CHUNK_EVENTS = EnvUtils.getInt('CODEX_ORCHESTRATOR_EXEC_EVENT_MAX_CHUNKS', 500);
16
16
  const PACKAGE_ROOT = findPackageRoot();
17
17
  export async function runCommandStage(context, hooks = {}) {
18
18
  const { env, paths, manifest, stage, index, events, persister, envOverrides } = context;
@@ -5,6 +5,7 @@ import process from 'node:process';
5
5
  import { EnvUtils } from '../../../../packages/shared/config/env.js';
6
6
  export const DEVTOOLS_SKILL_NAME = 'chrome-devtools';
7
7
  export const DEVTOOLS_CONFIG_OVERRIDE = 'mcp_servers.chrome-devtools.enabled=true';
8
+ const CONFIG_OVERRIDE_ENV_KEYS = ['CODEX_MCP_CONFIG_OVERRIDES', 'CODEX_CONFIG_OVERRIDES'];
8
9
  const DEVTOOLS_CONFIG_FILENAME = 'config.toml';
9
10
  const DEVTOOLS_MCP_COMMAND = [
10
11
  'mcp',
@@ -86,16 +87,18 @@ export function buildDevtoolsSetupPlan(env = process.env) {
86
87
  };
87
88
  }
88
89
  export function resolveCodexCommand(args, env = process.env) {
90
+ const overrides = parseConfigOverrides(env);
89
91
  if (!isDevtoolsEnabled(env)) {
90
- return { command: 'codex', args };
92
+ return { command: 'codex', args: applyConfigOverrides(overrides, args) };
91
93
  }
92
94
  const readiness = resolveDevtoolsReadiness(env);
93
95
  if (readiness.status !== 'ok') {
94
96
  throw new Error(formatDevtoolsPreflightError(readiness));
95
97
  }
98
+ const mergedOverrides = dedupeOverrides([DEVTOOLS_CONFIG_OVERRIDE, ...overrides]);
96
99
  return {
97
100
  command: 'codex',
98
- args: ['-c', DEVTOOLS_CONFIG_OVERRIDE, ...args]
101
+ args: applyConfigOverrides(mergedOverrides, args)
99
102
  };
100
103
  }
101
104
  export function formatDevtoolsPreflightError(readiness) {
@@ -187,6 +190,34 @@ function hasDevtoolsConfigEntry(raw) {
187
190
  }
188
191
  return false;
189
192
  }
193
+ function parseConfigOverrides(env) {
194
+ const overrides = [];
195
+ for (const key of CONFIG_OVERRIDE_ENV_KEYS) {
196
+ const raw = env[key];
197
+ if (!raw) {
198
+ continue;
199
+ }
200
+ const parts = raw
201
+ .split(/[,;\n]/)
202
+ .map((part) => part.trim())
203
+ .filter((part) => part.length > 0);
204
+ overrides.push(...parts);
205
+ }
206
+ return dedupeOverrides(overrides);
207
+ }
208
+ function applyConfigOverrides(overrides, args) {
209
+ if (overrides.length === 0) {
210
+ return args;
211
+ }
212
+ const configArgs = [];
213
+ for (const override of overrides) {
214
+ configArgs.push('-c', override);
215
+ }
216
+ return [...configArgs, ...args];
217
+ }
218
+ function dedupeOverrides(overrides) {
219
+ return Array.from(new Set(overrides.filter((override) => override.trim().length > 0)));
220
+ }
190
221
  function stripTomlComment(line) {
191
222
  const index = line.indexOf('#');
192
223
  if (index === -1) {