claude2codex 0.1.0 → 0.3.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/PATTERNS.md +118 -0
- package/PATTERNS.zh-CN.md +117 -0
- package/README.md +153 -43
- package/README.zh-CN.md +151 -41
- package/dist/server.js +545 -58
- package/package.json +6 -3
package/dist/server.js
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
// src/server.ts
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { stat } from "fs/promises";
|
|
7
|
+
import { resolve } from "path";
|
|
6
8
|
import { z } from "zod";
|
|
7
9
|
|
|
8
10
|
// src/contracts.ts
|
|
11
|
+
import { statSync } from "fs";
|
|
9
12
|
import { readFile } from "fs/promises";
|
|
10
13
|
import { join } from "path";
|
|
11
14
|
var PROTOCOL_INSTRUCTIONS = `## Protocol Instructions
|
|
@@ -47,6 +50,19 @@ async function optionalAgentsInstructions(cwd) {
|
|
|
47
50
|
throw error;
|
|
48
51
|
}
|
|
49
52
|
}
|
|
53
|
+
function renderContextFiles(contextFiles2) {
|
|
54
|
+
if (!contextFiles2?.length) return [];
|
|
55
|
+
return [
|
|
56
|
+
"### Context Files",
|
|
57
|
+
"Read these files before implementing. They are reference material, not necessarily files to modify.",
|
|
58
|
+
...contextFiles2.map((contextFile) => {
|
|
59
|
+
const directory = statSync(contextFile.path).isDirectory() ? " (directory)" : "";
|
|
60
|
+
const note = contextFile.note ? ` \u2014 ${contextFile.note}` : "";
|
|
61
|
+
return `- ${contextFile.path}${directory}${note}`;
|
|
62
|
+
}),
|
|
63
|
+
""
|
|
64
|
+
];
|
|
65
|
+
}
|
|
50
66
|
async function renderGoalContract(c, cwd = process.cwd()) {
|
|
51
67
|
const agents = await optionalAgentsInstructions(cwd);
|
|
52
68
|
const lines = [
|
|
@@ -58,6 +74,7 @@ async function renderGoalContract(c, cwd = process.cwd()) {
|
|
|
58
74
|
"### Goal",
|
|
59
75
|
c.goal.trim(),
|
|
60
76
|
"",
|
|
77
|
+
...renderContextFiles(c.context_files),
|
|
61
78
|
"### Constraints",
|
|
62
79
|
...c.constraints.map((x) => `- ${x}`),
|
|
63
80
|
"",
|
|
@@ -75,6 +92,7 @@ function renderDeltaContract(d) {
|
|
|
75
92
|
"### Findings",
|
|
76
93
|
...d.findings.map((x) => `- ${x}`),
|
|
77
94
|
"",
|
|
95
|
+
...renderContextFiles(d.context_files),
|
|
78
96
|
"### Failed Success Conditions",
|
|
79
97
|
...d.failed_conditions.map((x) => `- [ ] ${x}`),
|
|
80
98
|
"",
|
|
@@ -126,6 +144,7 @@ var CodexAppServer = class {
|
|
|
126
144
|
pending = /* @__PURE__ */ new Map();
|
|
127
145
|
opts;
|
|
128
146
|
stderrTail = [];
|
|
147
|
+
exitError;
|
|
129
148
|
exited = false;
|
|
130
149
|
constructor(opts) {
|
|
131
150
|
this.opts = opts;
|
|
@@ -142,24 +161,26 @@ var CodexAppServer = class {
|
|
|
142
161
|
if (this.stderrTail.length > 50) this.stderrTail.shift();
|
|
143
162
|
opts.onLog?.(line);
|
|
144
163
|
});
|
|
145
|
-
this.proc.on("
|
|
146
|
-
|
|
147
|
-
for (const [, p] of this.pending) {
|
|
148
|
-
clearTimeout(p.timer);
|
|
149
|
-
p.reject(new Error(`codex app-server exited (code ${code}). stderr: ${this.stderrTail.slice(-10).join(" | ")}`));
|
|
150
|
-
}
|
|
151
|
-
this.pending.clear();
|
|
152
|
-
opts.onExit?.(code);
|
|
153
|
-
});
|
|
164
|
+
this.proc.on("spawn", () => opts.onSpawn?.(this.proc.pid));
|
|
165
|
+
this.proc.on("exit", (code) => this.handleExit(code));
|
|
154
166
|
this.proc.on("error", (err) => {
|
|
155
|
-
this.
|
|
156
|
-
for (const [, p] of this.pending) {
|
|
157
|
-
clearTimeout(p.timer);
|
|
158
|
-
p.reject(new Error(`failed to spawn ${opts.bin}: ${err.message}`));
|
|
159
|
-
}
|
|
160
|
-
this.pending.clear();
|
|
167
|
+
this.handleExit(null, new Error(`failed to spawn ${opts.bin}: ${err.message}`));
|
|
161
168
|
});
|
|
162
169
|
}
|
|
170
|
+
handleExit(code, error) {
|
|
171
|
+
if (this.exited) return;
|
|
172
|
+
this.exited = true;
|
|
173
|
+
const stderr = this.stderrTail.slice(-10).join(" | ");
|
|
174
|
+
this.exitError = error ?? new Error(
|
|
175
|
+
`codex app-server exited (code ${code})${stderr ? `. stderr: ${stderr}` : ""}`
|
|
176
|
+
);
|
|
177
|
+
for (const [, p] of this.pending) {
|
|
178
|
+
clearTimeout(p.timer);
|
|
179
|
+
p.reject(this.exitError);
|
|
180
|
+
}
|
|
181
|
+
this.pending.clear();
|
|
182
|
+
this.opts.onExit?.(code, this.exitError);
|
|
183
|
+
}
|
|
163
184
|
handleLine(line) {
|
|
164
185
|
const trimmed = line.trim();
|
|
165
186
|
if (!trimmed) return;
|
|
@@ -192,13 +213,16 @@ var CodexAppServer = class {
|
|
|
192
213
|
this.proc.stdin.write(JSON.stringify(obj) + "\n");
|
|
193
214
|
}
|
|
194
215
|
request(method, params, timeoutMs = 6e4) {
|
|
216
|
+
if (this.exited) {
|
|
217
|
+
return Promise.reject(this.exitError ?? new Error("codex app-server is not running"));
|
|
218
|
+
}
|
|
195
219
|
const id = this.nextId++;
|
|
196
|
-
return new Promise((
|
|
220
|
+
return new Promise((resolve2, reject) => {
|
|
197
221
|
const timer = setTimeout(() => {
|
|
198
222
|
this.pending.delete(id);
|
|
199
223
|
reject(new Error(`request ${method} timed out after ${timeoutMs}ms`));
|
|
200
224
|
}, timeoutMs);
|
|
201
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
225
|
+
this.pending.set(id, { resolve: resolve2, reject, timer });
|
|
202
226
|
this.send({ id, method, ...params !== void 0 ? { params } : {} });
|
|
203
227
|
});
|
|
204
228
|
}
|
|
@@ -207,7 +231,7 @@ var CodexAppServer = class {
|
|
|
207
231
|
}
|
|
208
232
|
async initialize() {
|
|
209
233
|
const result = await this.request("initialize", {
|
|
210
|
-
clientInfo: { name: "claude2codex", title: "Claude\u2194Codex Contract Bridge", version: "0.
|
|
234
|
+
clientInfo: { name: "claude2codex", title: "Claude\u2194Codex Contract Bridge", version: "0.3.0" },
|
|
211
235
|
capabilities: { experimentalApi: true }
|
|
212
236
|
});
|
|
213
237
|
this.notify("initialized", {});
|
|
@@ -218,25 +242,178 @@ var CodexAppServer = class {
|
|
|
218
242
|
}
|
|
219
243
|
};
|
|
220
244
|
|
|
245
|
+
// src/log.ts
|
|
246
|
+
var priorities = { error: 1, info: 2, debug: 3 };
|
|
247
|
+
function configuredLevel() {
|
|
248
|
+
const value = process.env.C2C_LOG_LEVEL;
|
|
249
|
+
return value === "silent" || value === "error" || value === "info" || value === "debug" ? value : "info";
|
|
250
|
+
}
|
|
251
|
+
function writeLog(level, event, fields = {}) {
|
|
252
|
+
const configured = configuredLevel();
|
|
253
|
+
if (configured === "silent" || priorities[level] > priorities[configured]) return;
|
|
254
|
+
process.stderr.write(`${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), level, event, ...fields })}
|
|
255
|
+
`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/store.ts
|
|
259
|
+
import { accessSync, constants, mkdirSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
260
|
+
import { homedir } from "os";
|
|
261
|
+
import { join as join2 } from "path";
|
|
262
|
+
var MAX_JOBS = 50;
|
|
263
|
+
var warned = false;
|
|
264
|
+
function warnOnce(error) {
|
|
265
|
+
if (warned) return;
|
|
266
|
+
warned = true;
|
|
267
|
+
writeLog("error", "persistence_degraded", {
|
|
268
|
+
error: error instanceof Error ? error.message : String(error)
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function defaultStateDir() {
|
|
272
|
+
return process.env.C2C_STATE_DIR ?? join2(homedir(), ".claude2codex", "jobs");
|
|
273
|
+
}
|
|
274
|
+
function createJobStore(dir = defaultStateDir()) {
|
|
275
|
+
const records = /* @__PURE__ */ new Map();
|
|
276
|
+
let enabled = true;
|
|
277
|
+
try {
|
|
278
|
+
mkdirSync(dir, { recursive: true });
|
|
279
|
+
accessSync(dir, constants.R_OK | constants.W_OK);
|
|
280
|
+
const interrupted = [];
|
|
281
|
+
for (const name of readdirSync(dir)) {
|
|
282
|
+
if (!name.endsWith(".json")) continue;
|
|
283
|
+
try {
|
|
284
|
+
const job = JSON.parse(readFileSync(join2(dir, name), "utf8"));
|
|
285
|
+
if (!job?.id || !job?.startedAt || !Array.isArray(job.transcript)) continue;
|
|
286
|
+
if (job.state === "blocked" || job.state === "queued" || job.state === "starting" || job.state === "running") {
|
|
287
|
+
job.state = "error";
|
|
288
|
+
job.error = "interrupted by server restart";
|
|
289
|
+
job.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
290
|
+
interrupted.push(job);
|
|
291
|
+
}
|
|
292
|
+
records.set(job.id, job);
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const job of interrupted) atomicWrite(dir, job);
|
|
297
|
+
prune(dir, records);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
enabled = false;
|
|
300
|
+
warnOnce(error);
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
getJob: (id) => records.get(id),
|
|
304
|
+
listJobs: () => [...records.values()],
|
|
305
|
+
save(job) {
|
|
306
|
+
const record = { ...job, transcript: job.transcript.slice(-200) };
|
|
307
|
+
records.set(job.id, record);
|
|
308
|
+
if (!enabled) return;
|
|
309
|
+
try {
|
|
310
|
+
atomicWrite(dir, record);
|
|
311
|
+
prune(dir, records);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
enabled = false;
|
|
314
|
+
warnOnce(error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function atomicWrite(dir, job) {
|
|
320
|
+
const target = join2(dir, `${job.id}.json`);
|
|
321
|
+
const temp = join2(dir, `.${job.id}.${process.pid}.${Date.now()}.tmp`);
|
|
322
|
+
writeFileSync(temp, JSON.stringify(job), { encoding: "utf8", mode: 384 });
|
|
323
|
+
renameSync(temp, target);
|
|
324
|
+
}
|
|
325
|
+
function prune(dir, records) {
|
|
326
|
+
const entries = [...records.values()].sort(
|
|
327
|
+
(a, b) => Date.parse(b.endedAt ?? b.startedAt) - Date.parse(a.endedAt ?? a.startedAt)
|
|
328
|
+
);
|
|
329
|
+
for (const job of entries.slice(MAX_JOBS)) {
|
|
330
|
+
records.delete(job.id);
|
|
331
|
+
try {
|
|
332
|
+
unlinkSync(join2(dir, `${job.id}.json`));
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (error?.code !== "ENOENT") throw error;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
221
339
|
// src/jobs.ts
|
|
222
|
-
var
|
|
340
|
+
var store = createJobStore();
|
|
341
|
+
var jobs = new Map(store.listJobs().map((job) => [job.id, job]));
|
|
223
342
|
function getJob(id) {
|
|
224
|
-
return jobs.get(id);
|
|
343
|
+
return jobs.get(id) ?? store.getJob(id);
|
|
344
|
+
}
|
|
345
|
+
function listJobs() {
|
|
346
|
+
return [...jobs.values()];
|
|
347
|
+
}
|
|
348
|
+
var queue = [];
|
|
349
|
+
var blocked = /* @__PURE__ */ new Map();
|
|
350
|
+
function getQueuePosition(id) {
|
|
351
|
+
const index = queue.findIndex((entry) => entry.job.id === id);
|
|
352
|
+
return index === -1 ? void 0 : index + 1;
|
|
353
|
+
}
|
|
354
|
+
function activeJobCount() {
|
|
355
|
+
return [...jobs.values()].filter((job) => job.state === "starting" || job.state === "running").length;
|
|
356
|
+
}
|
|
357
|
+
function launch(job, opts) {
|
|
358
|
+
void runJob(job, opts).catch((err) => {
|
|
359
|
+
finish(job, "error", void 0, String(err?.message ?? err));
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
function drainQueue() {
|
|
363
|
+
while (queue.length > 0 && activeJobCount() < queue[0].opts.config.maxConcurrent) {
|
|
364
|
+
const { job, opts } = queue.shift();
|
|
365
|
+
job.state = "starting";
|
|
366
|
+
log(job, "queue", "dequeued; starting job");
|
|
367
|
+
store.save(job);
|
|
368
|
+
writeLog("info", "job_dequeued", { job_id: job.id, kind: job.kind });
|
|
369
|
+
opts.onProgress?.({ jobId: job.id, event: "dequeued", message: "job dequeued; starting" });
|
|
370
|
+
launch(job, opts);
|
|
371
|
+
}
|
|
225
372
|
}
|
|
226
373
|
function startJob(opts) {
|
|
374
|
+
const dependency = opts.dependsOn ? getJob(opts.dependsOn) : void 0;
|
|
375
|
+
if (opts.dependsOn && !dependency) throw new Error(`unknown depends_on job_id ${opts.dependsOn}`);
|
|
376
|
+
if (dependency?.state === "error" || dependency?.state === "timeout") {
|
|
377
|
+
throw new Error(dependencyFailureMessage(dependency));
|
|
378
|
+
}
|
|
379
|
+
const isBlocked = dependency !== void 0 && dependency.state !== "done";
|
|
380
|
+
const queued = !isBlocked && (queue.length > 0 || activeJobCount() >= opts.config.maxConcurrent);
|
|
381
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
227
382
|
const job = {
|
|
228
383
|
id: randomUUID().slice(0, 8),
|
|
229
384
|
kind: opts.kind,
|
|
230
|
-
state: "starting",
|
|
385
|
+
state: isBlocked ? "blocked" : queued ? "queued" : "starting",
|
|
231
386
|
goalSet: false,
|
|
387
|
+
usage: {},
|
|
388
|
+
attempts: 1,
|
|
232
389
|
turns: 0,
|
|
233
390
|
transcript: [],
|
|
234
|
-
|
|
391
|
+
...opts.contextFiles ? { contextFiles: opts.contextFiles } : {},
|
|
392
|
+
...opts.dependsOn ? { dependsOn: opts.dependsOn } : {},
|
|
393
|
+
startedAt,
|
|
394
|
+
lastActivityAt: startedAt
|
|
235
395
|
};
|
|
236
396
|
jobs.set(job.id, job);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
397
|
+
if (isBlocked) {
|
|
398
|
+
const dependents = blocked.get(opts.dependsOn) ?? [];
|
|
399
|
+
dependents.push({ job, opts });
|
|
400
|
+
blocked.set(opts.dependsOn, dependents);
|
|
401
|
+
log(job, "blocked", `blocked on dependency ${opts.dependsOn}`);
|
|
402
|
+
} else if (queued) {
|
|
403
|
+
queue.push({ job, opts });
|
|
404
|
+
log(job, "queue", `enqueued at position ${queue.length}`);
|
|
405
|
+
}
|
|
406
|
+
store.save(job);
|
|
407
|
+
writeLog("info", "job_created", { job_id: job.id, kind: job.kind, state: job.state });
|
|
408
|
+
if (isBlocked) {
|
|
409
|
+
writeLog("info", "job_blocked", { job_id: job.id, kind: job.kind, depends_on: opts.dependsOn });
|
|
410
|
+
opts.onProgress?.({ jobId: job.id, event: "blocked", message: `blocked on dependency ${opts.dependsOn}` });
|
|
411
|
+
} else if (queued) {
|
|
412
|
+
writeLog("info", "job_enqueued", { job_id: job.id, kind: job.kind, queue_position: queue.length });
|
|
413
|
+
opts.onProgress?.({ jobId: job.id, event: "queued", message: `job queued at position ${queue.length}` });
|
|
414
|
+
} else {
|
|
415
|
+
launch(job, opts);
|
|
416
|
+
}
|
|
240
417
|
return job;
|
|
241
418
|
}
|
|
242
419
|
function log(job, kind, detail) {
|
|
@@ -245,11 +422,73 @@ function log(job, kind, detail) {
|
|
|
245
422
|
}
|
|
246
423
|
function finish(job, state, client, error) {
|
|
247
424
|
if (job.state === "done" || job.state === "error" || job.state === "timeout") return;
|
|
425
|
+
const previousState = job.state;
|
|
248
426
|
job.state = state;
|
|
249
427
|
job.error = error;
|
|
250
428
|
job.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
251
429
|
if (job.finalMessage) job.handoff = parseHandoff(job.finalMessage);
|
|
430
|
+
store.save(job);
|
|
431
|
+
writeLog(state === "error" || state === "timeout" ? "error" : "info", "job_state_changed", {
|
|
432
|
+
job_id: job.id,
|
|
433
|
+
from: previousState,
|
|
434
|
+
to: state,
|
|
435
|
+
...error ? { error } : {}
|
|
436
|
+
});
|
|
437
|
+
writeLog("info", "job_usage", {
|
|
438
|
+
job_id: job.id,
|
|
439
|
+
state,
|
|
440
|
+
usage: job.usage ?? {}
|
|
441
|
+
});
|
|
252
442
|
client?.kill();
|
|
443
|
+
resolveDependents(job);
|
|
444
|
+
drainQueue();
|
|
445
|
+
}
|
|
446
|
+
function dependencyFailureMessage(dependency) {
|
|
447
|
+
return `dependency ${dependency.id} failed: ${dependency.error ?? `state ${dependency.state}`}`;
|
|
448
|
+
}
|
|
449
|
+
function resolveDependents(dependency) {
|
|
450
|
+
const dependents = blocked.get(dependency.id);
|
|
451
|
+
if (!dependents?.length) return;
|
|
452
|
+
blocked.delete(dependency.id);
|
|
453
|
+
for (const { job, opts } of dependents) {
|
|
454
|
+
if (dependency.state === "done") {
|
|
455
|
+
log(job, "unblocked", `dependency ${dependency.id} completed; unblocked`);
|
|
456
|
+
store.save(job);
|
|
457
|
+
writeLog("info", "job_unblocked", { job_id: job.id, depends_on: dependency.id });
|
|
458
|
+
opts.onProgress?.({
|
|
459
|
+
jobId: job.id,
|
|
460
|
+
event: "unblocked",
|
|
461
|
+
message: `dependency ${dependency.id} completed; job unblocked`
|
|
462
|
+
});
|
|
463
|
+
enqueueOrLaunch(job, opts);
|
|
464
|
+
} else {
|
|
465
|
+
const error = dependencyFailureMessage(dependency);
|
|
466
|
+
log(job, "dependency_failed", error);
|
|
467
|
+
store.save(job);
|
|
468
|
+
writeLog("info", "job_dependency_failed", {
|
|
469
|
+
job_id: job.id,
|
|
470
|
+
depends_on: dependency.id,
|
|
471
|
+
error
|
|
472
|
+
});
|
|
473
|
+
opts.onProgress?.({ jobId: job.id, event: "dependency_failed", message: error });
|
|
474
|
+
finish(job, "error", void 0, error);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function enqueueOrLaunch(job, opts) {
|
|
479
|
+
if (queue.length > 0 || activeJobCount() >= opts.config.maxConcurrent) {
|
|
480
|
+
job.state = "queued";
|
|
481
|
+
queue.push({ job, opts });
|
|
482
|
+
log(job, "queue", `enqueued at position ${queue.length} after dependency completed`);
|
|
483
|
+
store.save(job);
|
|
484
|
+
writeLog("info", "job_enqueued", { job_id: job.id, kind: job.kind, queue_position: queue.length });
|
|
485
|
+
opts.onProgress?.({ jobId: job.id, event: "queued", message: `job queued at position ${queue.length}` });
|
|
486
|
+
drainQueue();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
job.state = "starting";
|
|
490
|
+
store.save(job);
|
|
491
|
+
launch(job, opts);
|
|
253
492
|
}
|
|
254
493
|
function extractText(item) {
|
|
255
494
|
if (typeof item?.text === "string") return item.text;
|
|
@@ -270,23 +509,96 @@ function extractGoal(params) {
|
|
|
270
509
|
timeUsedSeconds: g.timeUsedSeconds ?? g.time_used_seconds
|
|
271
510
|
};
|
|
272
511
|
}
|
|
512
|
+
var usageAliases = {
|
|
513
|
+
input_tokens: "inputTokens",
|
|
514
|
+
input_token_count: "inputTokens",
|
|
515
|
+
output_tokens: "outputTokens",
|
|
516
|
+
output_token_count: "outputTokens",
|
|
517
|
+
total_tokens: "totalTokens",
|
|
518
|
+
total_token_count: "totalTokens",
|
|
519
|
+
tokensUsed: "totalTokens",
|
|
520
|
+
tokens_used: "totalTokens"
|
|
521
|
+
};
|
|
522
|
+
function extractUsage(params) {
|
|
523
|
+
const source = params?.turn?.usage ?? params?.usage ?? params?.turn?.tokenUsage ?? params?.tokenUsage;
|
|
524
|
+
if (!source || typeof source !== "object") return void 0;
|
|
525
|
+
const usage = {};
|
|
526
|
+
for (const [key, value] of Object.entries(source)) {
|
|
527
|
+
if (typeof value === "number" && Number.isFinite(value)) usage[usageAliases[key] ?? key] = value;
|
|
528
|
+
}
|
|
529
|
+
return Object.keys(usage).length ? usage : void 0;
|
|
530
|
+
}
|
|
531
|
+
function mergeUsage(job, usage, cumulative = false) {
|
|
532
|
+
if (!usage) return;
|
|
533
|
+
job.usage ??= {};
|
|
534
|
+
for (const [key, value] of Object.entries(usage)) {
|
|
535
|
+
job.usage[key] = cumulative ? value : (job.usage[key] ?? 0) + value;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function updateGoal(job, params) {
|
|
539
|
+
job.goal = extractGoal(params);
|
|
540
|
+
if (typeof job.goal.tokensUsed === "number") {
|
|
541
|
+
mergeUsage(job, { totalTokens: job.goal.tokensUsed }, true);
|
|
542
|
+
}
|
|
543
|
+
store.save(job);
|
|
544
|
+
}
|
|
273
545
|
var TERMINAL_GOAL_STATUSES = /* @__PURE__ */ new Set(["complete", "budget_limited", "budgetLimited"]);
|
|
274
546
|
async function runJob(job, opts) {
|
|
547
|
+
const deadline = Date.now() + opts.config.jobTimeoutMs;
|
|
548
|
+
const maxAttempts = (opts.config.retries ?? 1) + 1;
|
|
549
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
550
|
+
job.attempts = attempt;
|
|
551
|
+
log(job, "attempt", `attempt ${attempt} started`);
|
|
552
|
+
store.save(job);
|
|
553
|
+
writeLog("info", "job_attempt_started", { job_id: job.id, attempt });
|
|
554
|
+
const result = await runAttempt(job, opts, Math.max(0, deadline - Date.now()));
|
|
555
|
+
if (!result.error || job.state === "done" || job.state === "error" || job.state === "timeout") return;
|
|
556
|
+
const retryable = result.processFailure && job.state === "starting" && job.turns === 0;
|
|
557
|
+
if (!retryable || attempt === maxAttempts) {
|
|
558
|
+
finish(job, "error", void 0, result.error.message);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
log(job, "retry", `attempt ${attempt} failed before the first turn; retrying in 500ms`);
|
|
562
|
+
store.save(job);
|
|
563
|
+
writeLog("info", "job_retry_scheduled", {
|
|
564
|
+
job_id: job.id,
|
|
565
|
+
attempt,
|
|
566
|
+
next_attempt: attempt + 1,
|
|
567
|
+
delay_ms: 500,
|
|
568
|
+
error: result.error.message
|
|
569
|
+
});
|
|
570
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async function runAttempt(job, opts, timeoutMs) {
|
|
275
574
|
const cfg = opts.config;
|
|
276
575
|
let activeTurn = false;
|
|
576
|
+
let stalled = false;
|
|
577
|
+
let processError;
|
|
277
578
|
let quietTimer;
|
|
579
|
+
let stallTimer;
|
|
278
580
|
let settle;
|
|
279
581
|
const doneSignal = new Promise((res) => settle = res);
|
|
280
582
|
const progress = (event, message) => opts.onProgress?.({ jobId: job.id, event, message });
|
|
583
|
+
if (!opts.resumeThreadId) job.goalSet = false;
|
|
281
584
|
const client = new CodexAppServer({
|
|
282
585
|
bin: cfg.bin,
|
|
283
586
|
args: cfg.args,
|
|
284
587
|
cwd: cfg.cwd,
|
|
285
588
|
onLog: (line) => log(job, "log", line),
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
589
|
+
onSpawn: (pid) => writeLog("info", "codex_process_spawn", {
|
|
590
|
+
job_id: job.id,
|
|
591
|
+
attempt: job.attempts ?? 1,
|
|
592
|
+
...pid === void 0 ? {} : { pid }
|
|
593
|
+
}),
|
|
594
|
+
onExit: (code, error) => {
|
|
595
|
+
writeLog(job.state === "starting" || job.state === "running" ? "error" : "info", "codex_process_exit", {
|
|
596
|
+
job_id: job.id,
|
|
597
|
+
attempt: job.attempts ?? 1,
|
|
598
|
+
code,
|
|
599
|
+
error: error.message
|
|
600
|
+
});
|
|
601
|
+
if (job.state === "starting" || job.state === "running") processError = error;
|
|
290
602
|
settle();
|
|
291
603
|
},
|
|
292
604
|
onNotification: (method, params) => handleNotification(method, params)
|
|
@@ -295,18 +607,73 @@ async function runJob(job, opts) {
|
|
|
295
607
|
clearTimeout(quietTimer);
|
|
296
608
|
quietTimer = setTimeout(() => {
|
|
297
609
|
log(job, "quiet", `no goal continuation within ${cfg.quietMs}ms; treating turn as final`);
|
|
298
|
-
|
|
610
|
+
finalize("done");
|
|
299
611
|
settle();
|
|
300
612
|
}, cfg.quietMs);
|
|
301
613
|
}
|
|
614
|
+
function clearStallTimer() {
|
|
615
|
+
clearTimeout(stallTimer);
|
|
616
|
+
stallTimer = void 0;
|
|
617
|
+
}
|
|
618
|
+
function armStallTimer() {
|
|
619
|
+
clearStallTimer();
|
|
620
|
+
if (cfg.stallWarnMs === 0 || !activeTurn || stalled || !job.lastActivityAt) return;
|
|
621
|
+
const stalledForMs = Math.max(0, Date.now() - Date.parse(job.lastActivityAt));
|
|
622
|
+
stallTimer = setTimeout(checkForStall, Math.max(0, cfg.stallWarnMs - stalledForMs));
|
|
623
|
+
}
|
|
624
|
+
function checkForStall() {
|
|
625
|
+
stallTimer = void 0;
|
|
626
|
+
if (!activeTurn || stalled || !job.lastActivityAt) return;
|
|
627
|
+
const stalledForMs = Math.max(0, Date.now() - Date.parse(job.lastActivityAt));
|
|
628
|
+
if (stalledForMs < cfg.stallWarnMs) {
|
|
629
|
+
armStallTimer();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
stalled = true;
|
|
633
|
+
log(job, "stalled", `no Codex activity for ${stalledForMs}ms`);
|
|
634
|
+
store.save(job);
|
|
635
|
+
writeLog("info", "job_stalled", { job_id: job.id, stalled_for_ms: stalledForMs });
|
|
636
|
+
progress("stalled", `job stalled for ${stalledForMs}ms`);
|
|
637
|
+
}
|
|
638
|
+
function recordActivity() {
|
|
639
|
+
const now = /* @__PURE__ */ new Date();
|
|
640
|
+
if (stalled && job.lastActivityAt) {
|
|
641
|
+
const stalledForMs = Math.max(0, now.getTime() - Date.parse(job.lastActivityAt));
|
|
642
|
+
stalled = false;
|
|
643
|
+
log(job, "resumed", `Codex activity resumed after ${stalledForMs}ms`);
|
|
644
|
+
writeLog("info", "job_resumed", { job_id: job.id, stalled_for_ms: stalledForMs });
|
|
645
|
+
progress("resumed", `job resumed after ${stalledForMs}ms`);
|
|
646
|
+
}
|
|
647
|
+
job.lastActivityAt = now.toISOString();
|
|
648
|
+
store.save(job);
|
|
649
|
+
if (activeTurn) armStallTimer();
|
|
650
|
+
}
|
|
651
|
+
function finalize(state, error) {
|
|
652
|
+
clearStallTimer();
|
|
653
|
+
finish(job, state, client, error);
|
|
654
|
+
}
|
|
302
655
|
function handleNotification(method, params) {
|
|
656
|
+
writeLog("debug", "codex_notification", {
|
|
657
|
+
job_id: job.id,
|
|
658
|
+
attempt: job.attempts ?? 1,
|
|
659
|
+
method,
|
|
660
|
+
params: params ?? null
|
|
661
|
+
});
|
|
662
|
+
recordActivity();
|
|
303
663
|
const p = params ?? {};
|
|
304
664
|
switch (method) {
|
|
305
665
|
case "turn/started": {
|
|
306
666
|
activeTurn = true;
|
|
667
|
+
armStallTimer();
|
|
307
668
|
job.turns += 1;
|
|
308
669
|
clearTimeout(quietTimer);
|
|
670
|
+
if (job.state === "starting") {
|
|
671
|
+
job.state = "running";
|
|
672
|
+
store.save(job);
|
|
673
|
+
writeLog("info", "job_state_changed", { job_id: job.id, from: "starting", to: "running" });
|
|
674
|
+
}
|
|
309
675
|
log(job, "turn", `turn ${job.turns} started`);
|
|
676
|
+
writeLog("info", "turn_started", { job_id: job.id, turn: job.turns });
|
|
310
677
|
progress("turn_started", `turn ${job.turns} started`);
|
|
311
678
|
break;
|
|
312
679
|
}
|
|
@@ -328,11 +695,16 @@ async function runJob(job, opts) {
|
|
|
328
695
|
break;
|
|
329
696
|
}
|
|
330
697
|
case "thread/goal/updated": {
|
|
331
|
-
job
|
|
332
|
-
log(job, "goal", `status=${job.goal
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
698
|
+
updateGoal(job, p);
|
|
699
|
+
log(job, "goal", `status=${job.goal?.status} tokensUsed=${job.goal?.tokensUsed ?? "?"}`);
|
|
700
|
+
writeLog("info", "goal_updated", {
|
|
701
|
+
job_id: job.id,
|
|
702
|
+
status: job.goal?.status ?? null,
|
|
703
|
+
tokens_used: job.goal?.tokensUsed ?? null
|
|
704
|
+
});
|
|
705
|
+
progress("goal_updated", `goal status=${job.goal?.status ?? "unknown"}`);
|
|
706
|
+
if (job.goal?.status && TERMINAL_GOAL_STATUSES.has(job.goal.status) && !activeTurn) {
|
|
707
|
+
finalize("done");
|
|
336
708
|
settle();
|
|
337
709
|
}
|
|
338
710
|
break;
|
|
@@ -340,8 +712,12 @@ async function runJob(job, opts) {
|
|
|
340
712
|
case "turn/completed":
|
|
341
713
|
case "turn/failed": {
|
|
342
714
|
activeTurn = false;
|
|
715
|
+
clearStallTimer();
|
|
716
|
+
mergeUsage(job, extractUsage(p));
|
|
717
|
+
store.save(job);
|
|
343
718
|
const status = p.turn?.status ?? (method === "turn/failed" ? "failed" : "completed");
|
|
344
719
|
log(job, "turn", `turn ended (${status})`);
|
|
720
|
+
writeLog("info", "turn_ended", { job_id: job.id, turn: job.turns, status });
|
|
345
721
|
progress("turn_ended", `turn ended (${status})`);
|
|
346
722
|
void onTurnEnded();
|
|
347
723
|
break;
|
|
@@ -352,21 +728,21 @@ async function runJob(job, opts) {
|
|
|
352
728
|
}
|
|
353
729
|
async function onTurnEnded() {
|
|
354
730
|
if (!job.goalSet) {
|
|
355
|
-
|
|
731
|
+
finalize("done");
|
|
356
732
|
settle();
|
|
357
733
|
return;
|
|
358
734
|
}
|
|
359
735
|
if (job.goal?.status && TERMINAL_GOAL_STATUSES.has(job.goal.status)) {
|
|
360
|
-
|
|
736
|
+
finalize("done");
|
|
361
737
|
settle();
|
|
362
738
|
return;
|
|
363
739
|
}
|
|
364
740
|
try {
|
|
365
741
|
const res = await client.request("thread/goal/get", { threadId: job.threadId }, 15e3);
|
|
366
742
|
if (res?.goal) {
|
|
367
|
-
job
|
|
368
|
-
if (job.goal
|
|
369
|
-
|
|
743
|
+
updateGoal(job, res);
|
|
744
|
+
if (job.goal?.status && TERMINAL_GOAL_STATUSES.has(job.goal.status)) {
|
|
745
|
+
finalize("done");
|
|
370
746
|
settle();
|
|
371
747
|
return;
|
|
372
748
|
}
|
|
@@ -377,9 +753,9 @@ async function runJob(job, opts) {
|
|
|
377
753
|
if (!activeTurn) armQuietTimer();
|
|
378
754
|
}
|
|
379
755
|
const overallTimer = setTimeout(() => {
|
|
380
|
-
|
|
756
|
+
finalize("timeout", `job exceeded ${cfg.jobTimeoutMs}ms`);
|
|
381
757
|
settle();
|
|
382
|
-
},
|
|
758
|
+
}, timeoutMs);
|
|
383
759
|
try {
|
|
384
760
|
await client.initialize();
|
|
385
761
|
const threadParams = {
|
|
@@ -399,6 +775,7 @@ async function runJob(job, opts) {
|
|
|
399
775
|
job.threadId = threadRes?.thread?.id ?? threadRes?.id;
|
|
400
776
|
}
|
|
401
777
|
if (!job.threadId) throw new Error(`could not determine thread id from ${JSON.stringify(threadRes).slice(0, 300)}`);
|
|
778
|
+
store.save(job);
|
|
402
779
|
log(job, "thread", `thread ${job.threadId} ${opts.resumeThreadId ? "resumed" : "started"}`);
|
|
403
780
|
if (opts.objective) {
|
|
404
781
|
try {
|
|
@@ -408,24 +785,29 @@ async function runJob(job, opts) {
|
|
|
408
785
|
...opts.tokenBudget ? { tokenBudget: opts.tokenBudget } : {}
|
|
409
786
|
});
|
|
410
787
|
job.goalSet = true;
|
|
411
|
-
job
|
|
412
|
-
log(job, "goal", `goal set (status=${job.goal
|
|
413
|
-
progress("goal_updated", `goal status=${job.goal
|
|
788
|
+
updateGoal(job, goalRes);
|
|
789
|
+
log(job, "goal", `goal set (status=${job.goal?.status ?? "active"})`);
|
|
790
|
+
progress("goal_updated", `goal status=${job.goal?.status ?? "active"}`);
|
|
414
791
|
} catch (err) {
|
|
792
|
+
if (client.exited) throw err;
|
|
415
793
|
log(job, "goal", `thread/goal/set failed (${err?.message ?? err}); continuing without goal loop`);
|
|
416
794
|
job.goalSet = false;
|
|
417
795
|
}
|
|
418
796
|
}
|
|
419
|
-
job.state = "running";
|
|
420
797
|
await client.request("turn/start", {
|
|
421
798
|
threadId: job.threadId,
|
|
422
799
|
input: [{ type: "text", text: opts.prompt }],
|
|
423
800
|
...opts.reasoningEffort ? { effort: opts.reasoningEffort } : {}
|
|
424
801
|
});
|
|
425
802
|
await doneSignal;
|
|
803
|
+
return processError ? { error: processError, processFailure: true } : { processFailure: false };
|
|
804
|
+
} catch (error) {
|
|
805
|
+
const failure = processError ?? (error instanceof Error ? error : new Error(String(error)));
|
|
806
|
+
return { error: failure, processFailure: client.exited };
|
|
426
807
|
} finally {
|
|
427
808
|
clearTimeout(overallTimer);
|
|
428
809
|
clearTimeout(quietTimer);
|
|
810
|
+
clearStallTimer();
|
|
429
811
|
client.kill();
|
|
430
812
|
}
|
|
431
813
|
}
|
|
@@ -463,7 +845,10 @@ function config(cwd) {
|
|
|
463
845
|
approvalPolicy: process.env.CODEX_APPROVAL_POLICY ?? "never",
|
|
464
846
|
permissions: process.env.CODEX_PERMISSIONS || void 0,
|
|
465
847
|
jobTimeoutMs: envInt("CODEX_JOB_TIMEOUT_MS", 18e5),
|
|
466
|
-
quietMs: envInt("CODEX_QUIET_MS", 3e4)
|
|
848
|
+
quietMs: envInt("CODEX_QUIET_MS", 3e4),
|
|
849
|
+
stallWarnMs: Math.max(0, envInt("C2C_STALL_WARN_MS", 12e4)),
|
|
850
|
+
retries: Math.max(0, envInt("C2C_RETRIES", 1)),
|
|
851
|
+
maxConcurrent: Math.max(1, envInt("C2C_MAX_CONCURRENT", 2))
|
|
467
852
|
};
|
|
468
853
|
}
|
|
469
854
|
var OBJECTIVE_MAX = envInt("GOAL_OBJECTIVE_MAX", 2e3);
|
|
@@ -474,10 +859,17 @@ function jobSummary(job, transcriptTail = 15) {
|
|
|
474
859
|
state: job.state,
|
|
475
860
|
thread_id: job.threadId ?? null,
|
|
476
861
|
goal: job.goal ?? null,
|
|
862
|
+
usage: job.usage ?? {},
|
|
863
|
+
attempts: job.attempts ?? 1,
|
|
477
864
|
turns: job.turns,
|
|
478
865
|
started_at: job.startedAt,
|
|
479
866
|
ended_at: job.endedAt ?? null,
|
|
480
867
|
error: job.error ?? null,
|
|
868
|
+
...!["done", "error", "timeout"].includes(job.state) ? { last_activity_at: job.lastActivityAt ?? null } : {},
|
|
869
|
+
...job.state === "running" && job.lastActivityAt ? { seconds_since_activity: Math.max(0, (Date.now() - Date.parse(job.lastActivityAt)) / 1e3) } : {},
|
|
870
|
+
...job.contextFiles ? { context_files: job.contextFiles } : {},
|
|
871
|
+
...job.dependsOn ? { depends_on: job.dependsOn } : {},
|
|
872
|
+
...job.state === "queued" ? { queue_position: getQueuePosition(job.id) ?? null } : {},
|
|
481
873
|
transcript_tail: job.transcript.slice(-transcriptTail)
|
|
482
874
|
};
|
|
483
875
|
}
|
|
@@ -487,8 +879,53 @@ function textResult(obj, isError = false) {
|
|
|
487
879
|
isError
|
|
488
880
|
};
|
|
489
881
|
}
|
|
490
|
-
var server = new McpServer({ name: "c2c-codex", version: "0.
|
|
882
|
+
var server = new McpServer({ name: "c2c-codex", version: "0.3.0" });
|
|
491
883
|
var reasoningEffort = z.enum(["low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
884
|
+
var contextFiles = z.array(z.object({
|
|
885
|
+
path: z.string().min(1),
|
|
886
|
+
note: z.string().optional()
|
|
887
|
+
}));
|
|
888
|
+
async function resolveContextFiles(files, cwd) {
|
|
889
|
+
if (!files?.length) return { files: void 0, missing: [] };
|
|
890
|
+
const resolved = files.map((file) => ({ ...file, path: resolve(cwd, file.path) }));
|
|
891
|
+
const exists = await Promise.all(resolved.map(async (file) => {
|
|
892
|
+
try {
|
|
893
|
+
await stat(file.path);
|
|
894
|
+
return true;
|
|
895
|
+
} catch {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
}));
|
|
899
|
+
return {
|
|
900
|
+
files: resolved,
|
|
901
|
+
missing: resolved.filter((_, index) => !exists[index]).map((file) => file.path)
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function missingContextFiles(paths) {
|
|
905
|
+
return textResult({ error: `context_files not found:
|
|
906
|
+
${paths.map((path) => `- ${path}`).join("\n")}` }, true);
|
|
907
|
+
}
|
|
908
|
+
function implementHistory() {
|
|
909
|
+
const totals = listJobs().filter((job) => job.kind === "implement" && job.state === "done").map((job) => job.usage?.totalTokens).filter((total) => typeof total === "number" && Number.isFinite(total)).sort((a, b) => a - b);
|
|
910
|
+
if (!totals.length) return null;
|
|
911
|
+
const middle = Math.floor(totals.length / 2);
|
|
912
|
+
const median = totals.length % 2 ? totals[middle] : (totals[middle - 1] + totals[middle]) / 2;
|
|
913
|
+
return {
|
|
914
|
+
samples: totals.length,
|
|
915
|
+
median_total_tokens: Math.round(median),
|
|
916
|
+
p90_total_tokens: Math.round(totals[Math.ceil(totals.length * 0.9) - 1]),
|
|
917
|
+
mean_total_tokens: Math.round(totals.reduce((sum, total) => sum + total, 0) / totals.length)
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function dependencyValidationError(dependsOn) {
|
|
921
|
+
if (!dependsOn) return void 0;
|
|
922
|
+
const dependency = getJob(dependsOn);
|
|
923
|
+
if (!dependency) return `unknown depends_on job_id ${dependsOn}`;
|
|
924
|
+
if (dependency.state === "error" || dependency.state === "timeout") {
|
|
925
|
+
return `dependency ${dependency.id} failed: ${dependency.error ?? `state ${dependency.state}`}`;
|
|
926
|
+
}
|
|
927
|
+
return void 0;
|
|
928
|
+
}
|
|
492
929
|
function progressReporter() {
|
|
493
930
|
let sequence = 0;
|
|
494
931
|
return (event) => {
|
|
@@ -514,21 +951,31 @@ server.registerTool(
|
|
|
514
951
|
success_conditions: z.array(z.string()).min(1).describe("Checkable criteria proving the goal is met. At least one must be a runnable command/test."),
|
|
515
952
|
token_budget: z.number().int().positive().optional().describe("Optional token budget for the Codex goal loop (thread/goal/set tokenBudget)."),
|
|
516
953
|
cwd: z.string().optional().describe("Working directory for Codex. Defaults to the project directory."),
|
|
954
|
+
context_files: contextFiles.optional().describe("Files or directories Codex should read before implementing."),
|
|
955
|
+
depends_on: z.string().min(1).optional().describe("Job id of a prior job that must succeed before this job starts."),
|
|
517
956
|
reasoning_effort: reasoningEffort.optional().describe("Optional Codex reasoning effort; omitted uses the Codex default.")
|
|
518
957
|
}
|
|
519
958
|
},
|
|
520
959
|
async (input) => {
|
|
960
|
+
const jobConfig = config(input.cwd);
|
|
961
|
+
const resolvedContext = await resolveContextFiles(input.context_files, jobConfig.cwd);
|
|
962
|
+
if (resolvedContext.missing.length) return missingContextFiles(resolvedContext.missing);
|
|
521
963
|
const contract = {
|
|
522
964
|
goal: input.goal,
|
|
523
965
|
constraints: input.constraints,
|
|
524
|
-
success_conditions: input.success_conditions
|
|
966
|
+
success_conditions: input.success_conditions,
|
|
967
|
+
context_files: resolvedContext.files
|
|
525
968
|
};
|
|
526
|
-
const
|
|
969
|
+
const prompt = await renderGoalContract(contract, jobConfig.cwd);
|
|
970
|
+
const dependencyError = dependencyValidationError(input.depends_on);
|
|
971
|
+
if (dependencyError) return textResult({ error: dependencyError }, true);
|
|
527
972
|
const job = startJob({
|
|
528
973
|
kind: "implement",
|
|
529
|
-
prompt
|
|
974
|
+
prompt,
|
|
530
975
|
objective: renderObjective(contract, OBJECTIVE_MAX),
|
|
531
976
|
tokenBudget: input.token_budget,
|
|
977
|
+
contextFiles: resolvedContext.files?.map((file) => file.path),
|
|
978
|
+
dependsOn: input.depends_on,
|
|
532
979
|
config: jobConfig,
|
|
533
980
|
reasoningEffort: input.reasoning_effort,
|
|
534
981
|
onProgress: progressReporter()
|
|
@@ -536,7 +983,41 @@ server.registerTool(
|
|
|
536
983
|
return textResult({
|
|
537
984
|
job_id: job.id,
|
|
538
985
|
state: job.state,
|
|
539
|
-
note: "Job started. Poll codex_status until state is 'done', then call codex_result."
|
|
986
|
+
note: job.state === "blocked" ? `Job blocked on dependency ${job.dependsOn}. Poll codex_status for its state.` : job.state === "queued" ? "Job queued. Poll codex_status for its queue position and state." : "Job started. Poll codex_status until state is 'done', then call codex_result."
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
);
|
|
990
|
+
server.registerTool(
|
|
991
|
+
"codex_estimate",
|
|
992
|
+
{
|
|
993
|
+
title: "Estimate a Goal Contract's token cost",
|
|
994
|
+
description: "Read-only estimate made before starting a job. Measures the fully rendered Goal Contract prompt and returns approx_prompt_tokens using an approximate characters/4 heuristic, plus local completed-job history.",
|
|
995
|
+
inputSchema: {
|
|
996
|
+
goal: z.string().min(1).describe("What the code must do when this task is done."),
|
|
997
|
+
constraints: z.array(z.string()).describe("Technical boundaries: files to touch, patterns to follow, 'Do not modify' lists."),
|
|
998
|
+
success_conditions: z.array(z.string()).min(1).describe("Checkable criteria proving the goal is met. At least one must be a runnable command/test."),
|
|
999
|
+
cwd: z.string().optional().describe("Working directory used to render the prompt. Defaults to the project directory."),
|
|
1000
|
+
context_files: contextFiles.optional().describe("Files or directories included in the rendered prompt.")
|
|
1001
|
+
},
|
|
1002
|
+
annotations: { readOnlyHint: true }
|
|
1003
|
+
},
|
|
1004
|
+
async (input) => {
|
|
1005
|
+
const jobConfig = config(input.cwd);
|
|
1006
|
+
const resolvedContext = await resolveContextFiles(input.context_files, jobConfig.cwd);
|
|
1007
|
+
if (resolvedContext.missing.length) return missingContextFiles(resolvedContext.missing);
|
|
1008
|
+
const prompt = await renderGoalContract({
|
|
1009
|
+
goal: input.goal,
|
|
1010
|
+
constraints: input.constraints,
|
|
1011
|
+
success_conditions: input.success_conditions,
|
|
1012
|
+
context_files: resolvedContext.files
|
|
1013
|
+
}, jobConfig.cwd);
|
|
1014
|
+
const history = implementHistory();
|
|
1015
|
+
return textResult({
|
|
1016
|
+
prompt_chars: prompt.length,
|
|
1017
|
+
approx_prompt_tokens: Math.ceil(prompt.length / 4),
|
|
1018
|
+
history,
|
|
1019
|
+
estimated_total_tokens: history?.median_total_tokens ?? null,
|
|
1020
|
+
note: history ? "Estimated total tokens are based on local history and vary with task complexity." : "No completed implement job history is available; estimates based on local history vary with task complexity."
|
|
540
1021
|
});
|
|
541
1022
|
}
|
|
542
1023
|
);
|
|
@@ -552,10 +1033,14 @@ server.registerTool(
|
|
|
552
1033
|
failed_conditions: z.array(z.string()).describe("The specific Success Conditions from the original contract that did not pass."),
|
|
553
1034
|
constraints: z.array(z.string()).optional().describe("Additional constraints beyond the originals."),
|
|
554
1035
|
cwd: z.string().optional().describe("Working directory for Codex. Defaults to the project directory."),
|
|
1036
|
+
context_files: contextFiles.optional().describe("Files or directories Codex should read before implementing."),
|
|
555
1037
|
reasoning_effort: reasoningEffort.optional().describe("Optional Codex reasoning effort; omitted uses the Codex default.")
|
|
556
1038
|
}
|
|
557
1039
|
},
|
|
558
1040
|
async (input) => {
|
|
1041
|
+
const jobConfig = config(input.cwd);
|
|
1042
|
+
const resolvedContext = await resolveContextFiles(input.context_files, jobConfig.cwd);
|
|
1043
|
+
if (resolvedContext.missing.length) return missingContextFiles(resolvedContext.missing);
|
|
559
1044
|
let threadId = input.thread_id;
|
|
560
1045
|
if (!threadId && input.job_id) {
|
|
561
1046
|
const prev = getJob(input.job_id);
|
|
@@ -570,10 +1055,12 @@ server.registerTool(
|
|
|
570
1055
|
prompt: renderDeltaContract({
|
|
571
1056
|
findings: input.findings,
|
|
572
1057
|
failed_conditions: input.failed_conditions,
|
|
573
|
-
constraints: input.constraints
|
|
1058
|
+
constraints: input.constraints,
|
|
1059
|
+
context_files: resolvedContext.files
|
|
574
1060
|
}),
|
|
575
1061
|
resumeThreadId: threadId,
|
|
576
|
-
|
|
1062
|
+
contextFiles: resolvedContext.files?.map((file) => file.path),
|
|
1063
|
+
config: jobConfig,
|
|
577
1064
|
reasoningEffort: input.reasoning_effort,
|
|
578
1065
|
onProgress: progressReporter()
|
|
579
1066
|
});
|
|
@@ -581,7 +1068,7 @@ server.registerTool(
|
|
|
581
1068
|
job_id: job.id,
|
|
582
1069
|
resumed_thread_id: threadId,
|
|
583
1070
|
state: job.state,
|
|
584
|
-
note: "Rework job started. Poll codex_status until state is 'done', then call codex_result."
|
|
1071
|
+
note: job.state === "queued" ? "Rework job queued. Poll codex_status for its queue position and state." : "Rework job started. Poll codex_status until state is 'done', then call codex_result."
|
|
585
1072
|
});
|
|
586
1073
|
}
|
|
587
1074
|
);
|
|
@@ -601,7 +1088,7 @@ server.registerTool(
|
|
|
601
1088
|
"codex_status",
|
|
602
1089
|
{
|
|
603
1090
|
title: "Check a Codex job",
|
|
604
|
-
description: "Status of a running or finished Codex job: state, thread id, goal state (status/tokens), turn count, and a tail of the activity transcript.",
|
|
1091
|
+
description: "Status of a blocked, queued, running, or finished Codex job: dependency, state, queue position, thread id, goal state (status/tokens), turn count, and a tail of the activity transcript.",
|
|
605
1092
|
inputSchema: {
|
|
606
1093
|
job_id: z.string().describe("Job id returned by codex_implement or codex_rework.")
|
|
607
1094
|
}
|
|
@@ -616,7 +1103,7 @@ server.registerTool(
|
|
|
616
1103
|
"codex_result",
|
|
617
1104
|
{
|
|
618
1105
|
title: "Fetch a finished Codex job's handoff",
|
|
619
|
-
description: "Final result of a finished job: Codex's final message, the parsed structured handoff (Changed Files / Validation / Success Conditions / Risks & Deviations \u2014 a missing section is itself a review failure), and final goal state. Errors if the job is
|
|
1106
|
+
description: "Final result of a finished job: Codex's final message, the parsed structured handoff (Changed Files / Validation / Success Conditions / Risks & Deviations \u2014 a missing section is itself a review failure), and final goal state. Errors if the job is not finished.",
|
|
620
1107
|
inputSchema: {
|
|
621
1108
|
job_id: z.string().describe("Job id returned by codex_implement or codex_rework.")
|
|
622
1109
|
}
|
|
@@ -624,7 +1111,7 @@ server.registerTool(
|
|
|
624
1111
|
async (input) => {
|
|
625
1112
|
const job = getJob(input.job_id);
|
|
626
1113
|
if (!job) return textResult({ error: `unknown job_id ${input.job_id}` }, true);
|
|
627
|
-
if (job.state === "starting" || job.state === "running") {
|
|
1114
|
+
if (job.state === "blocked" || job.state === "queued" || job.state === "starting" || job.state === "running") {
|
|
628
1115
|
return textResult({ error: `job ${job.id} is still ${job.state}; poll codex_status` }, true);
|
|
629
1116
|
}
|
|
630
1117
|
return textResult({
|