imprint-mcp 0.6.0 → 0.6.2
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 +2 -0
- package/package.json +4 -2
- package/prompts/audit-agent.md +4 -2
- package/prompts/auth-compile-agent.md +149 -158
- package/prompts/build-planning.md +14 -9
- package/prompts/compile-agent.md +46 -23
- package/prompts/live-verifier-agent.md +101 -0
- package/prompts/playbook-compilation.md +8 -1
- package/prompts/tool-candidate-detection.md +9 -29
- package/src/cli.ts +83 -1
- package/src/imprint/audit.ts +219 -20
- package/src/imprint/auth-compile-agent.ts +215 -155
- package/src/imprint/auth-compile-tools.ts +300 -118
- package/src/imprint/auth-verifier.ts +167 -180
- package/src/imprint/backend-cache.ts +18 -11
- package/src/imprint/backend-ladder.ts +225 -177
- package/src/imprint/build-plan.ts +3 -8
- package/src/imprint/cdp-browser-fetch.ts +930 -83
- package/src/imprint/claude-cli-compile.ts +46 -20
- package/src/imprint/codex-cli-compile.ts +436 -139
- package/src/imprint/compile-agent-types.ts +15 -10
- package/src/imprint/compile-agent.ts +88 -28
- package/src/imprint/compile-tools.ts +228 -39
- package/src/imprint/compile-verification.ts +187 -0
- package/src/imprint/compile.ts +71 -2
- package/src/imprint/credential-store.ts +57 -44
- package/src/imprint/emit.ts +3 -1
- package/src/imprint/freeform-redact.ts +27 -6
- package/src/imprint/live-verifier.ts +1665 -0
- package/src/imprint/llm.ts +22 -3
- package/src/imprint/login.ts +107 -25
- package/src/imprint/mcp-compile-server.ts +447 -114
- package/src/imprint/mcp-live-verifier-server.ts +359 -0
- package/src/imprint/mcp-server.ts +116 -35
- package/src/imprint/probe-backends.ts +88 -7
- package/src/imprint/recording-request.ts +36 -0
- package/src/imprint/request-capture.ts +40 -17
- package/src/imprint/runtime.ts +490 -337
- package/src/imprint/teach-state.ts +36 -2
- package/src/imprint/teach.ts +302 -146
- package/src/imprint/tool-candidates.ts +8 -10
- package/src/imprint/tool-loader.ts +12 -4
- package/src/imprint/types.ts +137 -68
|
@@ -7,17 +7,21 @@
|
|
|
7
7
|
* success only after the MCP done() tool writes the verified sentinel.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { type ChildProcess, spawn } from 'node:child_process';
|
|
10
|
+
import { type ChildProcess, spawn, spawnSync } from 'node:child_process';
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { isAbsolute as pathIsAbsolute, join as pathJoin } from 'node:path';
|
|
13
|
-
import {
|
|
13
|
+
import type { Span } from '@opentelemetry/api';
|
|
14
14
|
import type { AuthCliCompileMode } from './auth-compile-tools.ts';
|
|
15
15
|
import { type SharedModuleManifestEntry, resolvePlanSliceFromFile } from './build-plan.ts';
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
AuthCheckpoint,
|
|
18
|
+
CompileAgentProgress,
|
|
19
|
+
CompileAgentResult,
|
|
20
|
+
} from './compile-agent-types.ts';
|
|
17
21
|
import { formatCandidateContext, formatToolPlan } from './compile-agent-types.ts';
|
|
18
22
|
import { preferredAgentModel } from './llm.ts';
|
|
19
23
|
import { createLog } from './log.ts';
|
|
20
|
-
import { COMPILE_SENTINELS } from './mcp-compile-server.ts';
|
|
24
|
+
import { COMPILE_SENTINELS, compileDeadlineAfterVerification } from './mcp-compile-server.ts';
|
|
21
25
|
import type { SharedCompileContext, ToolCandidate } from './tool-candidates.ts';
|
|
22
26
|
import {
|
|
23
27
|
endTraceSpan,
|
|
@@ -39,9 +43,14 @@ const REPO_ROOT = pathJoin(import.meta.dir, '..', '..');
|
|
|
39
43
|
const CLI_PATH = pathJoin(REPO_ROOT, 'src', 'cli.ts');
|
|
40
44
|
const MCP_SERVER_NAME = 'imprint-compile';
|
|
41
45
|
const MAX_VERIFICATION_CYCLES = 5;
|
|
42
|
-
const MIN_MCP_TOOL_TIMEOUT_SEC = 300;
|
|
43
46
|
const MAX_MCP_TOOL_TIMEOUT_SEC = 1800;
|
|
44
47
|
|
|
48
|
+
function formatRevisionMode(enabled: boolean | undefined): string {
|
|
49
|
+
return enabled
|
|
50
|
+
? 'REVISION MODE: inspect read_session_summary.revisionContext and the listed existing artifacts/diagnostics first. Preserve proven behavior; repair or honestly narrow only what evidence contradicts.'
|
|
51
|
+
: '';
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
interface CompileViaCodexCliOptions {
|
|
46
55
|
session: Session;
|
|
47
56
|
absoluteToolDir: string;
|
|
@@ -59,13 +68,21 @@ interface CompileViaCodexCliOptions {
|
|
|
59
68
|
sharedModules?: SharedModuleManifestEntry[];
|
|
60
69
|
/** Per-tool implementation plan injected into the agent's initial message. */
|
|
61
70
|
toolPlan?: string;
|
|
71
|
+
/** Revise existing generated artifacts from durable verification feedback. */
|
|
72
|
+
revisionMode?: boolean;
|
|
62
73
|
/** Present → drive an auth compile rather than a data compile. */
|
|
63
74
|
authMode?: AuthCliCompileMode;
|
|
75
|
+
/** Auth segments only: resume the same non-interactive Codex session. */
|
|
76
|
+
resume?: { sessionId: string; message: string };
|
|
77
|
+
/** Explicit model selected by the caller. Defaults to the provider preference. */
|
|
78
|
+
model?: string;
|
|
64
79
|
}
|
|
65
80
|
|
|
66
81
|
interface CodexJsonEvent {
|
|
67
82
|
type: string;
|
|
68
83
|
thread_id?: string;
|
|
84
|
+
response_item?: unknown;
|
|
85
|
+
event_msg?: unknown;
|
|
69
86
|
item?: {
|
|
70
87
|
id?: string;
|
|
71
88
|
type?: string;
|
|
@@ -106,7 +123,7 @@ export async function compileViaCodexCli(
|
|
|
106
123
|
'imprint.tool_name': opts.candidate?.toolName,
|
|
107
124
|
'imprint.session_path': opts.sessionPath,
|
|
108
125
|
'imprint.tool_dir': opts.absoluteToolDir,
|
|
109
|
-
'imprint.model': preferredAgentModel('codex-cli'),
|
|
126
|
+
'imprint.model': opts.model ?? preferredAgentModel('codex-cli'),
|
|
110
127
|
},
|
|
111
128
|
async (span) => {
|
|
112
129
|
const result = await compileViaCodexCliImpl(opts, span);
|
|
@@ -129,7 +146,12 @@ async function compileViaCodexCliImpl(
|
|
|
129
146
|
traceSpan?: Span,
|
|
130
147
|
): Promise<CompileAgentResult> {
|
|
131
148
|
mkdirSync(opts.absoluteToolDir, { recursive: true });
|
|
132
|
-
for (const name of [
|
|
149
|
+
for (const name of [
|
|
150
|
+
COMPILE_SENTINELS.done,
|
|
151
|
+
COMPILE_SENTINELS.giveUp,
|
|
152
|
+
COMPILE_SENTINELS.checkpoint,
|
|
153
|
+
COMPILE_SENTINELS.verificationState,
|
|
154
|
+
]) {
|
|
133
155
|
const p = pathJoin(opts.absoluteToolDir, name);
|
|
134
156
|
if (existsSync(p)) {
|
|
135
157
|
try {
|
|
@@ -153,7 +175,9 @@ async function compileViaCodexCliImpl(
|
|
|
153
175
|
}
|
|
154
176
|
|
|
155
177
|
// Auth and data compiles share the spawn + JSONL driver below; only the MCP
|
|
156
|
-
// server args and the initial prompt body differ.
|
|
178
|
+
// server args and the initial prompt body differ. Auth compiles are resumable
|
|
179
|
+
// segments; resumed segments get only the orchestrator result as the next
|
|
180
|
+
// user message because Codex restores the prior conversation by session id.
|
|
157
181
|
let mcpArgs: string[];
|
|
158
182
|
let initialPrompt: string;
|
|
159
183
|
|
|
@@ -171,13 +195,9 @@ async function compileViaCodexCliImpl(
|
|
|
171
195
|
'--auth-plan-json',
|
|
172
196
|
opts.authMode.authPlanJson,
|
|
173
197
|
];
|
|
174
|
-
initialPrompt =
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
${opts.authMode.initialPrompt}
|
|
179
|
-
|
|
180
|
-
Use the imprint-compile MCP tools to inspect the session, write workflow.json, test it live, and call done() when the auth works.`;
|
|
198
|
+
initialPrompt = opts.resume
|
|
199
|
+
? opts.resume.message
|
|
200
|
+
: buildAuthCodexInitialPrompt(systemPrompt, opts.authMode.initialPrompt);
|
|
181
201
|
} else {
|
|
182
202
|
mcpArgs = [
|
|
183
203
|
'run',
|
|
@@ -187,10 +207,13 @@ Use the imprint-compile MCP tools to inspect the session, write workflow.json, t
|
|
|
187
207
|
sessionPathAbs,
|
|
188
208
|
'--tool-dir',
|
|
189
209
|
opts.absoluteToolDir,
|
|
210
|
+
'--provider',
|
|
211
|
+
'codex-cli',
|
|
190
212
|
...(opts.candidate ? ['--candidate-json', JSON.stringify(opts.candidate)] : []),
|
|
191
213
|
...(opts.sharedContext ? ['--shared-context-json', JSON.stringify(opts.sharedContext)] : []),
|
|
192
214
|
...(opts.buildPlanPath ? ['--build-plan-path', opts.buildPlanPath] : []),
|
|
193
215
|
...(opts.sharedModules ? ['--shared-modules-json', JSON.stringify(opts.sharedModules)] : []),
|
|
216
|
+
...(opts.revisionMode ? ['--revision-mode'] : []),
|
|
194
217
|
];
|
|
195
218
|
const { assignedSharedModules } = resolvePlanSliceFromFile(
|
|
196
219
|
opts.buildPlanPath,
|
|
@@ -208,14 +231,17 @@ Tool directory: ${opts.absoluteToolDir}
|
|
|
208
231
|
You will write artifacts into the tool directory.
|
|
209
232
|
${formatCandidateContext(opts.candidate, opts.sharedContext, assignedSharedModules)}
|
|
210
233
|
${formatToolPlan(opts.toolPlan)}
|
|
234
|
+
${formatRevisionMode(opts.revisionMode)}
|
|
211
235
|
|
|
212
236
|
Use the imprint-compile MCP tools to inspect the session, write artifacts, run tests, and call done(). Begin by calling read_session_summary, then proceed per the system instructions.`;
|
|
213
237
|
}
|
|
214
238
|
|
|
215
|
-
const model = preferredAgentModel('codex-cli');
|
|
239
|
+
const model = opts.model ?? preferredAgentModel('codex-cli');
|
|
216
240
|
const initialTokenCount = resolveTraceTokenCount(null, initialPrompt);
|
|
217
241
|
const captureLlmIo = traceLlmIoEnabled();
|
|
218
|
-
|
|
242
|
+
// done() owns a separately bounded live-verification phase, so its transport
|
|
243
|
+
// timeout cannot be derived from the compiler's remaining reasoning budget.
|
|
244
|
+
const mcpToolTimeoutSec = MAX_MCP_TOOL_TIMEOUT_SEC;
|
|
219
245
|
setSpanAttributes(traceSpan, {
|
|
220
246
|
...llmSpanAttributes({
|
|
221
247
|
provider: 'codex-cli',
|
|
@@ -237,15 +263,18 @@ Use the imprint-compile MCP tools to inspect the session, write artifacts, run t
|
|
|
237
263
|
'imprint.compile.initial_prompt_chars': initialPrompt.length,
|
|
238
264
|
});
|
|
239
265
|
|
|
266
|
+
const execArgs = opts.resume
|
|
267
|
+
? ['exec', 'resume']
|
|
268
|
+
: [
|
|
269
|
+
'exec',
|
|
270
|
+
// Data compiles are single-shot. Auth compiles need persisted sessions
|
|
271
|
+
// because run_verification checkpoints resume through `codex exec resume`.
|
|
272
|
+
...(!opts.authMode ? ['--ephemeral'] : []),
|
|
273
|
+
];
|
|
274
|
+
|
|
240
275
|
const args = [
|
|
241
276
|
'-a',
|
|
242
277
|
'never',
|
|
243
|
-
'exec',
|
|
244
|
-
'--json',
|
|
245
|
-
'--ephemeral',
|
|
246
|
-
'--ignore-user-config',
|
|
247
|
-
'--ignore-rules',
|
|
248
|
-
'--skip-git-repo-check',
|
|
249
278
|
'-C',
|
|
250
279
|
REPO_ROOT,
|
|
251
280
|
'-s',
|
|
@@ -261,11 +290,26 @@ Use the imprint-compile MCP tools to inspect the session, write artifacts, run t
|
|
|
261
290
|
'-c',
|
|
262
291
|
`mcp_servers.${MCP_SERVER_NAME}.tool_timeout_sec=${mcpToolTimeoutSec}`,
|
|
263
292
|
'-c',
|
|
293
|
+
`mcp_servers.${MCP_SERVER_NAME}.required=true`,
|
|
294
|
+
'-c',
|
|
264
295
|
'shell_environment_policy.inherit=all',
|
|
296
|
+
...execArgs,
|
|
297
|
+
// Compiler subprocesses need only the explicitly configured imprint MCP
|
|
298
|
+
// server. Desktop plugins add unrelated tools/instructions and can delay the
|
|
299
|
+
// mandatory first auth MCP call long enough to trip the progress watchdog.
|
|
300
|
+
'--disable',
|
|
301
|
+
'plugins',
|
|
302
|
+
'--json',
|
|
303
|
+
'--ignore-user-config',
|
|
304
|
+
'--ignore-rules',
|
|
305
|
+
'--skip-git-repo-check',
|
|
306
|
+
...(opts.resume ? [opts.resume.sessionId] : []),
|
|
265
307
|
'-',
|
|
266
308
|
];
|
|
267
309
|
|
|
268
|
-
log(
|
|
310
|
+
log(
|
|
311
|
+
`spawning codex (mcp-server=${MCP_SERVER_NAME}${opts.resume ? `, resume=${opts.resume.sessionId.slice(0, 8)}` : ''})`,
|
|
312
|
+
);
|
|
269
313
|
|
|
270
314
|
let child: ChildProcess;
|
|
271
315
|
try {
|
|
@@ -273,6 +317,11 @@ Use the imprint-compile MCP tools to inspect the session, write artifacts, run t
|
|
|
273
317
|
cwd: REPO_ROOT,
|
|
274
318
|
env: process.env,
|
|
275
319
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
320
|
+
// The npm `codex` launcher is a Node wrapper around the native binary.
|
|
321
|
+
// Give the wrapper and its descendants a dedicated process group so a
|
|
322
|
+
// watchdog can terminate the whole tree, including descendants that keep
|
|
323
|
+
// the inherited stdout pipe open after the wrapper exits.
|
|
324
|
+
detached: true,
|
|
276
325
|
});
|
|
277
326
|
} catch (err) {
|
|
278
327
|
return finalErrorResult(opts, `failed to spawn codex-cli: ${errMsg(err)}`);
|
|
@@ -282,14 +331,27 @@ Use the imprint-compile MCP tools to inspect the session, write artifacts, run t
|
|
|
282
331
|
child.stdin?.end(initialPrompt);
|
|
283
332
|
} catch (err) {
|
|
284
333
|
try {
|
|
285
|
-
child
|
|
334
|
+
signalCodexProcessTree(child, 'SIGTERM');
|
|
286
335
|
} catch {
|
|
287
336
|
// already gone
|
|
288
337
|
}
|
|
289
338
|
return finalErrorResult(opts, `failed to send prompt to codex-cli: ${errMsg(err)}`);
|
|
290
339
|
}
|
|
291
340
|
|
|
292
|
-
const
|
|
341
|
+
const terminateOnParentExit = (): void => {
|
|
342
|
+
try {
|
|
343
|
+
signalCodexProcessTree(child, 'SIGTERM');
|
|
344
|
+
} catch {
|
|
345
|
+
// Parent shutdown is best-effort; the OS may already have reaped the child.
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
process.once('exit', terminateOnParentExit);
|
|
349
|
+
let result: CompileAgentResult;
|
|
350
|
+
try {
|
|
351
|
+
result = await driveJsonl(child, opts, traceSpan);
|
|
352
|
+
} finally {
|
|
353
|
+
process.removeListener('exit', terminateOnParentExit);
|
|
354
|
+
}
|
|
293
355
|
const hasActualUsage = result.inputTokens > 0 || result.outputTokens > 0;
|
|
294
356
|
const inputTokenCount = resolveTraceTokenCount(
|
|
295
357
|
hasActualUsage ? result.inputTokens : null,
|
|
@@ -315,12 +377,14 @@ Use the imprint-compile MCP tools to inspect the session, write artifacts, run t
|
|
|
315
377
|
return result;
|
|
316
378
|
}
|
|
317
379
|
|
|
318
|
-
function
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
380
|
+
function buildAuthCodexInitialPrompt(systemPrompt: string, initialPrompt: string): string {
|
|
381
|
+
return `<system_instructions>
|
|
382
|
+
${systemPrompt}
|
|
383
|
+
</system_instructions>
|
|
384
|
+
|
|
385
|
+
${initialPrompt}
|
|
386
|
+
|
|
387
|
+
Codex provider framing: use only the imprint-compile MCP tools exposed for this task. Your first response must be the requested read_session_summary tool call, without preceding prose.`;
|
|
324
388
|
}
|
|
325
389
|
|
|
326
390
|
async function driveJsonl(
|
|
@@ -328,25 +392,33 @@ async function driveJsonl(
|
|
|
328
392
|
opts: CompileViaCodexCliOptions,
|
|
329
393
|
traceSpan?: Span,
|
|
330
394
|
): Promise<CompileAgentResult> {
|
|
331
|
-
// Capture OTel context so child-process event handlers can parent spans
|
|
332
|
-
// under the current compile.codex_cli_agent span. Bun's event emitters
|
|
333
|
-
// don't propagate AsyncLocalStorage, so without this the agent.turn.*
|
|
334
|
-
// spans appear as orphaned root traces in Phoenix.
|
|
335
|
-
const parentCtx = otelContext.active();
|
|
336
|
-
|
|
337
395
|
const conversationLog: unknown[] = [];
|
|
338
396
|
const conversationLogPath = pathJoin(opts.absoluteToolDir, '.compile-log.json');
|
|
397
|
+
const rawStdoutPath = pathJoin(opts.absoluteToolDir, '.codex-stdout.jsonl');
|
|
398
|
+
const rawStderrPath = pathJoin(opts.absoluteToolDir, '.codex-stderr.log');
|
|
339
399
|
const flushLog = (): void => {
|
|
340
400
|
try {
|
|
341
401
|
writeFileSync(conversationLogPath, JSON.stringify(conversationLog, null, 2), 'utf8');
|
|
342
402
|
} catch {}
|
|
343
403
|
};
|
|
404
|
+
let flushLogTimer: ReturnType<typeof setTimeout> | undefined;
|
|
405
|
+
const scheduleLogFlush = (): void => {
|
|
406
|
+
if (flushLogTimer) return;
|
|
407
|
+
flushLogTimer = setTimeout(() => {
|
|
408
|
+
flushLogTimer = undefined;
|
|
409
|
+
flushLog();
|
|
410
|
+
}, 50);
|
|
411
|
+
flushLogTimer.unref?.();
|
|
412
|
+
};
|
|
413
|
+
const rawStdoutChunks: string[] = [];
|
|
414
|
+
const rawStderrChunks: string[] = [];
|
|
344
415
|
let inputTokens = 0;
|
|
345
416
|
let outputTokens = 0;
|
|
346
417
|
let turn = 0;
|
|
347
418
|
let lastErrorMessage = '';
|
|
348
419
|
let stderrBuf = '';
|
|
349
420
|
let agentMessageCount = 0;
|
|
421
|
+
let capturedSessionId: string | undefined;
|
|
350
422
|
const toolSpans = new Map<string, Span>();
|
|
351
423
|
let currentTurnSpan: Span | null = null;
|
|
352
424
|
|
|
@@ -361,139 +433,202 @@ async function driveJsonl(
|
|
|
361
433
|
inputTokens,
|
|
362
434
|
outputTokens,
|
|
363
435
|
verificationCycle: 1,
|
|
364
|
-
maxVerificationCycles: MAX_VERIFICATION_CYCLES,
|
|
436
|
+
maxVerificationCycles: opts.authMode ? undefined : MAX_VERIFICATION_CYCLES,
|
|
365
437
|
});
|
|
366
438
|
};
|
|
367
439
|
|
|
368
440
|
const doneSentinel = pathJoin(opts.absoluteToolDir, COMPILE_SENTINELS.done);
|
|
369
441
|
const giveUpSentinel = pathJoin(opts.absoluteToolDir, COMPILE_SENTINELS.giveUp);
|
|
370
|
-
|
|
371
|
-
const
|
|
372
|
-
|
|
442
|
+
const checkpointSentinel = pathJoin(opts.absoluteToolDir, COMPILE_SENTINELS.checkpoint);
|
|
443
|
+
const workflowPath = pathJoin(opts.absoluteToolDir, 'workflow.json');
|
|
444
|
+
const parserPath = pathJoin(opts.absoluteToolDir, 'parser.ts');
|
|
445
|
+
const parserTestPath = pathJoin(opts.absoluteToolDir, 'parser.test.ts');
|
|
446
|
+
const terminateChild = (graceMs: number): void => {
|
|
373
447
|
try {
|
|
374
|
-
child
|
|
448
|
+
signalCodexProcessTree(child, 'SIGTERM');
|
|
375
449
|
} catch {
|
|
376
450
|
// already gone
|
|
377
451
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const deadlineTimer = setTimeout(
|
|
381
|
-
() => {
|
|
382
|
-
log('wall-clock deadline exceeded, terminating codex');
|
|
452
|
+
const forceTimer = setTimeout(() => {
|
|
383
453
|
try {
|
|
384
|
-
child
|
|
385
|
-
setTimeout(() => {
|
|
386
|
-
if (!child.killed) child.kill('SIGKILL');
|
|
387
|
-
}, 5000);
|
|
454
|
+
signalCodexProcessTree(child, 'SIGKILL');
|
|
388
455
|
} catch {
|
|
389
456
|
// already gone
|
|
390
457
|
}
|
|
391
|
-
},
|
|
392
|
-
|
|
393
|
-
|
|
458
|
+
}, graceMs);
|
|
459
|
+
forceTimer.unref?.();
|
|
460
|
+
};
|
|
461
|
+
let childExited = false;
|
|
462
|
+
const scheduleDeadlineCheck = (): ReturnType<typeof setTimeout> => {
|
|
463
|
+
const effectiveDeadlineMs = compileDeadlineAfterVerification(
|
|
464
|
+
opts.absoluteToolDir,
|
|
465
|
+
opts.deadlineMs,
|
|
466
|
+
);
|
|
467
|
+
return setTimeout(
|
|
468
|
+
() => {
|
|
469
|
+
if (childExited) return;
|
|
470
|
+
if (Date.now() < compileDeadlineAfterVerification(opts.absoluteToolDir, opts.deadlineMs)) {
|
|
471
|
+
deadlineTimer = scheduleDeadlineCheck();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
log('wall-clock deadline exceeded, terminating codex');
|
|
475
|
+
terminateChild(5000);
|
|
476
|
+
},
|
|
477
|
+
Math.max(0, effectiveDeadlineMs - Date.now()),
|
|
478
|
+
);
|
|
479
|
+
};
|
|
480
|
+
let deadlineTimer = scheduleDeadlineCheck();
|
|
394
481
|
|
|
395
482
|
let stdoutBuf = '';
|
|
396
483
|
child.stdout?.on('data', (chunk: Buffer) => {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
let evt: CodexJsonEvent;
|
|
407
|
-
try {
|
|
408
|
-
evt = JSON.parse(line) as CodexJsonEvent;
|
|
409
|
-
} catch (err) {
|
|
410
|
-
log(`unparseable jsonl line: ${errMsg(err)}`);
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
484
|
+
const chunkText = chunk.toString('utf8');
|
|
485
|
+
rawStdoutChunks.push(chunkText);
|
|
486
|
+
stdoutBuf += chunkText;
|
|
487
|
+
while (true) {
|
|
488
|
+
const nl = stdoutBuf.indexOf('\n');
|
|
489
|
+
if (nl < 0) break;
|
|
490
|
+
const line = stdoutBuf.slice(0, nl).trim();
|
|
491
|
+
stdoutBuf = stdoutBuf.slice(nl + 1);
|
|
492
|
+
if (!line) continue;
|
|
413
493
|
|
|
414
|
-
|
|
494
|
+
let evt: CodexJsonEvent;
|
|
495
|
+
try {
|
|
496
|
+
evt = JSON.parse(line) as CodexJsonEvent;
|
|
497
|
+
} catch (err) {
|
|
498
|
+
log(`unparseable jsonl line: ${errMsg(err)}`);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
415
501
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
502
|
+
conversationLog.push(evt);
|
|
503
|
+
// Keep the human-readable compile log current during a long agent turn.
|
|
504
|
+
// Rewriting is throttled so streamed bursts do not cause one full-file
|
|
505
|
+
// write per JSONL event, while a stalled/crashed turn still leaves the
|
|
506
|
+
// latest tool activity visible on disk.
|
|
507
|
+
scheduleLogFlush();
|
|
421
508
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
'imprint.agent.cumulative_input_tokens': inputTokens,
|
|
429
|
-
'imprint.agent.cumulative_output_tokens': outputTokens,
|
|
430
|
-
});
|
|
431
|
-
fireProgress('thinking');
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
509
|
+
if (evt.type === 'thread.started') {
|
|
510
|
+
log(`thread_id=${evt.thread_id ?? '(none)'}`);
|
|
511
|
+
setSpanAttributes(traceSpan, { 'codex.thread_id': evt.thread_id });
|
|
512
|
+
if (evt.thread_id) capturedSessionId = evt.thread_id;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
434
515
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const toolName = codexToolName(evt.item);
|
|
456
|
-
if (toolName) {
|
|
457
|
-
traceCodexToolEvent(toolSpans, evt.type, evt.item, toolName);
|
|
458
|
-
fireProgress(evt.type === 'item.started' ? 'tool' : 'thinking', toolName);
|
|
459
|
-
}
|
|
460
|
-
continue;
|
|
516
|
+
if (evt.type === 'turn.started') {
|
|
517
|
+
if (currentTurnSpan) endTraceSpan(currentTurnSpan);
|
|
518
|
+
flushLog();
|
|
519
|
+
turn++;
|
|
520
|
+
currentTurnSpan = startTraceSpan(`agent.turn.${turn}`, 'CHAIN', {
|
|
521
|
+
'imprint.agent.turn': turn,
|
|
522
|
+
'imprint.agent.cumulative_input_tokens': inputTokens,
|
|
523
|
+
'imprint.agent.cumulative_output_tokens': outputTokens,
|
|
524
|
+
});
|
|
525
|
+
fireProgress('thinking');
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const normalizedToolEvt = normalizeCodexToolEvent(evt);
|
|
530
|
+
if (normalizedToolEvt) {
|
|
531
|
+
const { eventType, item } = normalizedToolEvt;
|
|
532
|
+
const toolName = codexToolName(item);
|
|
533
|
+
if (toolName) {
|
|
534
|
+
traceCodexToolEvent(toolSpans, eventType, item, toolName);
|
|
535
|
+
fireProgress(eventType === 'item.started' ? 'tool' : 'thinking', toolName);
|
|
461
536
|
}
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
462
539
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
540
|
+
if ((evt.type === 'item.started' || evt.type === 'item.completed') && evt.item) {
|
|
541
|
+
const agentMessage = codexAgentMessageText(evt.item);
|
|
542
|
+
if (agentMessage && evt.type === 'item.completed') {
|
|
543
|
+
agentMessageCount++;
|
|
544
|
+
setSpanAttributes(traceSpan, {
|
|
545
|
+
'imprint.codex.agent_messages': agentMessageCount,
|
|
546
|
+
'imprint.codex.last_agent_message_chars': agentMessage.length,
|
|
547
|
+
...(traceLlmIoEnabled()
|
|
548
|
+
? llmSpanAttributes({
|
|
549
|
+
provider: 'codex-cli',
|
|
550
|
+
model: opts.model ?? preferredAgentModel('codex-cli'),
|
|
551
|
+
outputMessages: traceLlmMessages([{ role: 'assistant', content: agentMessage }]),
|
|
552
|
+
outputValue: agentMessage,
|
|
553
|
+
})
|
|
554
|
+
: {}),
|
|
555
|
+
});
|
|
476
556
|
continue;
|
|
477
557
|
}
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
478
560
|
|
|
479
|
-
|
|
480
|
-
|
|
561
|
+
if (evt.type === 'turn.completed') {
|
|
562
|
+
const turnInput = evt.usage?.input_tokens ?? 0;
|
|
563
|
+
const turnOutput = evt.usage?.output_tokens ?? 0;
|
|
564
|
+
inputTokens += turnInput;
|
|
565
|
+
outputTokens += turnOutput;
|
|
566
|
+
if (currentTurnSpan) {
|
|
567
|
+
setSpanAttributes(currentTurnSpan, {
|
|
568
|
+
'imprint.agent.turn_input_tokens': turnInput,
|
|
569
|
+
'imprint.agent.turn_output_tokens': turnOutput,
|
|
570
|
+
});
|
|
571
|
+
endTraceSpan(currentTurnSpan);
|
|
572
|
+
currentTurnSpan = null;
|
|
481
573
|
}
|
|
574
|
+
continue;
|
|
482
575
|
}
|
|
483
|
-
|
|
576
|
+
|
|
577
|
+
if (evt.type === 'error' || evt.type === 'turn.failed') {
|
|
578
|
+
lastErrorMessage = evt.message ?? evt.error?.message ?? JSON.stringify(evt);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
484
581
|
});
|
|
485
582
|
|
|
486
583
|
child.stderr?.on('data', (chunk: Buffer) => {
|
|
487
584
|
const s = chunk.toString('utf8');
|
|
585
|
+
rawStderrChunks.push(s);
|
|
488
586
|
stderrBuf += s;
|
|
489
587
|
log(`[codex stderr] ${s.trim()}`);
|
|
490
588
|
});
|
|
491
589
|
|
|
492
590
|
const exitCode: number = await new Promise((resolve) => {
|
|
493
|
-
|
|
494
|
-
|
|
591
|
+
let resolved = false;
|
|
592
|
+
let forcedKillTimer: ReturnType<typeof setTimeout> | undefined;
|
|
593
|
+
const finish = (code: number): void => {
|
|
594
|
+
if (resolved) return;
|
|
595
|
+
resolved = true;
|
|
596
|
+
if (forcedKillTimer) clearTimeout(forcedKillTimer);
|
|
597
|
+
clearInterval(sentinelTimer);
|
|
598
|
+
resolve(code);
|
|
599
|
+
};
|
|
600
|
+
const terminateForSentinel = (): void => {
|
|
601
|
+
try {
|
|
602
|
+
signalCodexProcessTree(child, 'SIGTERM');
|
|
603
|
+
} catch {
|
|
604
|
+
// already gone
|
|
605
|
+
}
|
|
606
|
+
forcedKillTimer = setTimeout(() => {
|
|
607
|
+
if (resolved) return;
|
|
608
|
+
try {
|
|
609
|
+
signalCodexProcessTree(child, 'SIGKILL');
|
|
610
|
+
} catch {
|
|
611
|
+
// already gone
|
|
612
|
+
}
|
|
613
|
+
// A checkpoint sentinel is enough to let the auth orchestrator proceed;
|
|
614
|
+
// don't wait forever for Codex to acknowledge SIGTERM.
|
|
615
|
+
finish(-1);
|
|
616
|
+
}, 2000);
|
|
617
|
+
forcedKillTimer.unref?.();
|
|
618
|
+
};
|
|
619
|
+
const sentinelTimer = setInterval(() => {
|
|
620
|
+
const checkpointReached = opts.authMode && existsSync(checkpointSentinel);
|
|
621
|
+
if (!existsSync(doneSentinel) && !existsSync(giveUpSentinel) && !checkpointReached) return;
|
|
622
|
+
terminateForSentinel();
|
|
623
|
+
}, 500);
|
|
624
|
+
child.once('close', (code) => {
|
|
625
|
+
finish(code ?? -1);
|
|
626
|
+
});
|
|
627
|
+
child.once('error', () => {
|
|
628
|
+
finish(-1);
|
|
629
|
+
});
|
|
495
630
|
});
|
|
496
|
-
|
|
631
|
+
childExited = true;
|
|
497
632
|
clearTimeout(deadlineTimer);
|
|
498
633
|
if (currentTurnSpan) endTraceSpan(currentTurnSpan);
|
|
499
634
|
for (const span of toolSpans.values()) endTraceSpan(span);
|
|
@@ -503,12 +638,15 @@ async function driveJsonl(
|
|
|
503
638
|
log(`unflushed stdout tail (${stdoutBuf.length} bytes) discarded`);
|
|
504
639
|
}
|
|
505
640
|
|
|
641
|
+
try {
|
|
642
|
+
writeFileSync(rawStdoutPath, rawStdoutChunks.join(''), 'utf8');
|
|
643
|
+
writeFileSync(rawStderrPath, rawStderrChunks.join(''), 'utf8');
|
|
644
|
+
} catch {
|
|
645
|
+
// best effort diagnostics
|
|
646
|
+
}
|
|
647
|
+
if (flushLogTimer) clearTimeout(flushLogTimer);
|
|
506
648
|
flushLog();
|
|
507
649
|
|
|
508
|
-
const workflowPath = pathJoin(opts.absoluteToolDir, 'workflow.json');
|
|
509
|
-
const parserPath = pathJoin(opts.absoluteToolDir, 'parser.ts');
|
|
510
|
-
const parserTestPath = pathJoin(opts.absoluteToolDir, 'parser.test.ts');
|
|
511
|
-
|
|
512
650
|
const verifiedOk =
|
|
513
651
|
existsSync(doneSentinel) &&
|
|
514
652
|
(() => {
|
|
@@ -539,6 +677,7 @@ async function driveJsonl(
|
|
|
539
677
|
| 'outputTokens'
|
|
540
678
|
| 'cacheReadInputTokens'
|
|
541
679
|
| 'cacheCreationInputTokens'
|
|
680
|
+
| 'sessionId'
|
|
542
681
|
> = {
|
|
543
682
|
workflowPath: existsSync(workflowPath) ? workflowPath : undefined,
|
|
544
683
|
parserPath: existsSync(parserPath) ? parserPath : undefined,
|
|
@@ -550,8 +689,32 @@ async function driveJsonl(
|
|
|
550
689
|
outputTokens,
|
|
551
690
|
cacheReadInputTokens: 0,
|
|
552
691
|
cacheCreationInputTokens: 0,
|
|
692
|
+
sessionId: capturedSessionId,
|
|
553
693
|
};
|
|
554
694
|
|
|
695
|
+
// Auth segment: the agent paused at a checkpoint for the orchestrator to act.
|
|
696
|
+
// The auth orchestrator resumes the same Codex session with the checkpoint
|
|
697
|
+
// result after running live verification.
|
|
698
|
+
if (opts.authMode && existsSync(checkpointSentinel)) {
|
|
699
|
+
let cp: AuthCheckpoint | undefined;
|
|
700
|
+
try {
|
|
701
|
+
const raw = readFileSync(checkpointSentinel, 'utf8').trim();
|
|
702
|
+
const parsed = raw ? (JSON.parse(raw) as Record<string, unknown>) : undefined;
|
|
703
|
+
if (parsed && typeof parsed.kind === 'string') cp = parsed as unknown as AuthCheckpoint;
|
|
704
|
+
} catch (err) {
|
|
705
|
+
log(`failed to parse checkpoint sentinel: ${errMsg(err)}`);
|
|
706
|
+
}
|
|
707
|
+
if (cp) {
|
|
708
|
+
return {
|
|
709
|
+
success: false,
|
|
710
|
+
outcome: 'checkpoint',
|
|
711
|
+
checkpoint: cp,
|
|
712
|
+
message: `checkpoint:${cp.kind}`,
|
|
713
|
+
...baseResult,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
555
718
|
if (existsSync(doneSentinel)) {
|
|
556
719
|
let payload: {
|
|
557
720
|
summary?: string;
|
|
@@ -597,7 +760,7 @@ async function driveJsonl(
|
|
|
597
760
|
};
|
|
598
761
|
}
|
|
599
762
|
|
|
600
|
-
if (Date.now() > opts.deadlineMs) {
|
|
763
|
+
if (Date.now() > compileDeadlineAfterVerification(opts.absoluteToolDir, opts.deadlineMs)) {
|
|
601
764
|
return {
|
|
602
765
|
success: false,
|
|
603
766
|
outcome: 'timeout',
|
|
@@ -624,6 +787,69 @@ async function driveJsonl(
|
|
|
624
787
|
};
|
|
625
788
|
}
|
|
626
789
|
|
|
790
|
+
export function collectDescendantPids(
|
|
791
|
+
rows: Array<{ pid: number; ppid: number }>,
|
|
792
|
+
rootPid: number,
|
|
793
|
+
): number[] {
|
|
794
|
+
const childrenByParent = new Map<number, number[]>();
|
|
795
|
+
for (const row of rows) {
|
|
796
|
+
const children = childrenByParent.get(row.ppid) ?? [];
|
|
797
|
+
children.push(row.pid);
|
|
798
|
+
childrenByParent.set(row.ppid, children);
|
|
799
|
+
}
|
|
800
|
+
const descendants: number[] = [];
|
|
801
|
+
const visit = (pid: number): void => {
|
|
802
|
+
for (const childPid of childrenByParent.get(pid) ?? []) {
|
|
803
|
+
visit(childPid);
|
|
804
|
+
descendants.push(childPid);
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
visit(rootPid);
|
|
808
|
+
return descendants;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function liveDescendantPids(rootPid: number): number[] {
|
|
812
|
+
const ps = spawnSync('ps', ['-axo', 'pid=,ppid='], { encoding: 'utf8' });
|
|
813
|
+
if (ps.status !== 0 || typeof ps.stdout !== 'string') return [];
|
|
814
|
+
const rows = ps.stdout
|
|
815
|
+
.split('\n')
|
|
816
|
+
.map((line) => line.trim().split(/\s+/).map(Number))
|
|
817
|
+
.filter((pair): pair is [number, number] => pair.length === 2 && pair.every(Number.isFinite))
|
|
818
|
+
.map(([pid, ppid]) => ({ pid, ppid }));
|
|
819
|
+
return collectDescendantPids(rows, rootPid);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function signalPidOrGroup(pid: number, signal: NodeJS.Signals): void {
|
|
823
|
+
try {
|
|
824
|
+
process.kill(-pid, signal);
|
|
825
|
+
return;
|
|
826
|
+
} catch {
|
|
827
|
+
// The descendant may not lead its own group. Fall back to the process itself.
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
process.kill(pid, signal);
|
|
831
|
+
} catch {
|
|
832
|
+
// already gone
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function signalCodexProcessTree(child: ChildProcess, signal: NodeJS.Signals): void {
|
|
837
|
+
if (child.pid !== undefined) {
|
|
838
|
+
// Codex-launched MCP servers may create their own process groups. Killing only
|
|
839
|
+
// the detached Codex group leaves those grandchildren running after Ctrl-C,
|
|
840
|
+
// where they can keep compiling and issuing live calls. Snapshot and terminate
|
|
841
|
+
// descendants deepest-first while the parent relationship still exists.
|
|
842
|
+
for (const pid of liveDescendantPids(child.pid)) signalPidOrGroup(pid, signal);
|
|
843
|
+
try {
|
|
844
|
+
process.kill(-child.pid, signal);
|
|
845
|
+
return;
|
|
846
|
+
} catch {
|
|
847
|
+
// Fall back to the direct child when process groups are unavailable.
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
child.kill(signal);
|
|
851
|
+
}
|
|
852
|
+
|
|
627
853
|
function traceCodexToolEvent(
|
|
628
854
|
spans: Map<string, Span>,
|
|
629
855
|
eventType: string,
|
|
@@ -670,6 +896,72 @@ function traceCodexToolEvent(
|
|
|
670
896
|
spans.delete(id);
|
|
671
897
|
}
|
|
672
898
|
|
|
899
|
+
function normalizeCodexToolEvent(evt: CodexJsonEvent):
|
|
900
|
+
| {
|
|
901
|
+
eventType: 'item.started' | 'item.completed';
|
|
902
|
+
item: NonNullable<CodexJsonEvent['item']>;
|
|
903
|
+
}
|
|
904
|
+
| undefined {
|
|
905
|
+
if (
|
|
906
|
+
(evt.type === 'item.started' || evt.type === 'item.completed') &&
|
|
907
|
+
evt.item &&
|
|
908
|
+
codexToolName(evt.item)
|
|
909
|
+
) {
|
|
910
|
+
return { eventType: evt.type, item: evt.item };
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const responseItem = isRecord(evt.response_item)
|
|
914
|
+
? evt.response_item
|
|
915
|
+
: evt.type === 'response_item' && isRecord(evt.item)
|
|
916
|
+
? evt.item
|
|
917
|
+
: undefined;
|
|
918
|
+
if (responseItem) {
|
|
919
|
+
const responseType = stringField(responseItem, 'type');
|
|
920
|
+
const name = stringField(responseItem, 'name') ?? stringField(responseItem, 'tool_name');
|
|
921
|
+
if (responseType === 'function_call' && name) {
|
|
922
|
+
return {
|
|
923
|
+
eventType:
|
|
924
|
+
stringField(responseItem, 'status') === 'in_progress' ? 'item.started' : 'item.completed',
|
|
925
|
+
item: {
|
|
926
|
+
id: stringField(responseItem, 'id') ?? stringField(responseItem, 'call_id'),
|
|
927
|
+
type: 'mcp_tool_call',
|
|
928
|
+
name,
|
|
929
|
+
arguments: responseItem.arguments,
|
|
930
|
+
status: stringField(responseItem, 'status'),
|
|
931
|
+
},
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const eventMsg = isRecord(evt.event_msg)
|
|
937
|
+
? evt.event_msg
|
|
938
|
+
: evt.type === 'event_msg' && isRecord(evt.item)
|
|
939
|
+
? evt.item
|
|
940
|
+
: undefined;
|
|
941
|
+
if (eventMsg) {
|
|
942
|
+
const name = stringField(eventMsg, 'name') ?? stringField(eventMsg, 'tool_name');
|
|
943
|
+
if (name) {
|
|
944
|
+
const msgType = stringField(eventMsg, 'type') ?? evt.type;
|
|
945
|
+
return {
|
|
946
|
+
eventType:
|
|
947
|
+
msgType.includes('start') || msgType.includes('begin')
|
|
948
|
+
? 'item.started'
|
|
949
|
+
: 'item.completed',
|
|
950
|
+
item: {
|
|
951
|
+
id: stringField(eventMsg, 'id') ?? stringField(eventMsg, 'call_id'),
|
|
952
|
+
type: 'mcp_tool_call',
|
|
953
|
+
name,
|
|
954
|
+
status: stringField(eventMsg, 'status'),
|
|
955
|
+
result: eventMsg.result ?? eventMsg.output,
|
|
956
|
+
error: eventMsg.error,
|
|
957
|
+
},
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return undefined;
|
|
963
|
+
}
|
|
964
|
+
|
|
673
965
|
function codexAgentMessageText(item: NonNullable<CodexJsonEvent['item']>): string | undefined {
|
|
674
966
|
if (item.type !== 'agent_message') return undefined;
|
|
675
967
|
if (typeof item.text === 'string') return item.text;
|
|
@@ -730,6 +1022,11 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
730
1022
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
731
1023
|
}
|
|
732
1024
|
|
|
1025
|
+
function stringField(record: Record<string, unknown>, key: string): string | undefined {
|
|
1026
|
+
const value = record[key];
|
|
1027
|
+
return typeof value === 'string' ? value : undefined;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
733
1030
|
function finalErrorResult(opts: CompileViaCodexCliOptions, message: string): CompileAgentResult {
|
|
734
1031
|
mkdirSync(opts.absoluteToolDir, { recursive: true });
|
|
735
1032
|
const conversationLogPath = pathJoin(opts.absoluteToolDir, '.compile-log.json');
|