@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
@@ -1,10 +1,11 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { readFile, mkdir, rm } from 'node:fs/promises';
2
+ import { createReadStream } from 'node:fs';
3
+ import { appendFile, mkdir, open, rm } from 'node:fs/promises';
3
4
  import { join } from 'node:path';
5
+ import { createInterface } from 'node:readline';
4
6
  import { listDirectories, resolveEnvironmentPaths } from '../../../scripts/lib/run-manifests.js';
5
7
  import { acquireLockWithRetry } from './lockFile.js';
6
8
  import { sanitizeTaskId } from './sanitizeTaskId.js';
7
- import { writeAtomicFile } from '../utils/atomicWrite.js';
8
9
  export class ExperienceStoreLockError extends Error {
9
10
  taskId;
10
11
  constructor(message, taskId) {
@@ -30,7 +31,8 @@ export class ExperienceStore {
30
31
  maxAttempts: 5,
31
32
  initialDelayMs: 100,
32
33
  backoffFactor: 2,
33
- maxDelayMs: 1000
34
+ maxDelayMs: 1000,
35
+ staleMs: 5 * 60 * 1000
34
36
  };
35
37
  const overrides = options.lockRetry ?? {};
36
38
  const sanitizedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== undefined));
@@ -52,10 +54,10 @@ export class ExperienceStore {
52
54
  const targetDir = join(this.outDir, taskId);
53
55
  await mkdir(targetDir, { recursive: true });
54
56
  const filePath = join(targetDir, 'experiences.jsonl');
55
- const existing = await this.readRecords(filePath);
56
57
  const nextRecords = inputs.map((input) => this.prepareRecord(input, manifestPath));
57
- const serialized = [...existing, ...nextRecords].map((record) => JSON.stringify(record)).join('\n');
58
- await writeAtomicFile(filePath, `${serialized}\n`);
58
+ await this.ensureTrailingNewline(filePath);
59
+ const payload = nextRecords.map((record) => JSON.stringify(record)).join('\n');
60
+ await appendFile(filePath, `${payload}\n`, 'utf8');
59
61
  return nextRecords;
60
62
  }
61
63
  finally {
@@ -72,36 +74,26 @@ export class ExperienceStore {
72
74
  if (limit === 0) {
73
75
  return [];
74
76
  }
75
- const allRecords = taskFilter
76
- ? await this.readRecords(join(this.outDir, taskFilter, 'experiences.jsonl'))
77
- : await this.readAllRecords();
78
- const filtered = allRecords.filter((record) => {
77
+ const collector = createTopKCollector(limit, params.minReward);
78
+ const applyRecord = (record) => {
79
79
  if (record.domain !== safeDomain) {
80
- return false;
80
+ return;
81
81
  }
82
82
  if (taskFilter && record.taskId !== taskFilter) {
83
- return false;
83
+ return;
84
84
  }
85
- return true;
86
- });
87
- const scored = filtered
88
- .map((record) => ({
89
- record,
90
- score: record.reward.gtScore + record.reward.relativeRank
91
- }))
92
- .filter((entry) => {
93
- if (params.minReward === undefined) {
94
- return true;
95
- }
96
- return entry.score >= params.minReward;
97
- })
98
- .sort((a, b) => {
99
- if (b.score === a.score) {
100
- return b.record.createdAt.localeCompare(a.record.createdAt);
85
+ collector.add(record);
86
+ };
87
+ if (taskFilter) {
88
+ await this.scanRecords(join(this.outDir, taskFilter, 'experiences.jsonl'), applyRecord);
89
+ }
90
+ else {
91
+ const directories = await listDirectories(this.outDir);
92
+ for (const dir of directories) {
93
+ await this.scanRecords(join(this.outDir, dir, 'experiences.jsonl'), applyRecord);
101
94
  }
102
- return b.score - a.score;
103
- });
104
- return scored.slice(0, limit).map((entry) => entry.record);
95
+ }
96
+ return collector.finalize();
105
97
  }
106
98
  verifyStamp(record) {
107
99
  return HEX_STAMP_PATTERN.test(record.stampSignature);
@@ -156,35 +148,110 @@ export class ExperienceStore {
156
148
  async releaseLock(lockPath) {
157
149
  await rm(lockPath, { force: true });
158
150
  }
159
- async readRecords(filePath) {
151
+ async scanRecords(filePath, onRecord) {
160
152
  try {
161
- const raw = await readFile(filePath, 'utf8');
162
- return raw
163
- .split('\n')
164
- .filter(Boolean)
165
- .map((line) => JSON.parse(line));
153
+ const stream = createReadStream(filePath, { encoding: 'utf8' });
154
+ const reader = createInterface({ input: stream, crlfDelay: Infinity });
155
+ for await (const line of reader) {
156
+ const trimmed = line.trim();
157
+ if (!trimmed) {
158
+ continue;
159
+ }
160
+ try {
161
+ const record = JSON.parse(trimmed);
162
+ onRecord(record);
163
+ }
164
+ catch {
165
+ continue;
166
+ }
167
+ }
166
168
  }
167
169
  catch (error) {
168
170
  if (error.code === 'ENOENT') {
169
- return [];
171
+ return;
170
172
  }
171
173
  throw error;
172
174
  }
173
175
  }
174
- async readAllRecords() {
175
- const directories = await listDirectories(this.outDir);
176
- const all = [];
177
- for (const dir of directories) {
178
- const filePath = join(this.outDir, dir, 'experiences.jsonl');
179
- all.push(...(await this.readRecords(filePath)));
180
- }
181
- return all;
176
+ async ensureTrailingNewline(filePath) {
177
+ try {
178
+ const handle = await open(filePath, 'r');
179
+ let needsNewline = false;
180
+ try {
181
+ const { size } = await handle.stat();
182
+ if (size === 0) {
183
+ return;
184
+ }
185
+ const buffer = Buffer.alloc(1);
186
+ await handle.read(buffer, 0, 1, size - 1);
187
+ needsNewline = buffer[0] !== 0x0a;
188
+ }
189
+ finally {
190
+ await handle.close();
191
+ }
192
+ if (needsNewline) {
193
+ await appendFile(filePath, '\n', 'utf8');
194
+ }
195
+ }
196
+ catch (error) {
197
+ if (error.code === 'ENOENT') {
198
+ return;
199
+ }
200
+ throw error;
201
+ }
182
202
  }
183
203
  generateId() {
184
204
  const suffix = randomBytes(3).toString('hex');
185
205
  return `exp-${Date.now().toString(36)}-${suffix}`;
186
206
  }
187
207
  }
208
+ function createTopKCollector(limit, minReward) {
209
+ const entries = [];
210
+ const threshold = typeof minReward === 'number' ? minReward : null;
211
+ const compare = (a, b) => {
212
+ if (a.score !== b.score) {
213
+ return a.score - b.score;
214
+ }
215
+ const aTime = a.record.createdAt ?? '';
216
+ const bTime = b.record.createdAt ?? '';
217
+ return aTime.localeCompare(bTime);
218
+ };
219
+ const add = (record) => {
220
+ const score = record.reward.gtScore + record.reward.relativeRank;
221
+ if (threshold !== null && score < threshold) {
222
+ return;
223
+ }
224
+ const entry = { record, score };
225
+ if (entries.length === 0) {
226
+ entries.push(entry);
227
+ return;
228
+ }
229
+ const worst = entries[0];
230
+ if (entries.length >= limit && worst && compare(entry, worst) <= 0) {
231
+ return;
232
+ }
233
+ let index = 0;
234
+ while (index < entries.length && compare(entries[index], entry) <= 0) {
235
+ index += 1;
236
+ }
237
+ entries.splice(index, 0, entry);
238
+ if (entries.length > limit) {
239
+ entries.shift();
240
+ }
241
+ };
242
+ const finalize = () => entries
243
+ .slice()
244
+ .sort((a, b) => {
245
+ if (a.score !== b.score) {
246
+ return b.score - a.score;
247
+ }
248
+ const aTime = a.record.createdAt ?? '';
249
+ const bTime = b.record.createdAt ?? '';
250
+ return bTime.localeCompare(aTime);
251
+ })
252
+ .map((entry) => entry.record);
253
+ return { add, finalize };
254
+ }
188
255
  function truncateSummary(value, maxWords) {
189
256
  const tokens = value.trim().split(/\s+/u).filter(Boolean);
190
257
  if (tokens.length <= maxWords) {
@@ -33,10 +33,12 @@ export class PersistenceCoordinator {
33
33
  }
34
34
  async handleRunCompleted(summary) {
35
35
  let stateStoreError = null;
36
- try {
37
- await this.stateStore.recordRun(summary);
38
- }
39
- catch (error) {
36
+ const [stateResult, manifestResult] = await Promise.allSettled([
37
+ this.stateStore.recordRun(summary),
38
+ this.manifestWriter.write(summary)
39
+ ]);
40
+ if (stateResult.status === 'rejected') {
41
+ const error = stateResult.reason;
40
42
  stateStoreError = error;
41
43
  if (error instanceof TaskStateStoreLockError) {
42
44
  logger.warn(`Task state snapshot skipped for task ${summary.taskId} (run ${summary.runId}): ${error.message}`);
@@ -45,10 +47,8 @@ export class PersistenceCoordinator {
45
47
  logger.error(`Task state snapshot failed for task ${summary.taskId} (run ${summary.runId})`, error);
46
48
  }
47
49
  }
48
- try {
49
- await this.manifestWriter.write(summary);
50
- }
51
- catch (error) {
50
+ if (manifestResult.status === 'rejected') {
51
+ const error = manifestResult.reason;
52
52
  this.options.onError?.(error, summary);
53
53
  if (!this.options.onError) {
54
54
  logger.error(`PersistenceCoordinator manifest write error for task ${summary.taskId} (run ${summary.runId})`, error);
@@ -27,7 +27,8 @@ export class TaskStateStore {
27
27
  maxAttempts: 5,
28
28
  initialDelayMs: 100,
29
29
  backoffFactor: 2,
30
- maxDelayMs: 1000
30
+ maxDelayMs: 1000,
31
+ staleMs: 5 * 60 * 1000
31
32
  };
32
33
  const overrides = options.lockRetry ?? {};
33
34
  const sanitizedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== undefined));
@@ -1,8 +1,9 @@
1
- import { open } from 'node:fs/promises';
1
+ import { open, rm, stat } from 'node:fs/promises';
2
2
  import { setTimeout as delay } from 'node:timers/promises';
3
3
  export async function acquireLockWithRetry(params) {
4
4
  await params.ensureDirectory();
5
5
  const { maxAttempts, initialDelayMs, backoffFactor, maxDelayMs } = params.retry;
6
+ const staleMs = params.retry.staleMs ?? 0;
6
7
  let attempt = 0;
7
8
  let delayMs = initialDelayMs;
8
9
  while (attempt < maxAttempts) {
@@ -16,6 +17,13 @@ export async function acquireLockWithRetry(params) {
16
17
  if (error.code !== 'EEXIST') {
17
18
  throw error;
18
19
  }
20
+ if (staleMs > 0) {
21
+ const cleared = await clearStaleLock(params.lockPath, staleMs);
22
+ if (cleared) {
23
+ attempt -= 1;
24
+ continue;
25
+ }
26
+ }
19
27
  if (attempt >= maxAttempts) {
20
28
  throw params.createError(params.taskId, attempt);
21
29
  }
@@ -24,3 +32,20 @@ export async function acquireLockWithRetry(params) {
24
32
  }
25
33
  }
26
34
  }
35
+ async function clearStaleLock(lockPath, staleMs) {
36
+ try {
37
+ const stats = await stat(lockPath);
38
+ const ageMs = Date.now() - stats.mtimeMs;
39
+ if (!Number.isFinite(ageMs) || ageMs <= staleMs) {
40
+ return false;
41
+ }
42
+ await rm(lockPath, { force: true });
43
+ return true;
44
+ }
45
+ catch (error) {
46
+ if (error.code === 'ENOENT') {
47
+ return false;
48
+ }
49
+ return false;
50
+ }
51
+ }
@@ -52,7 +52,7 @@ export class CloudSyncWorker {
52
52
  }
53
53
  catch (error) {
54
54
  this.options.onError?.(error, summary, 0);
55
- await this.appendAuditLog({
55
+ await this.safeAppendAuditLog({
56
56
  level: 'error',
57
57
  message: 'Failed to read manifest before sync',
58
58
  summary,
@@ -74,7 +74,7 @@ export class CloudSyncWorker {
74
74
  idempotencyKey
75
75
  });
76
76
  this.options.onSuccess?.(result, summary);
77
- await this.appendAuditLog({
77
+ await this.safeAppendAuditLog({
78
78
  level: 'info',
79
79
  message: 'Cloud sync completed',
80
80
  summary,
@@ -84,7 +84,7 @@ export class CloudSyncWorker {
84
84
  }
85
85
  catch (error) {
86
86
  this.options.onError?.(error, summary, attempt);
87
- await this.appendAuditLog({
87
+ await this.safeAppendAuditLog({
88
88
  level: 'error',
89
89
  message: 'Cloud sync attempt failed',
90
90
  summary,
@@ -120,6 +120,19 @@ export class CloudSyncWorker {
120
120
  });
121
121
  await appendFile(logPath, `${line}\n`, 'utf-8');
122
122
  }
123
+ async safeAppendAuditLog(entry) {
124
+ try {
125
+ await this.appendAuditLog(entry);
126
+ }
127
+ catch (error) {
128
+ try {
129
+ this.options.onError?.(error, entry.summary, 0);
130
+ }
131
+ catch {
132
+ // swallow audit log failures
133
+ }
134
+ }
135
+ }
123
136
  shouldRetry(error) {
124
137
  if (this.options.retryDecider) {
125
138
  return this.options.retryDecider(error);
@@ -160,7 +173,7 @@ export class CloudSyncWorker {
160
173
  if (repaired) {
161
174
  try {
162
175
  const parsed = JSON.parse(repaired);
163
- await this.appendAuditLog({
176
+ await this.safeAppendAuditLog({
164
177
  level: 'info',
165
178
  message: 'Recovered manifest from partial JSON',
166
179
  summary,
@@ -17,6 +17,7 @@ export function createTelemetrySink(options = {}) {
17
17
  maxFailures: options.maxFailures ?? DEFAULT_MAX_FAILURES,
18
18
  backoffMs: options.backoffMs ?? DEFAULT_BACKOFF_MS,
19
19
  maxBackoffMs: options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS,
20
+ maxQueueSize: options.maxQueueSize,
20
21
  fetch: fetchImpl,
21
22
  logger: options.logger ?? console
22
23
  });
@@ -43,6 +44,7 @@ class OtelTelemetrySink {
43
44
  maxFailures;
44
45
  backoffMs;
45
46
  maxBackoffMs;
47
+ maxQueueSize;
46
48
  queue = [];
47
49
  disabled = false;
48
50
  consecutiveFailures = 0;
@@ -54,12 +56,14 @@ class OtelTelemetrySink {
54
56
  this.maxFailures = config.maxFailures;
55
57
  this.backoffMs = config.backoffMs;
56
58
  this.maxBackoffMs = config.maxBackoffMs;
59
+ this.maxQueueSize = normalizeQueueSize(config.maxQueueSize);
57
60
  }
58
61
  async record(event) {
59
62
  if (this.disabled) {
60
63
  return;
61
64
  }
62
65
  this.queue.push(event);
66
+ this.trimQueue();
63
67
  }
64
68
  async recordSummary(summary) {
65
69
  if (this.disabled) {
@@ -86,6 +90,7 @@ class OtelTelemetrySink {
86
90
  }
87
91
  catch (error) {
88
92
  this.queue.unshift(...batch);
93
+ this.trimQueue();
89
94
  this.handleFailure(error);
90
95
  }
91
96
  }
@@ -113,6 +118,12 @@ class OtelTelemetrySink {
113
118
  this.logger.warn(`Telemetry disabled after ${this.consecutiveFailures} consecutive failures: ${String(error)}`);
114
119
  }
115
120
  }
121
+ trimQueue() {
122
+ if (this.maxQueueSize === null || this.queue.length <= this.maxQueueSize) {
123
+ return;
124
+ }
125
+ this.queue.splice(0, this.queue.length - this.maxQueueSize);
126
+ }
116
127
  }
117
128
  function buildSummaryDimensions(metrics) {
118
129
  if (!metrics) {
@@ -140,3 +151,13 @@ async function wait(ms) {
140
151
  setTimeout(resolve, ms);
141
152
  });
142
153
  }
154
+ function normalizeQueueSize(value) {
155
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
156
+ return null;
157
+ }
158
+ const normalized = Math.floor(value);
159
+ if (normalized <= 0) {
160
+ return null;
161
+ }
162
+ return normalized;
163
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbediako/codex-orchestrator",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -100,7 +100,9 @@
100
100
  "esbuild": "^0.25.11"
101
101
  },
102
102
  "dependencies": {
103
+ "@iarna/toml": "^2.2.5",
103
104
  "ajv": "^8.17.1",
105
+ "canonicalize": "^2.1.0",
104
106
  "ink": "^4.4.1",
105
107
  "js-yaml": "^4.1.0",
106
108
  "react": "^18.3.1"