@ckanner/flywheel 0.32.1 → 0.33.1
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/CHANGELOG.md +20 -1
- package/README.md +7 -0
- package/README.zh-CN.md +5 -0
- package/THIRD_PARTY_NOTICES.md +42 -0
- package/bin/runtime.js +25 -31
- package/cordis.patch.yml +5 -0
- package/dist/bridge.mjs +421 -0
- package/dist/claude-code-pro-bridge.mjs +82 -0
- package/dist/claude-code-pro-worker.mjs +5 -0
- package/dist/client.js +4140 -0
- package/dist/codex-pro-bridge.mjs +75 -0
- package/dist/codex-pro-worker.mjs +5 -0
- package/dist/console-read.mjs +51 -0
- package/dist/control.mjs +218 -0
- package/dist/index.mjs +185 -0
- package/dist/installation.mjs +56 -0
- package/dist/machine-worker.mjs +241 -0
- package/dist/policy.mjs +91 -0
- package/dist/prewarm.mjs +136 -0
- package/dist/protocol.mjs +260 -0
- package/dist/task-association.mjs +127 -0
- package/dist/worker.mjs +52 -0
- package/dist/workspaces.mjs +42 -0
- package/docs/README.md +1 -1
- package/docs/README.zh-CN.md +1 -1
- package/docs/design.md +8 -5
- package/docs/en/configuration.md +1 -1
- package/docs/en/contributing.md +5 -6
- package/docs/en/flow-console.md +2 -2
- package/docs/en/getting-started.md +9 -2
- package/docs/en/releasing.md +132 -174
- package/docs/validation/2026-11-13-release-0.32.1.md +83 -0
- package/docs/validation/2026-11-14-release-0.33.0.md +49 -0
- package/docs/validation/2026-11-14-single-package.md +60 -0
- package/docs/validation/README.md +3 -0
- package/docs/zh-CN/configuration.md +1 -1
- package/docs/zh-CN/contributing.md +4 -5
- package/docs/zh-CN/flow-console.md +2 -2
- package/docs/zh-CN/getting-started.md +7 -3
- package/docs/zh-CN/releasing.md +97 -81
- package/native/darwin-amd64/flywheel +0 -0
- package/native/darwin-arm64/flywheel +0 -0
- package/native/linux-amd64/flywheel +0 -0
- package/native/linux-arm64/flywheel +0 -0
- package/native/manifest.json +22 -0
- package/node_modules/dsh-subagent-codex-pro/CHANGELOG.md +403 -0
- package/node_modules/dsh-subagent-codex-pro/LICENSE +21 -0
- package/node_modules/dsh-subagent-codex-pro/README.md +557 -0
- package/node_modules/dsh-subagent-codex-pro/cordis.patch.yml +30 -0
- package/node_modules/dsh-subagent-codex-pro/docs/README.zh.md +355 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle-validation.md +65 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.md +144 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine-lifecycle.zh.md +98 -0
- package/node_modules/dsh-subagent-codex-pro/docs/machine.md +194 -0
- package/node_modules/dsh-subagent-codex-pro/docs/orchestration.zh.md +86 -0
- package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js +68 -0
- package/node_modules/dsh-subagent-codex-pro/lib/codex-exec.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js +15 -0
- package/node_modules/dsh-subagent-codex-pro/lib/dsh-types.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/index.js +830 -0
- package/node_modules/dsh-subagent-codex-pro/lib/index.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jobs.js +35 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jobs.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js +229 -0
- package/node_modules/dsh-subagent-codex-pro/lib/jsonrpc.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js +101 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-cli.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js +122 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-local.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js +167 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-recovery.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js +141 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervision.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js +128 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine-supervisor.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine.js +104 -0
- package/node_modules/dsh-subagent-codex-pro/lib/machine.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js +156 -0
- package/node_modules/dsh-subagent-codex-pro/lib/process-guardian.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/run.js +842 -0
- package/node_modules/dsh-subagent-codex-pro/lib/run.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/structured.js +329 -0
- package/node_modules/dsh-subagent-codex-pro/lib/structured.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/tool.js +295 -0
- package/node_modules/dsh-subagent-codex-pro/lib/tool.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts +88 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/codex-exec.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts +179 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/dsh-types.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts +266 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/index.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts +34 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jobs.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts +113 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/jsonrpc.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts +3 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-cli.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts +4 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-local.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts +53 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-recovery.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts +8 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervision.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts +2 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine-supervisor.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts +70 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/machine.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts +22 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/process-guardian.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts +386 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/run.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts +35 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/structured.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts +107 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/tool.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts +131 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/usage.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts +321 -0
- package/node_modules/dsh-subagent-codex-pro/lib/types/wire.d.ts.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/usage.js +356 -0
- package/node_modules/dsh-subagent-codex-pro/lib/usage.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/lib/wire.js +986 -0
- package/node_modules/dsh-subagent-codex-pro/lib/wire.js.map +1 -0
- package/node_modules/dsh-subagent-codex-pro/package.json +76 -0
- package/npm/install.js +59 -6
- package/package.json +74 -12
- package/plugins/dsh/LICENSE +21 -0
- package/plugins/dsh/PROTOCOL.md +609 -0
- package/plugins/dsh/README.en.md +4 -0
- package/plugins/dsh/README.md +342 -0
- package/plugins/dsh/README.zh-CN.md +279 -0
- package/plugins/dsh/THIRD_PARTY_NOTICES.md +42 -0
- package/plugins/dsh/vendor/README.md +17 -0
- package/plugins/dsh/vendor/codex-pro.json +16 -0
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot Codex child lifecycle over the real app-server.
|
|
3
|
+
*
|
|
4
|
+
* The run has one deadline from before startup through the terminal turn. Every
|
|
5
|
+
* startup RPC (initialize, thread/start, thread/resume, thread/name/set) and the
|
|
6
|
+
* turn use the same derived abort signal, so a server that stops answering
|
|
7
|
+
* cannot keep a delegation alive past its budget. Cleanup is a separate, finite
|
|
8
|
+
* grace: the deadline ends work, the grace ends the process range.
|
|
9
|
+
*
|
|
10
|
+
* The child is spawned as the leader of its own POSIX process group. Termination
|
|
11
|
+
* targets the group and then proves it is gone; a descendant that ignores
|
|
12
|
+
* SIGTERM or outlives its parent is reported instead of being silently assumed
|
|
13
|
+
* dead.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-subagent-codex-pro/run
|
|
16
|
+
*/
|
|
17
|
+
import { spawn } from 'node:child_process';
|
|
18
|
+
import { randomUUID } from 'node:crypto';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { LineTransport } from "./jsonrpc.js";
|
|
21
|
+
import { verifyStructuredOutput } from "./structured.js";
|
|
22
|
+
import { CodexAppServerWire, DEFAULT_ORIGINATOR, raceAbort, } from "./wire.js";
|
|
23
|
+
/** Default grace between termination tiers. */
|
|
24
|
+
const DEFAULT_DISPOSE_GRACE_MS = 3_000;
|
|
25
|
+
/** Hard upper bound on observing a process group disappear after SIGKILL. */
|
|
26
|
+
const GROUP_REAP_TIMEOUT_MS = 2_000;
|
|
27
|
+
/** A dead leader cannot lend an infinite stdout lease to a descendant. */
|
|
28
|
+
const EXIT_DRAIN_TIMEOUT_MS = 1_000;
|
|
29
|
+
/** Bound protocol and diagnostic output retained/forwarded by this adapter. */
|
|
30
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
|
|
31
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
32
|
+
/**
|
|
33
|
+
* How long one delegation may take before it is cancelled.
|
|
34
|
+
*
|
|
35
|
+
* A delegation that hangs is invisible: the job stays `running`, nothing is
|
|
36
|
+
* logged, and the session waits. The deadline turns that into a failure with a
|
|
37
|
+
* reason. Ten minutes is generous against a package whose own README says a
|
|
38
|
+
* delegation takes "a minute or more", and `0` disables it.
|
|
39
|
+
*/
|
|
40
|
+
export const DEFAULT_RUN_TIMEOUT_MS = 600_000;
|
|
41
|
+
/** A run failure, kept for its structured facts and any cleanup it observed. */
|
|
42
|
+
export class CodexRunFailure extends Error {
|
|
43
|
+
facts;
|
|
44
|
+
cleanup;
|
|
45
|
+
execution;
|
|
46
|
+
configuration;
|
|
47
|
+
usage;
|
|
48
|
+
rateLimits;
|
|
49
|
+
turnConfiguration;
|
|
50
|
+
constructor(facts, cause, cleanup, observations) {
|
|
51
|
+
const cleanupDetail = cleanup?.confirmed === false
|
|
52
|
+
? `; process cleanup unconfirmed${cleanup.detail === undefined ? '' : `: ${cleanup.detail}`}`
|
|
53
|
+
: '';
|
|
54
|
+
super(`codex-pro: product subagent failure (${describeFacts(facts)})${cleanupDetail}`, cause === undefined ? undefined : { cause });
|
|
55
|
+
this.facts = facts;
|
|
56
|
+
this.cleanup = cleanup;
|
|
57
|
+
this.execution = observations?.execution;
|
|
58
|
+
this.configuration = observations?.configuration;
|
|
59
|
+
this.usage = observations?.usage;
|
|
60
|
+
this.rateLimits = observations?.rateLimits;
|
|
61
|
+
this.turnConfiguration = observations?.turnConfiguration;
|
|
62
|
+
this.name = 'CodexRunFailure';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** The diagnostic for a delegation the deadline stopped. */
|
|
66
|
+
export function describeTimeout(timeoutMs) {
|
|
67
|
+
return `codex-pro: the delegation exceeded its ${String(timeoutMs)} ms deadline and was cancelled`;
|
|
68
|
+
}
|
|
69
|
+
/** Render failure facts for a diagnostic string. */
|
|
70
|
+
function describeFacts(facts) {
|
|
71
|
+
const fields = ['product: Codex', `stage: ${facts.stage}`, `category: ${facts.category}`];
|
|
72
|
+
if (facts.httpStatus !== undefined)
|
|
73
|
+
fields.push(`HTTP status: ${facts.httpStatus}`);
|
|
74
|
+
return fields.join('; ');
|
|
75
|
+
}
|
|
76
|
+
/** Normalise an unknown throwable. */
|
|
77
|
+
function thrown(value) {
|
|
78
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
79
|
+
}
|
|
80
|
+
/** Append a bounded reason to an optional diagnostic. */
|
|
81
|
+
function appendDiagnostic(diagnostic, detail) {
|
|
82
|
+
return diagnostic === undefined ? detail : `${diagnostic}; ${detail}`;
|
|
83
|
+
}
|
|
84
|
+
/** A valid snapshot before interruption is a known lower bound, not a final bill. */
|
|
85
|
+
function partialUsage(usage, reason) {
|
|
86
|
+
if (usage.status !== 'complete')
|
|
87
|
+
return usage;
|
|
88
|
+
return { ...usage, status: 'partial', reason: usage.reason === undefined ? reason : `${usage.reason}; ${reason}` };
|
|
89
|
+
}
|
|
90
|
+
/** A short non-throwing delay. */
|
|
91
|
+
function delay(ms) {
|
|
92
|
+
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Validate and preserve the one-shot task before crossing the process boundary.
|
|
96
|
+
*
|
|
97
|
+
* @param prompt - the delegation's content blocks.
|
|
98
|
+
* @returns the exact non-empty text block sequence.
|
|
99
|
+
*/
|
|
100
|
+
export function textTask(prompt) {
|
|
101
|
+
if (prompt.length === 0)
|
|
102
|
+
throw new Error('codex-pro: a delegation prompt must be text; images reach Codex through the '
|
|
103
|
+
+ '`images` parameter, which takes absolute paths');
|
|
104
|
+
const texts = [];
|
|
105
|
+
for (const block of prompt) {
|
|
106
|
+
if (block.type !== 'text' || typeof block.text !== 'string') {
|
|
107
|
+
throw new Error('codex-pro: a delegation prompt must be text; images reach Codex through the '
|
|
108
|
+
+ '`images` parameter, which takes absolute paths');
|
|
109
|
+
}
|
|
110
|
+
texts.push(block.text);
|
|
111
|
+
}
|
|
112
|
+
if (texts.every((text) => text.trim().length === 0)) {
|
|
113
|
+
throw new Error('codex-pro: the one-shot task must not be empty');
|
|
114
|
+
}
|
|
115
|
+
return texts;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Spawn the app-server and wrap its lifecycle.
|
|
119
|
+
*
|
|
120
|
+
* The child is detached into its own process group on POSIX so termination can
|
|
121
|
+
* target everything the delegation owns without a global `killall`. Termination
|
|
122
|
+
* is idempotent and resolves with a confirmation report rather than assuming
|
|
123
|
+
* that a signal is the same thing as a dead process.
|
|
124
|
+
*
|
|
125
|
+
* @param spec - the executable, working directory, and environment.
|
|
126
|
+
* @returns the managed child.
|
|
127
|
+
*/
|
|
128
|
+
function spawnAppServer(spec) {
|
|
129
|
+
const posix = process.platform !== 'win32';
|
|
130
|
+
const graceMs = spec.disposeGraceMs ?? DEFAULT_DISPOSE_GRACE_MS;
|
|
131
|
+
// tsc rewrites import specifiers, not new URL strings. Native TS tests and
|
|
132
|
+
// the built npm package must each start their matching guardian artifact.
|
|
133
|
+
const extension = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
|
|
134
|
+
const guardian = fileURLToPath(new URL(`./process-guardian${extension}`, import.meta.url));
|
|
135
|
+
const proc = spawn(posix ? process.execPath : spec.command, posix ? [guardian, spec.command, String(graceMs)] : ['app-server', '--stdio'], {
|
|
136
|
+
cwd: spec.cwd,
|
|
137
|
+
env: { ...process.env, ...spec.env },
|
|
138
|
+
stdio: posix ? ['pipe', 'pipe', 'pipe', 'ipc'] : ['pipe', 'pipe', 'pipe'],
|
|
139
|
+
detached: posix,
|
|
140
|
+
windowsHide: true,
|
|
141
|
+
});
|
|
142
|
+
let actualPid = posix ? null : proc.pid ?? null;
|
|
143
|
+
let actualExit;
|
|
144
|
+
let stdoutEnded = false;
|
|
145
|
+
let drainTimer;
|
|
146
|
+
let outputIncomplete = false;
|
|
147
|
+
let supervisorFailed = false;
|
|
148
|
+
let supervisorExit;
|
|
149
|
+
let spawnError = null;
|
|
150
|
+
let terminationRequested = false;
|
|
151
|
+
let completeActual;
|
|
152
|
+
let failActual;
|
|
153
|
+
const done = new Promise((resolve, reject) => {
|
|
154
|
+
completeActual = resolve;
|
|
155
|
+
failActual = reject;
|
|
156
|
+
});
|
|
157
|
+
done.catch(() => { });
|
|
158
|
+
proc.stdout.once('end', () => {
|
|
159
|
+
stdoutEnded = true;
|
|
160
|
+
if (drainTimer !== undefined)
|
|
161
|
+
clearTimeout(drainTimer);
|
|
162
|
+
drainTimer = undefined;
|
|
163
|
+
if (actualExit !== undefined)
|
|
164
|
+
completeActual(actualExit);
|
|
165
|
+
else if (supervisorFailed || supervisorExit !== undefined) {
|
|
166
|
+
failActual(new CodexRunFailure({ stage: 'supervisor', category: 'process' }));
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
proc.on('message', (message) => {
|
|
170
|
+
if (message === null || typeof message !== 'object')
|
|
171
|
+
return;
|
|
172
|
+
const value = message;
|
|
173
|
+
if (value.type === 'started' && Number.isSafeInteger(value.pid) && Number(value.pid) > 0) {
|
|
174
|
+
actualPid = Number(value.pid);
|
|
175
|
+
}
|
|
176
|
+
else if (value.type === 'exit') {
|
|
177
|
+
actualExit = {
|
|
178
|
+
exitCode: typeof value.exitCode === 'number' ? value.exitCode : null,
|
|
179
|
+
signal: typeof value.signal === 'string' ? value.signal : null,
|
|
180
|
+
};
|
|
181
|
+
if (stdoutEnded)
|
|
182
|
+
completeActual(actualExit);
|
|
183
|
+
else if (drainTimer === undefined) {
|
|
184
|
+
// IPC and stdout use different pipes. Drain already-written final
|
|
185
|
+
// frames before declaring an early exit, but never wait indefinitely
|
|
186
|
+
// for a descendant that inherited stdout (even with runTimeoutMs=0).
|
|
187
|
+
drainTimer = setTimeout(() => {
|
|
188
|
+
outputIncomplete = true;
|
|
189
|
+
failActual(new Error('app-server exited but its output did not drain within 1000 ms'));
|
|
190
|
+
}, EXIT_DRAIN_TIMEOUT_MS);
|
|
191
|
+
drainTimer.unref?.();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (value.type === 'spawn-error') {
|
|
195
|
+
spawnError = typeof value.message === 'string' ? value.message : 'app-server spawn failed';
|
|
196
|
+
failActual(new Error(spawnError));
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
const supervisorDone = new Promise((resolve) => {
|
|
200
|
+
proc.once('error', (error) => {
|
|
201
|
+
spawnError = error.message;
|
|
202
|
+
failActual(error);
|
|
203
|
+
resolve();
|
|
204
|
+
});
|
|
205
|
+
proc.once('exit', (exitCode, signal) => {
|
|
206
|
+
if (posix) {
|
|
207
|
+
supervisorExit = { exitCode, signal };
|
|
208
|
+
supervisorFailed ||= !terminationRequested;
|
|
209
|
+
// Guardian liveness cannot depend on stdio close: its Codex child may
|
|
210
|
+
// still hold the inherited pipe. Allow queued final IPC/stdout frames
|
|
211
|
+
// a bounded drain, then fail and let the caller terminate the group.
|
|
212
|
+
if (actualExit === undefined && drainTimer === undefined) {
|
|
213
|
+
drainTimer = setTimeout(() => {
|
|
214
|
+
outputIncomplete = !stdoutEnded;
|
|
215
|
+
failActual(new CodexRunFailure({ stage: 'supervisor', category: 'process' }));
|
|
216
|
+
}, EXIT_DRAIN_TIMEOUT_MS);
|
|
217
|
+
drainTimer.unref?.();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
actualExit = { exitCode, signal };
|
|
222
|
+
if (stdoutEnded)
|
|
223
|
+
completeActual(actualExit);
|
|
224
|
+
else if (drainTimer === undefined) {
|
|
225
|
+
drainTimer = setTimeout(() => {
|
|
226
|
+
outputIncomplete = true;
|
|
227
|
+
failActual(new CodexRunFailure({ stage: 'process', category: 'output-incomplete' }));
|
|
228
|
+
}, EXIT_DRAIN_TIMEOUT_MS);
|
|
229
|
+
drainTimer.unref?.();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
resolve();
|
|
233
|
+
});
|
|
234
|
+
proc.once('disconnect', () => {
|
|
235
|
+
if (!posix || terminationRequested)
|
|
236
|
+
return;
|
|
237
|
+
supervisorFailed = true;
|
|
238
|
+
if (actualExit !== undefined && stdoutEnded)
|
|
239
|
+
completeActual(actualExit);
|
|
240
|
+
else if (stdoutEnded)
|
|
241
|
+
failActual(new CodexRunFailure({ stage: 'supervisor', category: 'process' }));
|
|
242
|
+
else if (drainTimer === undefined) {
|
|
243
|
+
drainTimer = setTimeout(() => {
|
|
244
|
+
outputIncomplete = !stdoutEnded;
|
|
245
|
+
failActual(new CodexRunFailure({ stage: 'supervisor', category: 'process' }));
|
|
246
|
+
}, EXIT_DRAIN_TIMEOUT_MS);
|
|
247
|
+
drainTimer.unref?.();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
proc.once('close', (exitCode, signal) => {
|
|
251
|
+
if (drainTimer !== undefined)
|
|
252
|
+
clearTimeout(drainTimer);
|
|
253
|
+
if (posix)
|
|
254
|
+
supervisorExit = { exitCode, signal };
|
|
255
|
+
else {
|
|
256
|
+
actualExit = { exitCode, signal };
|
|
257
|
+
completeActual(actualExit);
|
|
258
|
+
}
|
|
259
|
+
// A guardian exit is not an app-server exit. In particular, a group KILL
|
|
260
|
+
// can prevent the guardian from sending the actual child's final facts.
|
|
261
|
+
if (actualExit === undefined)
|
|
262
|
+
failActual(new Error('Codex guardian exited before an app-server exit receipt'));
|
|
263
|
+
resolve();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
const groupPid = posix ? proc.pid : undefined;
|
|
267
|
+
let termination;
|
|
268
|
+
const signalErrors = [];
|
|
269
|
+
const groupState = () => {
|
|
270
|
+
if (groupPid !== undefined) {
|
|
271
|
+
try {
|
|
272
|
+
process.kill(-groupPid, 0);
|
|
273
|
+
return { alive: true };
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
if (error.code === 'ESRCH')
|
|
277
|
+
return { alive: false };
|
|
278
|
+
return { alive: undefined, detail: `cannot inspect the owned process group: ${thrown(error).message}` };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { alive: proc.exitCode === null && proc.signalCode === null };
|
|
282
|
+
};
|
|
283
|
+
const sendSignal = (signal) => {
|
|
284
|
+
terminationRequested = true;
|
|
285
|
+
try {
|
|
286
|
+
if (groupPid !== undefined)
|
|
287
|
+
process.kill(-groupPid, signal);
|
|
288
|
+
else
|
|
289
|
+
proc.kill(signal);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
if (error.code !== 'ESRCH') {
|
|
293
|
+
signalErrors.push(`${signal}: ${thrown(error).message}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const waitForExit = (timeoutMs) => new Promise((resolve) => {
|
|
298
|
+
const timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
|
|
299
|
+
supervisorDone.then(() => { clearTimeout(timer); resolve(true); });
|
|
300
|
+
});
|
|
301
|
+
const terminate = (grace) => {
|
|
302
|
+
if (termination !== undefined)
|
|
303
|
+
return termination;
|
|
304
|
+
termination = (async () => {
|
|
305
|
+
if (proc.pid === undefined)
|
|
306
|
+
return { confirmed: true };
|
|
307
|
+
sendSignal('SIGTERM');
|
|
308
|
+
await waitForExit(grace);
|
|
309
|
+
// A caller-requested guardian exit can happen before a descendant that
|
|
310
|
+
// inherited stdout has let the stream drain. Give the existing drain
|
|
311
|
+
// timer its bounded window before killing the group, so a held pipe is
|
|
312
|
+
// still reported as output-incomplete instead of being silently closed.
|
|
313
|
+
if (actualExit !== undefined && !stdoutEnded && !outputIncomplete) {
|
|
314
|
+
await delay(EXIT_DRAIN_TIMEOUT_MS);
|
|
315
|
+
}
|
|
316
|
+
if (groupState().alive !== false) {
|
|
317
|
+
sendSignal('SIGKILL');
|
|
318
|
+
await waitForExit(Math.min(Math.max(grace, 250), GROUP_REAP_TIMEOUT_MS));
|
|
319
|
+
}
|
|
320
|
+
const until = performance.now() + GROUP_REAP_TIMEOUT_MS;
|
|
321
|
+
let state = groupState();
|
|
322
|
+
while (state.alive !== false && performance.now() < until) {
|
|
323
|
+
await delay(25);
|
|
324
|
+
state = groupState();
|
|
325
|
+
}
|
|
326
|
+
// A signal can race the guardian's own group KILL. In particular, Darwin
|
|
327
|
+
// can return EPERM while a group contains only exiting/zombie members.
|
|
328
|
+
// That error is not proof of disappearance, but a later independent
|
|
329
|
+
// ESRCH is. Do not let a historical signal error override that proof.
|
|
330
|
+
if (state.alive === false)
|
|
331
|
+
return { confirmed: true };
|
|
332
|
+
return {
|
|
333
|
+
confirmed: false,
|
|
334
|
+
detail: [...signalErrors, state.detail ?? 'the owned process group was still alive after SIGKILL'].join('; '),
|
|
335
|
+
};
|
|
336
|
+
})();
|
|
337
|
+
return termination;
|
|
338
|
+
};
|
|
339
|
+
return {
|
|
340
|
+
proc, done, terminate, waitForExit,
|
|
341
|
+
outputIncomplete: () => outputIncomplete,
|
|
342
|
+
supervisorFailed: () => supervisorFailed,
|
|
343
|
+
releaseUnconfirmed: () => {
|
|
344
|
+
// A failed signal must not keep the caller alive forever through its
|
|
345
|
+
// own pipe/IPC references. Closing the lease lets the guardian make an
|
|
346
|
+
// independent cleanup attempt. This is not confirmation: the already
|
|
347
|
+
// failed receipt stays failed even if that later attempt succeeds.
|
|
348
|
+
try {
|
|
349
|
+
if (proc.connected)
|
|
350
|
+
proc.disconnect();
|
|
351
|
+
}
|
|
352
|
+
catch { /* lease already closed */ }
|
|
353
|
+
for (const stream of [proc.stdin, proc.stdout, proc.stderr]) {
|
|
354
|
+
try {
|
|
355
|
+
stream.destroy();
|
|
356
|
+
}
|
|
357
|
+
catch { /* already closed */ }
|
|
358
|
+
}
|
|
359
|
+
proc.unref();
|
|
360
|
+
},
|
|
361
|
+
facts: () => ({
|
|
362
|
+
pid: actualPid,
|
|
363
|
+
supervisorPid: posix ? proc.pid ?? null : null,
|
|
364
|
+
exitCode: actualExit?.exitCode ?? null,
|
|
365
|
+
signal: actualExit?.signal ?? null,
|
|
366
|
+
supervisorExitCode: supervisorExit?.exitCode ?? null,
|
|
367
|
+
supervisorSignal: supervisorExit?.signal ?? null,
|
|
368
|
+
terminationRequested,
|
|
369
|
+
spawnError,
|
|
370
|
+
}),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/** Build the argv, for diagnostics and tests. */
|
|
374
|
+
export function codexAppServerArgv(command) {
|
|
375
|
+
return [command, 'app-server', '--stdio'];
|
|
376
|
+
}
|
|
377
|
+
/** Build one failure for an unpublished startup. */
|
|
378
|
+
function startupFailure(stage, error, flags, cleanup) {
|
|
379
|
+
if (error instanceof CodexRunFailure) {
|
|
380
|
+
return new CodexRunFailure(error.facts, error.cause, cleanup);
|
|
381
|
+
}
|
|
382
|
+
const category = flags.timedOut
|
|
383
|
+
? 'timeout'
|
|
384
|
+
: flags.callerAborted
|
|
385
|
+
? 'aborted'
|
|
386
|
+
: stage === 'process'
|
|
387
|
+
? 'process'
|
|
388
|
+
: 'protocol';
|
|
389
|
+
return new CodexRunFailure({ stage, category }, error, cleanup);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Start the app-server child and publish its one-shot run.
|
|
393
|
+
*
|
|
394
|
+
* Resolves only after initialization and thread creation/resume, so a failure
|
|
395
|
+
* before that point rejects with no run published. After publication the result
|
|
396
|
+
* never rejects; even a failed run carries its observed usage, images, thread,
|
|
397
|
+
* configuration, and cleanup report.
|
|
398
|
+
*
|
|
399
|
+
* @param prompt - the delegation's content blocks.
|
|
400
|
+
* @param spec - working directory, executable, permission mode, and choices.
|
|
401
|
+
* @param signal - caller cancellation.
|
|
402
|
+
* @returns the published run.
|
|
403
|
+
*/
|
|
404
|
+
export async function startCodexRun(prompt, spec, signal) {
|
|
405
|
+
const id = spec.runId ?? randomUUID();
|
|
406
|
+
const startedAt = Date.now();
|
|
407
|
+
const startedMonotonic = performance.now();
|
|
408
|
+
let timedOut = false;
|
|
409
|
+
let callerAborted = false;
|
|
410
|
+
let truncated = false;
|
|
411
|
+
let initialSpawnError = null;
|
|
412
|
+
let observedChild;
|
|
413
|
+
let observedWire;
|
|
414
|
+
const execution = (failure) => ({
|
|
415
|
+
runId: id,
|
|
416
|
+
dispatch: observedWire?.dispatch ?? 'not-dispatched',
|
|
417
|
+
startedAt: new Date(startedAt).toISOString(),
|
|
418
|
+
finishedAt: new Date().toISOString(),
|
|
419
|
+
durationMs: Math.max(0, performance.now() - startedMonotonic),
|
|
420
|
+
threadId: observedWire?.threadId ?? null,
|
|
421
|
+
turnId: observedWire?.turnId ?? null,
|
|
422
|
+
...(observedChild?.facts() ?? {
|
|
423
|
+
pid: null, supervisorPid: null, exitCode: null, signal: null,
|
|
424
|
+
supervisorExitCode: null, supervisorSignal: null,
|
|
425
|
+
terminationRequested: false, spawnError: initialSpawnError,
|
|
426
|
+
}),
|
|
427
|
+
timedOut, cancelled: callerAborted || signal.aborted,
|
|
428
|
+
truncated: truncated || observedChild?.outputIncomplete() === true, failure,
|
|
429
|
+
ownership: process.platform === 'win32' ? 'owned-process' : 'supervised-group',
|
|
430
|
+
});
|
|
431
|
+
const graceMs = spec.disposeGraceMs ?? DEFAULT_DISPOSE_GRACE_MS;
|
|
432
|
+
const requestedTimeout = spec.runTimeoutMs ?? DEFAULT_RUN_TIMEOUT_MS;
|
|
433
|
+
const maxOutputBytes = spec.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
434
|
+
let texts;
|
|
435
|
+
try {
|
|
436
|
+
texts = textTask(prompt);
|
|
437
|
+
if (!Number.isSafeInteger(graceMs) || graceMs < 0 || graceMs > 30_000) {
|
|
438
|
+
throw new Error('disposeGraceMs must be an integer between 0 and 30000');
|
|
439
|
+
}
|
|
440
|
+
if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes <= 0) {
|
|
441
|
+
throw new Error('maxOutputBytes must be a positive safe integer');
|
|
442
|
+
}
|
|
443
|
+
if (!Number.isSafeInteger(requestedTimeout) || requestedTimeout < 0 || requestedTimeout > MAX_TIMER_MS) {
|
|
444
|
+
throw new Error('runTimeoutMs must be an integer between 0 and 2147483647');
|
|
445
|
+
}
|
|
446
|
+
if (spec.deadlineAt !== undefined && (!Number.isSafeInteger(spec.deadlineAt) || spec.deadlineAt <= 0
|
|
447
|
+
|| spec.deadlineAt - startedAt > MAX_TIMER_MS)) {
|
|
448
|
+
throw new Error('deadlineAt must be a positive safe timestamp within the supported timer range');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
const facts = { stage: 'request', category: 'invalid-request' };
|
|
453
|
+
throw new CodexRunFailure(facts, error, { confirmed: true }, { execution: execution(facts) });
|
|
454
|
+
}
|
|
455
|
+
if (signal.aborted) {
|
|
456
|
+
callerAborted = true;
|
|
457
|
+
const facts = { stage: 'initialize', category: 'aborted' };
|
|
458
|
+
throw new CodexRunFailure(facts, undefined, { confirmed: true }, { execution: execution(facts) });
|
|
459
|
+
}
|
|
460
|
+
const deadlineAt = spec.deadlineAt
|
|
461
|
+
?? (requestedTimeout > 0 ? startedAt + requestedTimeout : undefined);
|
|
462
|
+
const timeoutDescriptionMs = deadlineAt === undefined ? requestedTimeout : Math.max(1, deadlineAt - startedAt);
|
|
463
|
+
let child;
|
|
464
|
+
try {
|
|
465
|
+
child = spawnAppServer(spec);
|
|
466
|
+
observedChild = child;
|
|
467
|
+
}
|
|
468
|
+
catch (error) {
|
|
469
|
+
initialSpawnError = thrown(error).message;
|
|
470
|
+
const facts = { stage: 'initialize', category: 'process' };
|
|
471
|
+
throw new CodexRunFailure(facts, error, { confirmed: true }, { execution: execution(facts) });
|
|
472
|
+
}
|
|
473
|
+
const runAbort = new AbortController();
|
|
474
|
+
let outputBytes = 0;
|
|
475
|
+
const limitOutput = (chunk) => {
|
|
476
|
+
outputBytes += typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.byteLength;
|
|
477
|
+
if (outputBytes <= maxOutputBytes || truncated)
|
|
478
|
+
return;
|
|
479
|
+
truncated = true;
|
|
480
|
+
runAbort.abort(new Error(`codex-pro: output exceeded ${String(maxOutputBytes)} bytes`));
|
|
481
|
+
child.proc.stdout.destroy();
|
|
482
|
+
child.proc.stderr.destroy();
|
|
483
|
+
};
|
|
484
|
+
child.proc.stdout.on('data', limitOutput);
|
|
485
|
+
child.proc.stderr.on('data', limitOutput);
|
|
486
|
+
const transport = new LineTransport(child.proc.stdin, child.proc.stdout);
|
|
487
|
+
const wire = new CodexAppServerWire(child.proc.stdout, child.proc.stdin, transport, {
|
|
488
|
+
permissionMode: spec.permissionMode,
|
|
489
|
+
model: spec.model,
|
|
490
|
+
threadMode: spec.threadMode,
|
|
491
|
+
originator: spec.originator ?? DEFAULT_ORIGINATOR,
|
|
492
|
+
});
|
|
493
|
+
observedWire = wire;
|
|
494
|
+
if (spec.onProgress !== undefined)
|
|
495
|
+
wire.setProgressReporter(spec.onProgress);
|
|
496
|
+
const forwardStderr = (chunk) => {
|
|
497
|
+
if (truncated)
|
|
498
|
+
return;
|
|
499
|
+
try {
|
|
500
|
+
process.stderr.write(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
// Diagnostics must never take the run down.
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
child.proc.stderr.on('data', forwardStderr);
|
|
507
|
+
let timer;
|
|
508
|
+
const abortRun = () => {
|
|
509
|
+
if (runAbort.signal.aborted)
|
|
510
|
+
return;
|
|
511
|
+
runAbort.abort(new Error('codex-pro: run cancelled locally'));
|
|
512
|
+
wire.interrupt();
|
|
513
|
+
};
|
|
514
|
+
const requestCancel = () => {
|
|
515
|
+
callerAborted = true;
|
|
516
|
+
abortRun();
|
|
517
|
+
};
|
|
518
|
+
const onCallerAbort = () => {
|
|
519
|
+
requestCancel();
|
|
520
|
+
};
|
|
521
|
+
if (deadlineAt !== undefined) {
|
|
522
|
+
timer = setTimeout(() => {
|
|
523
|
+
timedOut = true;
|
|
524
|
+
abortRun();
|
|
525
|
+
}, Math.max(0, deadlineAt - Date.now()));
|
|
526
|
+
timer.unref?.();
|
|
527
|
+
}
|
|
528
|
+
signal.addEventListener('abort', onCallerAbort, { once: true });
|
|
529
|
+
// A signal that aborted between the pre-spawn check and this listener would
|
|
530
|
+
// otherwise never deliver its event; the run must still observe it.
|
|
531
|
+
if (signal.aborted)
|
|
532
|
+
onCallerAbort();
|
|
533
|
+
const teardown = async () => {
|
|
534
|
+
if (timer !== undefined)
|
|
535
|
+
clearTimeout(timer);
|
|
536
|
+
timer = undefined;
|
|
537
|
+
wire.close();
|
|
538
|
+
try {
|
|
539
|
+
child.proc.stdin.end();
|
|
540
|
+
}
|
|
541
|
+
catch { /* already closed */ }
|
|
542
|
+
const cleanup = await child.terminate(graceMs);
|
|
543
|
+
if (!cleanup.confirmed)
|
|
544
|
+
child.releaseUnconfirmed();
|
|
545
|
+
signal.removeEventListener('abort', onCallerAbort);
|
|
546
|
+
child.proc.stderr.off('data', forwardStderr);
|
|
547
|
+
child.proc.stdout.off('data', limitOutput);
|
|
548
|
+
child.proc.stderr.off('data', limitOutput);
|
|
549
|
+
return cleanup;
|
|
550
|
+
};
|
|
551
|
+
let tornDown;
|
|
552
|
+
const teardownOnce = () => (tornDown ??= teardown());
|
|
553
|
+
let processFailureFacts;
|
|
554
|
+
const processFailure = child.done.then(() => {
|
|
555
|
+
processFailureFacts = { stage: 'process', category: 'process' };
|
|
556
|
+
throw new CodexRunFailure(processFailureFacts);
|
|
557
|
+
}, (error) => {
|
|
558
|
+
processFailureFacts = error instanceof CodexRunFailure ? error.facts : { stage: 'process', category: 'process' };
|
|
559
|
+
throw new CodexRunFailure(processFailureFacts, error);
|
|
560
|
+
});
|
|
561
|
+
processFailure.catch(() => { });
|
|
562
|
+
const lifecycle = spec.onLifecycle === undefined ? undefined : async (event) => {
|
|
563
|
+
await raceAbort(Promise.race([Promise.resolve().then(() => spec.onLifecycle({ event, runId: id,
|
|
564
|
+
threadId: wire.threadId ?? null, turnId: wire.turnId ?? null, configuration: wire.configuration ?? null })),
|
|
565
|
+
processFailure]), runAbort.signal);
|
|
566
|
+
};
|
|
567
|
+
let startupStage = 'initialize';
|
|
568
|
+
try {
|
|
569
|
+
await Promise.race([wire.initialize(runAbort.signal), processFailure]);
|
|
570
|
+
if (spec.resumeThreadId === undefined) {
|
|
571
|
+
startupStage = 'thread-start';
|
|
572
|
+
await Promise.race([wire.startThread(spec.cwd, runAbort.signal), processFailure]);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
startupStage = 'thread-resume';
|
|
576
|
+
const overrides = {
|
|
577
|
+
cwd: spec.cwd,
|
|
578
|
+
...(spec.model === undefined ? {} : { model: spec.model }),
|
|
579
|
+
permission: spec.permissionMode,
|
|
580
|
+
};
|
|
581
|
+
await Promise.race([wire.resumeThread(spec.resumeThreadId, overrides, runAbort.signal), processFailure]);
|
|
582
|
+
}
|
|
583
|
+
// A persistent thread is named so the Codex application's list says where it
|
|
584
|
+
// came from; the id alone cannot, because the originator has to be the value
|
|
585
|
+
// that client filters on.
|
|
586
|
+
if (spec.threadMode === 'persistent' && spec.threadName !== undefined) {
|
|
587
|
+
startupStage = 'thread-name';
|
|
588
|
+
await Promise.race([wire.setThreadName(spec.threadName, runAbort.signal), processFailure]);
|
|
589
|
+
}
|
|
590
|
+
if (lifecycle !== undefined)
|
|
591
|
+
await lifecycle('thread-ready');
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
const failureStage = processFailureFacts === undefined ? startupStage : 'process';
|
|
595
|
+
const cleanup = await teardownOnce();
|
|
596
|
+
const failure = startupFailure(failureStage, error, { timedOut, callerAborted }, cleanup);
|
|
597
|
+
const facts = truncated ? { stage: startupStage, category: 'output-limit' }
|
|
598
|
+
: child.outputIncomplete() ? { stage: startupStage, category: 'output-incomplete' }
|
|
599
|
+
: failure.facts;
|
|
600
|
+
throw new CodexRunFailure(facts, error, cleanup, {
|
|
601
|
+
execution: execution(facts), configuration: wire.configuration,
|
|
602
|
+
usage: partialUsage(wire.usageReport, 'startup ended without a completed turn'), rateLimits: wire.rateLimitsReport,
|
|
603
|
+
turnConfiguration: wire.turnConfiguration,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
const finish = async (turn, error) => {
|
|
607
|
+
let failure = error instanceof CodexRunFailure
|
|
608
|
+
? error.facts
|
|
609
|
+
: error === undefined ? null : { stage: 'turn', category: 'protocol' };
|
|
610
|
+
const cleanup = await teardownOnce();
|
|
611
|
+
callerAborted ||= signal.aborted;
|
|
612
|
+
const effectiveTruncated = truncated || child.outputIncomplete();
|
|
613
|
+
const usage = turn === undefined || timedOut || callerAborted || effectiveTruncated || !cleanup.confirmed
|
|
614
|
+
? partialUsage(wire.usageReport, 'run ended without a reliable terminal usage boundary')
|
|
615
|
+
: wire.usageReport;
|
|
616
|
+
const rateLimits = wire.rateLimitsReport;
|
|
617
|
+
const configuration = wire.configuration;
|
|
618
|
+
const observedImages = turn?.images ?? wire.images;
|
|
619
|
+
let output = turn?.output ?? wire.partialOutput;
|
|
620
|
+
let stopReason = effectiveTruncated ? 'error'
|
|
621
|
+
: timedOut ? 'timeout'
|
|
622
|
+
: callerAborted ? 'aborted'
|
|
623
|
+
: turn?.stopReason ?? 'error';
|
|
624
|
+
const approvalDiagnostic = wire.collectDiagnostic();
|
|
625
|
+
let diagnostic = approvalDiagnostic;
|
|
626
|
+
let structured;
|
|
627
|
+
let schemaError;
|
|
628
|
+
const processFacts = child.facts();
|
|
629
|
+
if (child.supervisorFailed()) {
|
|
630
|
+
if (stopReason === 'completed')
|
|
631
|
+
stopReason = 'error';
|
|
632
|
+
failure = { stage: 'supervisor', category: 'process' };
|
|
633
|
+
diagnostic = appendDiagnostic(diagnostic, 'Codex guardian exited before cleanup was requested');
|
|
634
|
+
}
|
|
635
|
+
if ((processFacts.exitCode !== null && processFacts.exitCode !== 0)
|
|
636
|
+
|| (processFacts.signal !== null && !['SIGTERM', 'SIGKILL'].includes(processFacts.signal))) {
|
|
637
|
+
if (stopReason === 'completed')
|
|
638
|
+
stopReason = 'error';
|
|
639
|
+
failure = { stage: 'process', category: 'process' };
|
|
640
|
+
diagnostic = appendDiagnostic(diagnostic, `app-server exited unexpectedly (code=${String(processFacts.exitCode)}, signal=${String(processFacts.signal)})`);
|
|
641
|
+
}
|
|
642
|
+
if (spec.outputSchema !== undefined && stopReason === 'completed') {
|
|
643
|
+
const text = output.map((block) => block.text).join('\n\n');
|
|
644
|
+
const verification = verifyStructuredOutput(text, spec.outputSchema);
|
|
645
|
+
if (verification.ok) {
|
|
646
|
+
structured = verification.value;
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
schemaError = verification.error;
|
|
650
|
+
// A schema was requested: an unparseable or invalid answer is not a
|
|
651
|
+
// structured success. Preserve the raw output and say why it failed.
|
|
652
|
+
if (stopReason === 'completed')
|
|
653
|
+
stopReason = 'error';
|
|
654
|
+
failure = { stage: 'output', category: 'schema' };
|
|
655
|
+
diagnostic = diagnostic === undefined ? schemaError : `${diagnostic}; ${schemaError}`;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (!cleanup.confirmed) {
|
|
659
|
+
if (stopReason === 'completed')
|
|
660
|
+
stopReason = 'error';
|
|
661
|
+
failure ??= { stage: 'cleanup', category: 'cleanup' };
|
|
662
|
+
const cleanupDetail = `process cleanup could not be confirmed${cleanup.detail === undefined ? '' : `: ${cleanup.detail}`}`;
|
|
663
|
+
diagnostic = diagnostic === undefined ? cleanupDetail : `${diagnostic}; ${cleanupDetail}`;
|
|
664
|
+
}
|
|
665
|
+
if (effectiveTruncated) {
|
|
666
|
+
if (truncated)
|
|
667
|
+
failure = { stage: 'output', category: 'output-limit' };
|
|
668
|
+
else
|
|
669
|
+
failure ??= { stage: 'output', category: 'output-incomplete' };
|
|
670
|
+
const detail = truncated
|
|
671
|
+
? `codex-pro: output exceeded ${String(maxOutputBytes)} bytes and was truncated`
|
|
672
|
+
: 'codex-pro: app-server output could not be completely drained';
|
|
673
|
+
diagnostic = diagnostic === undefined ? detail : `${diagnostic}; ${detail}`;
|
|
674
|
+
}
|
|
675
|
+
else if (timedOut) {
|
|
676
|
+
failure = { stage: 'turn', category: 'timeout' };
|
|
677
|
+
const timeoutDetail = describeTimeout(timeoutDescriptionMs);
|
|
678
|
+
diagnostic = diagnostic === undefined ? timeoutDetail : `${diagnostic}; ${timeoutDetail}`;
|
|
679
|
+
}
|
|
680
|
+
else if (callerAborted) {
|
|
681
|
+
failure = { stage: 'turn', category: 'aborted' };
|
|
682
|
+
const abortDetail = 'codex-pro: the delegation was cancelled by its caller';
|
|
683
|
+
diagnostic = diagnostic === undefined ? abortDetail : `${diagnostic}; ${abortDetail}`;
|
|
684
|
+
}
|
|
685
|
+
else if (error !== undefined && diagnostic === undefined) {
|
|
686
|
+
diagnostic = error instanceof Error ? error.message : String(error);
|
|
687
|
+
}
|
|
688
|
+
else if (error !== undefined && !(error instanceof CodexRunFailure)) {
|
|
689
|
+
diagnostic = `${diagnostic}; ${error instanceof Error ? error.message : String(error)}`;
|
|
690
|
+
}
|
|
691
|
+
if (stopReason !== 'completed' && failure === null) {
|
|
692
|
+
failure = { stage: 'turn', category: stopReason };
|
|
693
|
+
}
|
|
694
|
+
return {
|
|
695
|
+
output,
|
|
696
|
+
stopReason,
|
|
697
|
+
...(diagnostic === undefined ? {} : { diagnostic }),
|
|
698
|
+
...(approvalDiagnostic === undefined ? {} : { approvalDiagnostic }),
|
|
699
|
+
threadId: wire.threadId,
|
|
700
|
+
images: observedImages,
|
|
701
|
+
usage,
|
|
702
|
+
rateLimits,
|
|
703
|
+
...(configuration === undefined ? {} : { configuration }),
|
|
704
|
+
cleanup,
|
|
705
|
+
execution: execution(failure),
|
|
706
|
+
...(wire.turnConfiguration === undefined ? {} : { turnConfiguration: wire.turnConfiguration }),
|
|
707
|
+
...(structured === undefined ? {} : { structured }),
|
|
708
|
+
...(schemaError === undefined ? {} : { schemaError }),
|
|
709
|
+
};
|
|
710
|
+
};
|
|
711
|
+
const result = (async () => {
|
|
712
|
+
try {
|
|
713
|
+
const turn = await Promise.race([
|
|
714
|
+
wire.runTurn(texts, runAbort.signal, spec.effort ?? spec.reasoningEffort, spec.images ?? [], spec.outputSchema, lifecycle),
|
|
715
|
+
processFailure,
|
|
716
|
+
]);
|
|
717
|
+
return await finish(turn, undefined);
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
return await finish(undefined, error);
|
|
721
|
+
}
|
|
722
|
+
})();
|
|
723
|
+
return {
|
|
724
|
+
id,
|
|
725
|
+
result,
|
|
726
|
+
requestCancel,
|
|
727
|
+
teardown: teardownOnce,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Decide which thread one call continues.
|
|
732
|
+
*
|
|
733
|
+
* `session` mode has one thread per Agent session, but a thread carries one turn
|
|
734
|
+
* at a time: two delegations sent together would otherwise both reach for it and
|
|
735
|
+
* the second would fail. A remembered thread that another delegation is already
|
|
736
|
+
* using is therefore skipped, and this call gets a thread of its own — which is
|
|
737
|
+
* what `persistent` does anyway, so parallel work degrades to the mode that
|
|
738
|
+
* supports it instead of failing and retrying.
|
|
739
|
+
*
|
|
740
|
+
* An id the caller passed explicitly is never skipped: continuing *that* thread
|
|
741
|
+
* was the request, and silently running somewhere else would answer a different
|
|
742
|
+
* question.
|
|
743
|
+
*
|
|
744
|
+
* @param input - the call's explicit id, the configured mode, the session's
|
|
745
|
+
* remembered thread, and the threads currently in flight.
|
|
746
|
+
* @returns the thread to continue and the remembered one that was busy.
|
|
747
|
+
*/
|
|
748
|
+
export function chooseThread(input) {
|
|
749
|
+
if (input.explicit !== undefined)
|
|
750
|
+
return { reuse: input.explicit, skippedBusy: undefined };
|
|
751
|
+
if (input.configured !== 'session' || input.remembered === undefined) {
|
|
752
|
+
return { reuse: undefined, skippedBusy: undefined };
|
|
753
|
+
}
|
|
754
|
+
if (input.inFlight.has(input.remembered)) {
|
|
755
|
+
return { reuse: undefined, skippedBusy: input.remembered };
|
|
756
|
+
}
|
|
757
|
+
return { reuse: input.remembered, skippedBusy: undefined };
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* A delegation that did not complete.
|
|
761
|
+
*
|
|
762
|
+
* Carries the thread, images, and (when publication reached the run) the whole
|
|
763
|
+
* outcome, so a caller can report observed usage even though the run failed.
|
|
764
|
+
*/
|
|
765
|
+
export class CodexDelegationFailure extends Error {
|
|
766
|
+
/** The thread the failed run used, when the app-server had got that far. */
|
|
767
|
+
threadId;
|
|
768
|
+
/** Images the failed run produced before it failed. */
|
|
769
|
+
images;
|
|
770
|
+
/** Everything observed about the failed run, when one was published. */
|
|
771
|
+
outcome;
|
|
772
|
+
constructor(message, threadId, images = [], outcome) {
|
|
773
|
+
super(message);
|
|
774
|
+
this.name = 'CodexDelegationFailure';
|
|
775
|
+
this.threadId = threadId;
|
|
776
|
+
this.images = images;
|
|
777
|
+
this.outcome = outcome;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
/** Run one delegation and return its settled outcome. */
|
|
781
|
+
export async function runCodexOnce(options) {
|
|
782
|
+
const run = await startCodexRun([{ type: 'text', text: options.prompt }], {
|
|
783
|
+
cwd: options.cwd,
|
|
784
|
+
command: options.command,
|
|
785
|
+
permissionMode: options.permissionMode,
|
|
786
|
+
model: options.model,
|
|
787
|
+
effort: options.reasoningEffort,
|
|
788
|
+
threadMode: options.threadMode,
|
|
789
|
+
resumeThreadId: options.threadId,
|
|
790
|
+
originator: options.originator,
|
|
791
|
+
runTimeoutMs: options.runTimeoutMs,
|
|
792
|
+
...(options.deadlineAt === undefined ? {} : { deadlineAt: options.deadlineAt }),
|
|
793
|
+
images: options.images,
|
|
794
|
+
outputSchema: options.outputSchema,
|
|
795
|
+
threadName: options.threadName,
|
|
796
|
+
env: options.env,
|
|
797
|
+
disposeGraceMs: options.disposeGraceMs,
|
|
798
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
799
|
+
onProgress: options.onProgress,
|
|
800
|
+
}, options.signal);
|
|
801
|
+
options.onStart?.(run.id);
|
|
802
|
+
try {
|
|
803
|
+
return await run.result;
|
|
804
|
+
}
|
|
805
|
+
finally {
|
|
806
|
+
await run.teardown();
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Run one delegation to completion and return its final text.
|
|
811
|
+
*
|
|
812
|
+
* The result keeps the thread, images, usage, configuration and cleanup report
|
|
813
|
+
* available to callers that need to report more than the answer. A run that
|
|
814
|
+
* does not complete rejects with the whole outcome attached.
|
|
815
|
+
*
|
|
816
|
+
* @param options - the task, its working directory, and the choices.
|
|
817
|
+
* @returns the child's final message and its observability fields.
|
|
818
|
+
* @throws `CodexDelegationFailure` when the run does not complete.
|
|
819
|
+
*/
|
|
820
|
+
export async function delegateOnce(options) {
|
|
821
|
+
const outcome = await runCodexOnce(options);
|
|
822
|
+
const text = outcome.output.map((block) => block.text).join('\n\n');
|
|
823
|
+
if (outcome.stopReason !== 'completed'
|
|
824
|
+
|| text === ''
|
|
825
|
+
|| outcome.schemaError !== undefined
|
|
826
|
+
|| !outcome.cleanup.confirmed) {
|
|
827
|
+
throw new CodexDelegationFailure(outcome.diagnostic ?? `codex-pro: the run ended as ${outcome.stopReason}`, outcome.threadId, outcome.images ?? [], outcome);
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
text,
|
|
831
|
+
threadId: outcome.threadId,
|
|
832
|
+
images: outcome.images ?? [],
|
|
833
|
+
usage: outcome.usage,
|
|
834
|
+
rateLimits: outcome.rateLimits,
|
|
835
|
+
...(outcome.configuration === undefined ? {} : { configuration: outcome.configuration }),
|
|
836
|
+
cleanup: outcome.cleanup,
|
|
837
|
+
execution: outcome.execution,
|
|
838
|
+
...(outcome.turnConfiguration === undefined ? {} : { turnConfiguration: outcome.turnConfiguration }),
|
|
839
|
+
...(outcome.structured === undefined ? {} : { structured: outcome.structured }),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
//# sourceMappingURL=run.js.map
|