@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.
- package/README.md +6 -1
- package/dist/bin/codex-orchestrator.js +38 -0
- package/dist/orchestrator/src/cli/config/delegationConfig.js +485 -0
- package/dist/orchestrator/src/cli/control/confirmations.js +262 -0
- package/dist/orchestrator/src/cli/control/controlServer.js +1476 -0
- package/dist/orchestrator/src/cli/control/controlState.js +46 -0
- package/dist/orchestrator/src/cli/control/controlWatcher.js +222 -0
- package/dist/orchestrator/src/cli/control/delegationTokens.js +62 -0
- package/dist/orchestrator/src/cli/control/questions.js +106 -0
- package/dist/orchestrator/src/cli/delegationServer.js +1368 -0
- package/dist/orchestrator/src/cli/events/runEventStream.js +246 -0
- package/dist/orchestrator/src/cli/exec/context.js +4 -1
- package/dist/orchestrator/src/cli/exec/stageRunner.js +30 -5
- package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +377 -147
- package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +3 -5
- package/dist/orchestrator/src/cli/orchestrator.js +217 -40
- package/dist/orchestrator/src/cli/rlmRunner.js +26 -3
- package/dist/orchestrator/src/cli/run/manifestPersister.js +33 -3
- package/dist/orchestrator/src/cli/run/runPaths.js +14 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
- package/dist/orchestrator/src/cli/utils/devtools.js +33 -2
- package/dist/orchestrator/src/persistence/ExperienceStore.js +113 -46
- package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +8 -8
- package/dist/orchestrator/src/persistence/TaskStateStore.js +2 -1
- package/dist/orchestrator/src/persistence/lockFile.js +26 -1
- package/dist/orchestrator/src/sync/CloudSyncWorker.js +17 -4
- package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +21 -0
- 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 {
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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 =
|
|
261
|
-
const maxMinutes = parsePositiveInt(parsedArgs.maxMinutes ?? env.RLM_MAX_MINUTES,
|
|
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
|
-
|
|
74
|
+
const tasks = [];
|
|
74
75
|
if (writeManifest) {
|
|
75
|
-
|
|
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
|
-
|
|
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',
|
|
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:
|
|
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) {
|