@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
@@ -0,0 +1,246 @@
1
+ import { createWriteStream } from 'node:fs';
2
+ import { mkdir, readFile } from 'node:fs/promises';
3
+ import { isoTimestamp } from '../utils/time.js';
4
+ const SCHEMA_VERSION = 1;
5
+ export class RunEventStream {
6
+ seq;
7
+ stream;
8
+ now;
9
+ taskId;
10
+ runId;
11
+ writeQueue = Promise.resolve();
12
+ streamError = null;
13
+ constructor(stream, options) {
14
+ this.stream = stream;
15
+ this.taskId = options.taskId;
16
+ this.runId = options.runId;
17
+ this.now = options.now;
18
+ this.seq = options.initialSeq;
19
+ this.stream.on('error', (error) => {
20
+ this.streamError = error;
21
+ });
22
+ }
23
+ static async create(options) {
24
+ await mkdir(options.paths.runDir, { recursive: true });
25
+ const initialSeq = await readLastSeq(options.paths.eventsPath);
26
+ const stream = createWriteStream(options.paths.eventsPath, { flags: 'a' });
27
+ return new RunEventStream(stream, {
28
+ taskId: options.taskId,
29
+ runId: options.runId,
30
+ now: options.now ?? isoTimestamp,
31
+ initialSeq
32
+ });
33
+ }
34
+ async append(input) {
35
+ if (this.streamError) {
36
+ throw this.streamError;
37
+ }
38
+ const entry = {
39
+ schema_version: SCHEMA_VERSION,
40
+ seq: this.nextSeq(),
41
+ timestamp: input.timestamp ?? this.now(),
42
+ task_id: this.taskId,
43
+ run_id: this.runId,
44
+ event: input.event,
45
+ actor: input.actor ?? 'runner',
46
+ payload: input.payload ?? null
47
+ };
48
+ const line = `${JSON.stringify(entry)}\n`;
49
+ this.writeQueue = this.writeQueue
50
+ .then(() => new Promise((resolve, reject) => {
51
+ this.stream.write(line, (error) => {
52
+ if (error) {
53
+ reject(error);
54
+ return;
55
+ }
56
+ resolve();
57
+ });
58
+ }))
59
+ .catch((error) => {
60
+ this.streamError = error;
61
+ throw error;
62
+ });
63
+ await this.writeQueue;
64
+ if (this.streamError) {
65
+ throw this.streamError;
66
+ }
67
+ return entry;
68
+ }
69
+ async close() {
70
+ try {
71
+ await this.writeQueue;
72
+ }
73
+ catch {
74
+ // Allow close to proceed even if a prior write failed.
75
+ }
76
+ if (this.streamError) {
77
+ this.stream.destroy();
78
+ return;
79
+ }
80
+ await new Promise((resolve, reject) => {
81
+ const onFinish = () => {
82
+ cleanup();
83
+ resolve();
84
+ };
85
+ const onError = (error) => {
86
+ cleanup();
87
+ this.stream.destroy();
88
+ reject(error);
89
+ };
90
+ const cleanup = () => {
91
+ this.stream.off('finish', onFinish);
92
+ this.stream.off('error', onError);
93
+ };
94
+ this.stream.once('finish', onFinish);
95
+ this.stream.once('error', onError);
96
+ this.stream.end();
97
+ });
98
+ }
99
+ nextSeq() {
100
+ this.seq += 1;
101
+ return this.seq;
102
+ }
103
+ }
104
+ export function attachRunEventAdapter(emitter, stream, onEntry, onError) {
105
+ return emitter.on('*', (event) => {
106
+ const mapped = mapRunEvent(event);
107
+ if (!mapped) {
108
+ return;
109
+ }
110
+ void stream
111
+ .append(mapped)
112
+ .then((entry) => {
113
+ onEntry?.(entry);
114
+ })
115
+ .catch((error) => {
116
+ onError?.(error, mapped);
117
+ });
118
+ });
119
+ }
120
+ function mapRunEvent(event) {
121
+ // The public event stream uses "step_*" naming for pipeline stages to match UI terminology.
122
+ switch (event.type) {
123
+ case 'run:started':
124
+ return {
125
+ event: 'run_started',
126
+ actor: 'runner',
127
+ payload: {
128
+ pipeline_id: event.pipelineId,
129
+ pipeline_title: event.pipelineTitle,
130
+ status: event.status,
131
+ manifest_path: event.manifestPath,
132
+ log_path: event.logPath
133
+ }
134
+ };
135
+ case 'stage:started':
136
+ return {
137
+ event: 'step_started',
138
+ actor: 'runner',
139
+ payload: {
140
+ stage_id: event.stageId,
141
+ stage_index: event.stageIndex,
142
+ title: event.title,
143
+ kind: event.kind,
144
+ status: event.status,
145
+ log_path: event.logPath
146
+ }
147
+ };
148
+ case 'stage:completed':
149
+ return {
150
+ event: event.status === 'failed' ? 'step_failed' : 'step_completed',
151
+ actor: 'runner',
152
+ payload: {
153
+ stage_id: event.stageId,
154
+ stage_index: event.stageIndex,
155
+ title: event.title,
156
+ kind: event.kind,
157
+ status: event.status,
158
+ exit_code: event.exitCode,
159
+ summary: event.summary,
160
+ log_path: event.logPath,
161
+ sub_run_id: event.subRunId ?? null
162
+ }
163
+ };
164
+ case 'run:completed':
165
+ return {
166
+ event: 'run_completed',
167
+ actor: 'runner',
168
+ payload: {
169
+ pipeline_id: event.pipelineId,
170
+ status: event.status,
171
+ manifest_path: event.manifestPath,
172
+ run_summary_path: event.runSummaryPath,
173
+ metrics_path: event.metricsPath,
174
+ summary: event.summary
175
+ }
176
+ };
177
+ case 'run:error':
178
+ return {
179
+ event: 'run_failed',
180
+ actor: 'runner',
181
+ payload: {
182
+ pipeline_id: event.pipelineId,
183
+ message: event.message,
184
+ stage_id: event.stageId ?? null
185
+ }
186
+ };
187
+ case 'log':
188
+ return {
189
+ event: 'agent_message',
190
+ actor: 'runner',
191
+ payload: {
192
+ message: event.message,
193
+ level: event.level,
194
+ stage_id: event.stageId ?? null,
195
+ stage_index: event.stageIndex ?? null,
196
+ source: event.source ?? null
197
+ }
198
+ };
199
+ case 'tool:call':
200
+ return {
201
+ event: 'tool_called',
202
+ actor: 'runner',
203
+ payload: {
204
+ tool_name: event.toolName,
205
+ status: event.status,
206
+ message: event.message ?? null,
207
+ attempt: event.attempt ?? null,
208
+ stage_id: event.stageId ?? null,
209
+ stage_index: event.stageIndex ?? null
210
+ }
211
+ };
212
+ default:
213
+ return null;
214
+ }
215
+ }
216
+ async function readLastSeq(pathname) {
217
+ try {
218
+ const raw = await readFile(pathname, 'utf8');
219
+ if (!raw.trim()) {
220
+ return 0;
221
+ }
222
+ const lines = raw.split('\n');
223
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
224
+ const line = lines[index]?.trim();
225
+ if (!line) {
226
+ continue;
227
+ }
228
+ try {
229
+ const parsed = JSON.parse(line);
230
+ if (typeof parsed.seq === 'number' && Number.isFinite(parsed.seq)) {
231
+ return parsed.seq;
232
+ }
233
+ }
234
+ catch {
235
+ continue;
236
+ }
237
+ }
238
+ return 0;
239
+ }
240
+ catch (error) {
241
+ if (error.code === 'ENOENT') {
242
+ return 0;
243
+ }
244
+ throw error;
245
+ }
246
+ }
@@ -2,9 +2,11 @@ import process from 'node:process';
2
2
  import { bootstrapManifest } from '../run/manifest.js';
3
3
  import { generateRunId } from '../utils/runId.js';
4
4
  import { ExperienceStore } from '../../persistence/ExperienceStore.js';
5
+ import { EnvUtils } from '../../../../packages/shared/config/index.js';
5
6
  import { createTelemetrySink } from '../../../../packages/orchestrator/src/telemetry/otel-exporter.js';
6
7
  import { createNotificationSink } from '../../../../packages/orchestrator/src/notifications/index.js';
7
8
  import { ManifestPersister, persistManifest } from '../run/manifestPersister.js';
9
+ const MAX_TELEMETRY_EVENTS = EnvUtils.getInt('CODEX_ORCHESTRATOR_TELEMETRY_MAX_EVENTS', 1000);
8
10
  export async function bootstrapExecContext(context, invocation) {
9
11
  const argv = [invocation.command, ...(invocation.args ?? [])];
10
12
  const shellCommand = buildShellCommand(argv);
@@ -38,7 +40,8 @@ export async function bootstrapExecContext(context, invocation) {
38
40
  await persistManifest(paths, manifest, persister, { force: true });
39
41
  const telemetrySink = context.telemetrySink ?? createTelemetrySink({
40
42
  endpoint: invocation.otelEndpoint,
41
- enabled: Boolean(invocation.otelEndpoint)
43
+ enabled: Boolean(invocation.otelEndpoint),
44
+ maxQueueSize: MAX_TELEMETRY_EVENTS
42
45
  });
43
46
  const envNotifications = parseNotificationEnv(process.env.CODEX_ORCHESTRATOR_NOTIFY);
44
47
  const notificationSink = context.notificationSink ??
@@ -1,20 +1,45 @@
1
- import { runCommandStage } from '../services/commandRunner.js';
1
+ import { runCommandStage, MAX_CAPTURED_CHUNK_EVENTS } from '../services/commandRunner.js';
2
2
  import { serializeExecEvent } from '../../../../packages/shared/events/serializer.js';
3
3
  import { sanitizeToolRunRecord } from '../../../../packages/shared/manifest/writer.js';
4
4
  export async function runExecStage(context) {
5
5
  let runResultSummary = null;
6
6
  let toolRecord = null;
7
+ const maxChunkEvents = MAX_CAPTURED_CHUNK_EVENTS;
8
+ let capturedChunkEvents = 0;
9
+ const shouldCaptureEvent = (event) => {
10
+ if (maxChunkEvents <= 0) {
11
+ return true;
12
+ }
13
+ if (event.type !== 'exec:chunk') {
14
+ return true;
15
+ }
16
+ if (capturedChunkEvents >= maxChunkEvents) {
17
+ return false;
18
+ }
19
+ capturedChunkEvents += 1;
20
+ return true;
21
+ };
7
22
  const hooks = {
8
23
  onEvent: (event) => {
9
- context.execEvents.push(event);
10
- const serialized = serializeExecEvent(event);
11
- context.telemetryTasks.push(Promise.resolve(context.telemetrySink.record(serialized)).then(() => undefined));
24
+ const captureEvent = shouldCaptureEvent(event);
25
+ let serialized = null;
26
+ const getSerialized = () => {
27
+ if (!serialized) {
28
+ serialized = serializeExecEvent(event);
29
+ }
30
+ return serialized;
31
+ };
12
32
  if (context.outputMode === 'jsonl' && context.jsonlWriter) {
13
- context.jsonlWriter(serialized);
33
+ context.jsonlWriter(getSerialized());
14
34
  }
15
35
  else if (context.outputMode === 'interactive') {
16
36
  streamInteractive(context.stdout, context.stderr, event);
17
37
  }
38
+ if (captureEvent) {
39
+ context.execEvents.push(event);
40
+ const payload = getSerialized();
41
+ context.telemetryTasks.push(Promise.resolve(context.telemetrySink.record(payload)).then(() => undefined));
42
+ }
18
43
  },
19
44
  onResult: (result) => {
20
45
  runResultSummary = {