humanish 0.100.0 → 0.100.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.
@@ -111,20 +111,19 @@ async function checkVersion(file, env, cwd, spawnFn, deadline) {
111
111
  }
112
112
  }
113
113
  }
114
+ const rawCompactionTypes = ["compaction", "compaction_summary", "context_compaction"];
115
+ const allowedRawItemTypes = ["message", "reasoning", ...rawCompactionTypes];
114
116
  const unclosedChildren = new Set();
115
117
  function retainUnclosedChild(closed) {
116
118
  unclosedChildren.add(closed);
117
119
  void closed.then(() => { unclosedChildren.delete(closed); });
118
120
  }
119
- /** Each call owns a separate process, home and thread. An unresolved child blocks
120
- * new work, but ordinary participant turns may run concurrently. */
121
+ /** Analysts/readiness keep their one-shot lifetime; participants own a session. */
121
122
  export async function runRestrictedCodexSession(request, options = {}, readinessOnly = false) {
122
- const error = restrictedCodexRequestError(request);
123
- if (error)
124
- return restrictedCodexFailure(error);
125
- if (unclosedChildren.size)
126
- return restrictedCodexFailure("codex_busy");
127
- return executeRestrictedCodexSession(request, options, readinessOnly);
123
+ const session = createRestrictedCodexSession(options);
124
+ const result = await session.run(request, readinessOnly);
125
+ return await session.close() ? result
126
+ : { ...restrictedCodexFailure("codex_cleanup_failed", result.dispatched, result.usage), failurePhase: "cleanup" };
128
127
  }
129
128
  async function writeRecoveryMarker(work, sourceEnv, reason) {
130
129
  const cache = sourceEnv.XDG_CACHE_HOME ?? path.join(sourceEnv.HOME ?? homedir(), ".cache");
@@ -163,128 +162,146 @@ function hasScopedIdentity(method, params, threadId, turnId) {
163
162
  && (turnId === undefined || params.turnId === turnId);
164
163
  return (params.threadId === undefined || params.threadId === threadId) && (params.turnId === undefined || params.turnId === turnId);
165
164
  }
166
- /** Exactly one fresh thread/turn; private host auth, no participant state reuse. */
167
- async function executeRestrictedCodexSession(request, options = {}, readinessOnly = false) {
168
- const admissionError = restrictedCodexRequestError(request);
169
- if (admissionError)
170
- return restrictedCodexFailure(admissionError);
165
+ /** One private process and conversation per owner. Only completed turns may continue. */
166
+ export function createRestrictedCodexSession(options = {}) {
171
167
  const platform = options.platform ?? process.platform, arch = options.arch ?? process.arch;
172
- if (!(platform === "linux" && arch === "x64") && !(platform === "darwin" && arch === "arm64"))
173
- return restrictedCodexFailure("codex_unsupported_platform");
174
- const deadline = new RestrictedCodexDeadline(request.timeoutMs, request.signal);
175
168
  const sourceEnv = options.env ?? process.env;
176
169
  const spawnFn = options.spawnFn ?? ((file, args, settings) => spawn(file, args, settings));
177
170
  let work, authLink, transport;
178
- let threadId, turnId, earlyTurnId;
179
- let dispatched = false, completed = false, usage = null;
180
- let generatedDeltaBytes = 0;
181
- let outputItem;
182
- let result = restrictedCodexFailure("codex_process_failed");
183
- let phase = "startup";
184
- const early = [];
185
- let resolveTurn;
186
- const finished = new Promise(resolve => { resolveTurn = resolve; });
187
- const handleTurnEvent = (method, params) => {
188
- if (!hasScopedIdentity(method, params, threadId, turnId)) {
189
- deadline.stop("codex_protocol_error");
190
- return;
191
- }
192
- const item = codexRecord(params.item);
193
- if (method === "rawResponseItem/completed") {
194
- if (!["message", "reasoning"].includes(String(item.type))) {
195
- deadline.stop("codex_tool_call");
196
- return;
197
- }
198
- if (item.type === "message" && Array.isArray(item.content)
199
- && item.content.some(content => codexRecord(content).type === "refusal"))
200
- deadline.stop("refusal");
201
- }
202
- if (method === "thread/tokenUsage/updated")
203
- usage = restrictedCodexUsage(params.tokenUsage);
204
- if (method === "item/started" || method === "item/completed") {
205
- if (!["userMessage", "agentMessage", "reasoning"].includes(String(item.type))) {
206
- deadline.stop("codex_tool_call");
207
- return;
171
+ let threadId, cwd = "", scratch = "";
172
+ let identity;
173
+ let previousUsage = { input: 0, output: 0, cachedInput: 0, cacheWriteInput: 0 };
174
+ let activeDeadline;
175
+ let interrupt;
176
+ let closed = false, cleanupTrusted = true;
177
+ let pending, closing, disposing;
178
+ const dispose = () => {
179
+ closed = true;
180
+ return disposing ??= (async () => {
181
+ let cleaned = cleanupTrusted;
182
+ if (transport) {
183
+ cleaned = await transport.close(interrupt).catch(() => false) && cleaned;
184
+ if (!cleaned)
185
+ retainUnclosedChild(transport.owned.closed);
208
186
  }
209
- if (item.type === "agentMessage") {
210
- if (item.delivery === "async" || (Array.isArray(item.questions) && item.questions.length > 0)) {
211
- deadline.stop("codex_tool_call");
212
- return;
187
+ let authReplaced = false;
188
+ if (authLink && cleaned) {
189
+ try {
190
+ authReplaced = !(await lstat(authLink)).isSymbolicLink();
191
+ if (!authReplaced)
192
+ await unlink(authLink);
213
193
  }
214
- if (method === "item/completed" && item.phase !== "commentary") {
215
- if (typeof item.id !== "string" || typeof item.text !== "string" || Buffer.byteLength(item.text) > CODEX_MAX_OUTPUT_BYTES
216
- || (item.phase !== null && item.phase !== "final_answer") || (item.delivery !== null && item.delivery !== undefined)
217
- || (outputItem && (outputItem.id !== item.id || outputItem.text !== item.text))) {
218
- deadline.stop("invalid_response");
219
- return;
220
- }
221
- outputItem = { id: item.id, text: item.text };
194
+ catch (error) {
195
+ if (codexRecord(error).code !== "ENOENT")
196
+ cleaned = false;
222
197
  }
223
198
  }
224
- }
225
- if (method === "turn/completed") {
226
- const turn = codexRecord(params.turn);
227
- if (turn.id !== turnId || completed) {
199
+ // Preserve unexpected login rotation for private recovery, never overwrite host auth.
200
+ if (authReplaced) {
201
+ cleaned = false;
202
+ if (work)
203
+ await preserveUnexpectedAuth(work, sourceEnv).catch(() => undefined);
204
+ }
205
+ if (work && cleaned)
206
+ await rm(work, { recursive: true, force: true }).catch(() => { cleaned = false; });
207
+ if (work && !cleaned && !authReplaced)
208
+ await writeRecoveryMarker(work, sourceEnv, "process_cleanup_unconfirmed").catch(() => undefined);
209
+ return cleaned;
210
+ })();
211
+ };
212
+ async function execute(request, readinessOnly) {
213
+ const deadline = new RestrictedCodexDeadline(request.timeoutMs, request.signal);
214
+ activeDeadline = deadline;
215
+ let turnId, earlyTurnId;
216
+ let dispatched = false, usage = null;
217
+ let completed = false, compacted = false;
218
+ let latestUsage = null;
219
+ let generatedDeltaBytes = 0;
220
+ let outputItem;
221
+ let result = restrictedCodexFailure("codex_process_failed");
222
+ let phase = "startup";
223
+ const early = [];
224
+ let resolveTurn;
225
+ const finished = new Promise(resolve => { resolveTurn = resolve; });
226
+ const handleTurnEvent = (method, params) => {
227
+ if (!hasScopedIdentity(method, params, threadId, turnId)) {
228
228
  deadline.stop("codex_protocol_error");
229
229
  return;
230
230
  }
231
- completed = true;
232
- if (turn.status === "interrupted") {
233
- deadline.stop("cancelled");
234
- return;
231
+ const item = codexRecord(params.item);
232
+ // CLI 0.154 thread totals omit compaction requests. Retain known usage,
233
+ // but never label it complete when native compaction occurred in this turn.
234
+ if (method === "thread/compacted" || item.type === "contextCompaction" || rawCompactionTypes.includes(String(item.type)))
235
+ compacted = true;
236
+ if (method === "rawResponseItem/completed") {
237
+ if (!allowedRawItemTypes.includes(String(item.type))) {
238
+ deadline.stop("codex_tool_call");
239
+ return;
240
+ }
241
+ if (item.type === "message" && Array.isArray(item.content)
242
+ && item.content.some(content => codexRecord(content).type === "refusal"))
243
+ deadline.stop("refusal");
235
244
  }
236
- if (turn.status !== "completed" || turn.error !== null || !outputItem) {
237
- deadline.stop("invalid_response");
238
- return;
245
+ if (method === "thread/tokenUsage/updated") {
246
+ const total = restrictedCodexUsage(params.tokenUsage);
247
+ // App-server reports cumulative thread usage. Receipts must charge only this turn.
248
+ if (total && previousUsage) {
249
+ const delta = { input: total.input - previousUsage.input, output: total.output - previousUsage.output,
250
+ cachedInput: (total.cachedInput ?? 0) - (previousUsage.cachedInput ?? 0),
251
+ cacheWriteInput: (total.cacheWriteInput ?? 0) - (previousUsage.cacheWriteInput ?? 0) };
252
+ usage = Object.values(delta).every(value => Number.isSafeInteger(value) && value >= 0)
253
+ && delta.cachedInput + delta.cacheWriteInput <= delta.input ? delta : null;
254
+ }
255
+ else
256
+ usage = null;
257
+ latestUsage = total;
239
258
  }
240
- try {
241
- resolveTurn({ status: "completed", output: JSON.parse(outputItem.text), usage,
242
- usageComplete: usage !== null, dispatched: true, errorCode: null });
259
+ if (method === "item/started" || method === "item/completed") {
260
+ if (!["userMessage", "agentMessage", "reasoning", "contextCompaction"].includes(String(item.type))) {
261
+ deadline.stop("codex_tool_call");
262
+ return;
263
+ }
264
+ if (item.type === "agentMessage") {
265
+ if (item.delivery === "async" || (Array.isArray(item.questions) && item.questions.length > 0)) {
266
+ deadline.stop("codex_tool_call");
267
+ return;
268
+ }
269
+ if (method === "item/completed" && item.phase !== "commentary") {
270
+ if (typeof item.id !== "string" || typeof item.text !== "string" || Buffer.byteLength(item.text) > CODEX_MAX_OUTPUT_BYTES
271
+ || (item.phase !== null && item.phase !== "final_answer") || (item.delivery !== null && item.delivery !== undefined)
272
+ || (outputItem && (outputItem.id !== item.id || outputItem.text !== item.text))) {
273
+ deadline.stop("invalid_response");
274
+ return;
275
+ }
276
+ outputItem = { id: item.id, text: item.text };
277
+ }
278
+ }
243
279
  }
244
- catch {
245
- deadline.stop("invalid_response");
280
+ if (method === "turn/completed") {
281
+ const turn = codexRecord(params.turn);
282
+ if (turn.id !== turnId || completed) {
283
+ deadline.stop("codex_protocol_error");
284
+ return;
285
+ }
286
+ completed = true;
287
+ if (turn.status === "interrupted") {
288
+ deadline.stop("cancelled");
289
+ return;
290
+ }
291
+ if (turn.status !== "completed" || turn.error !== null || !outputItem) {
292
+ deadline.stop("invalid_response");
293
+ return;
294
+ }
295
+ try {
296
+ resolveTurn({ status: "completed", output: JSON.parse(outputItem.text), usage,
297
+ usageComplete: usage !== null && !compacted, dispatched: true, errorCode: null });
298
+ }
299
+ catch {
300
+ deadline.stop("invalid_response");
301
+ }
246
302
  }
247
- }
248
- };
249
- try {
250
- deadline.check();
251
- const file = await resolveExecutable(options, sourceEnv);
252
- deadline.check();
253
- work = await mkdtemp(path.join(options.tempRoot ?? tmpdir(), "humanish-codex-analysis-"));
254
- await chmod(work, 0o700);
255
- work = await realpath(work);
256
- const home = path.join(work, "home"), cwd = path.join(work, "cwd"), scratch = path.join(work, "scratch");
257
- for (const directory of [home, cwd, scratch])
258
- await mkdir(directory, { mode: 0o700 });
259
- const env = childEnvironment(sourceEnv, home, scratch);
260
- await checkVersion(file, env, cwd, spawnFn, deadline);
261
- const config = restrictedCodexConfig(request.model), configPath = path.join(home, "config.toml");
262
- await writeFile(configPath, config.toml, { mode: 0o600, flag: "wx" });
263
- const authHome = options.authHome ?? sourceEnv.CODEX_HOME ?? path.join(sourceEnv.HOME ?? homedir(), ".codex");
264
- if (!path.isAbsolute(authHome))
265
- throw new RestrictedCodexStop("codex_unsupported_auth");
266
- let authFile;
267
- try {
268
- authFile = await realpath(path.join(authHome, "auth.json"));
269
- if (!(await stat(authFile)).isFile())
270
- throw new Error();
271
- }
272
- catch {
273
- throw new RestrictedCodexStop("codex_login_required");
274
- }
275
- deadline.check();
276
- authLink = path.join(home, "auth.json");
277
- await symlink(authFile, authLink);
278
- deadline.check();
279
- const owned = ownCodexProcess(spawnFn(file, ["app-server", "--strict-config"], {
280
- cwd, env, detached: false, stdio: ["pipe", "pipe", "pipe"]
281
- }));
282
- // Raw input notifications echo supplied image data. Bound that wire separately
283
- // from the 2 MiB generated report, without dropping admitted evidence.
284
- const frameLimit = Math.max(CODEX_MAX_OUTPUT_BYTES, Buffer.byteLength(JSON.stringify({ instructions: request.instructions,
285
- evidence: request.evidence, images: request.images, schema: request.schema })) + 1024 * 1024);
286
- transport = new RestrictedCodexTransport(owned, deadline, frameLimit);
287
- transport.onNotification = (method, params) => {
303
+ };
304
+ const onNotification = (method, params) => {
288
305
  if (!dispatched)
289
306
  return;
290
307
  if (!hasScopedIdentity(method, params, threadId, turnId ?? earlyTurnId)) {
@@ -304,7 +321,7 @@ async function executeRestrictedCodexSession(request, options = {}, readinessOnl
304
321
  }
305
322
  const item = codexRecord(params.item);
306
323
  // Tool requests must fail even if the turn-start acknowledgment is lost.
307
- if ((method === "rawResponseItem/completed" && !["message", "reasoning"].includes(String(item.type)))
324
+ if ((method === "rawResponseItem/completed" && !allowedRawItemTypes.includes(String(item.type)))
308
325
  || (["item/started", "item/completed"].includes(method) && item.type === "agentMessage"
309
326
  && (item.delivery === "async" || (Array.isArray(item.questions) && item.questions.length > 0)))) {
310
327
  deadline.stop("codex_tool_call");
@@ -314,118 +331,166 @@ async function executeRestrictedCodexSession(request, options = {}, readinessOnl
314
331
  const value = codexRecord(params.turn).id;
315
332
  if (typeof value !== "string" || value.length === 0 || (earlyTurnId !== undefined && value !== earlyTurnId))
316
333
  deadline.stop("codex_protocol_error");
317
- else
334
+ else {
318
335
  earlyTurnId = value;
336
+ interrupt = { threadId: threadId, turnId: value };
337
+ }
319
338
  }
320
339
  if (turnId === undefined)
321
340
  early.push({ method, params });
322
341
  else
323
342
  handleTurnEvent(method, params);
324
343
  };
325
- phase = "initialize";
326
- const initialize = await transport.rpc("initialize", {
327
- clientInfo: { name: "humanish_analysis", version: "1.0.0" }, capabilities: { experimentalApi: true }
328
- });
329
- if (typeof initialize.userAgent !== "string" || !initialize.userAgent.includes(`/0.154.0 `)
330
- || initialize.codexHome !== home || initialize.platformOs !== (platform === "darwin" ? "macos" : "linux") || initialize.platformFamily !== "unix")
331
- throw new RestrictedCodexStop("codex_unsupported_version");
332
- transport.notify("initialized", {});
333
- phase = "config/read";
334
- const effective = await transport.rpc("config/read", { includeLayers: true, cwd });
335
- if (!admitsRestrictedCodexConfig(effective, configPath, request.model))
336
- throw new RestrictedCodexStop("codex_unsafe_configuration");
337
- phase = "account/read";
338
- const account = await transport.rpc("account/read", { refreshToken: false });
339
- if (account.account === null)
340
- throw new RestrictedCodexStop("codex_login_required");
341
- if (codexRecord(account.account).type !== "chatgpt" || account.requiresOpenaiAuth !== true)
342
- throw new RestrictedCodexStop("codex_unsupported_auth");
343
- phase = "thread/start";
344
- const thread = await transport.rpc("thread/start", { cwd, ephemeral: true, experimentalRawEvents: true,
345
- approvalPolicy: "never", sandbox: "read-only", model: request.model, modelProvider: "openai", allowProviderModelFallback: false,
346
- environments: [], runtimeWorkspaceRoots: [], dynamicTools: [], baseInstructions: request.instructions, config: config.overrides });
347
- if (!admitsRestrictedCodexThread(thread, request.model, cwd))
348
- throw new RestrictedCodexStop("codex_unsafe_configuration");
349
- threadId = String(codexRecord(thread.thread).id);
350
- phase = "mcpServerStatus/list";
351
- const mcp = await transport.rpc("mcpServerStatus/list", { limit: 100 });
352
- if (!Array.isArray(mcp.data) || mcp.data.length !== 0 || mcp.nextCursor !== null)
353
- throw new RestrictedCodexStop("codex_unsafe_configuration");
354
- if (readinessOnly) {
355
- result = { status: "completed", output: null, usage: null, usageComplete: false, dispatched: false, errorCode: null };
356
- }
357
- else {
358
- phase = "turn/start";
359
- const input = [{ type: "text", text: request.evidence, text_elements: [] }];
360
- for (const [index, image] of request.images.entries()) {
344
+ try {
345
+ deadline.check();
346
+ const frameLimit = Math.max(CODEX_MAX_OUTPUT_BYTES, Buffer.byteLength(JSON.stringify({ instructions: request.instructions,
347
+ evidence: request.evidence, images: request.images, schema: request.schema })) + 1024 * 1024);
348
+ if (!transport) {
349
+ const file = await resolveExecutable(options, sourceEnv);
350
+ deadline.check();
351
+ work = await mkdtemp(path.join(options.tempRoot ?? tmpdir(), "humanish-codex-analysis-"));
352
+ await chmod(work, 0o700);
353
+ work = await realpath(work);
354
+ const home = path.join(work, "home");
355
+ cwd = path.join(work, "cwd");
356
+ scratch = path.join(work, "scratch");
357
+ for (const directory of [home, cwd, scratch])
358
+ await mkdir(directory, { mode: 0o700 });
359
+ const env = childEnvironment(sourceEnv, home, scratch);
360
+ await checkVersion(file, env, cwd, spawnFn, deadline);
361
+ const config = restrictedCodexConfig(request.model), configPath = path.join(home, "config.toml");
362
+ await writeFile(configPath, config.toml, { mode: 0o600, flag: "wx" });
363
+ const authHome = options.authHome ?? sourceEnv.CODEX_HOME ?? path.join(sourceEnv.HOME ?? homedir(), ".codex");
364
+ if (!path.isAbsolute(authHome))
365
+ throw new RestrictedCodexStop("codex_unsupported_auth");
366
+ let authFile;
367
+ try {
368
+ authFile = await realpath(path.join(authHome, "auth.json"));
369
+ if (!(await stat(authFile)).isFile())
370
+ throw new Error();
371
+ }
372
+ catch {
373
+ throw new RestrictedCodexStop("codex_login_required");
374
+ }
361
375
  deadline.check();
362
- const match = CODEX_IMAGE.exec(image.dataUrl);
363
- const imagePath = path.join(scratch, `evidence-${index}.${match[1] === "jpeg" ? "jpg" : match[1]}`);
364
- await writeFile(imagePath, Buffer.from(match[2], "base64"), { mode: 0o600, flag: "wx" });
365
- input.push({ type: "text", text: JSON.stringify({ captureEvidenceId: image.evidenceId }), text_elements: [] }, { type: "localImage", path: imagePath });
376
+ authLink = path.join(home, "auth.json");
377
+ await symlink(authFile, authLink);
378
+ deadline.check();
379
+ const owned = ownCodexProcess(spawnFn(file, ["app-server", "--strict-config"], {
380
+ cwd, env, detached: false, stdio: ["pipe", "pipe", "pipe"]
381
+ }));
382
+ transport = new RestrictedCodexTransport(owned, deadline, frameLimit);
383
+ transport.onNotification = onNotification;
384
+ phase = "initialize";
385
+ const initialize = await transport.rpc("initialize", {
386
+ clientInfo: { name: "humanish_analysis", version: "1.0.0" }, capabilities: { experimentalApi: true }
387
+ });
388
+ if (typeof initialize.userAgent !== "string" || !initialize.userAgent.includes(`/0.154.0 `)
389
+ || initialize.codexHome !== home || initialize.platformOs !== (platform === "darwin" ? "macos" : "linux") || initialize.platformFamily !== "unix")
390
+ throw new RestrictedCodexStop("codex_unsupported_version");
391
+ transport.notify("initialized", {});
392
+ phase = "config/read";
393
+ const effective = await transport.rpc("config/read", { includeLayers: true, cwd });
394
+ if (!admitsRestrictedCodexConfig(effective, configPath, request.model))
395
+ throw new RestrictedCodexStop("codex_unsafe_configuration");
396
+ phase = "account/read";
397
+ const account = await transport.rpc("account/read", { refreshToken: false });
398
+ if (account.account === null)
399
+ throw new RestrictedCodexStop("codex_login_required");
400
+ if (codexRecord(account.account).type !== "chatgpt" || account.requiresOpenaiAuth !== true)
401
+ throw new RestrictedCodexStop("codex_unsupported_auth");
402
+ phase = "thread/start";
403
+ const thread = await transport.rpc("thread/start", { cwd, ephemeral: true, experimentalRawEvents: true,
404
+ approvalPolicy: "never", sandbox: "read-only", model: request.model, modelProvider: "openai", allowProviderModelFallback: false,
405
+ environments: [], runtimeWorkspaceRoots: [], dynamicTools: [], baseInstructions: request.instructions, config: config.overrides });
406
+ if (!admitsRestrictedCodexThread(thread, request.model, cwd))
407
+ throw new RestrictedCodexStop("codex_unsafe_configuration");
408
+ threadId = String(codexRecord(thread.thread).id);
409
+ phase = "mcpServerStatus/list";
410
+ const mcp = await transport.rpc("mcpServerStatus/list", { limit: 100 });
411
+ if (!Array.isArray(mcp.data) || mcp.data.length !== 0 || mcp.nextCursor !== null)
412
+ throw new RestrictedCodexStop("codex_unsafe_configuration");
413
+ identity = { model: request.model, instructions: request.instructions };
366
414
  }
367
- deadline.check();
368
- // Even a lost acknowledgment may have dispatched the request. Never claim zero cost.
369
- dispatched = true;
370
- const turn = await transport.rpc("turn/start", { threadId, cwd, approvalPolicy: "never", sandboxPolicy: { type: "readOnly" },
371
- environments: [], runtimeWorkspaceRoots: [], effort: "low", model: request.model, outputSchema: request.schema, input });
372
- const returnedTurnId = codexRecord(turn.turn).id;
373
- if (typeof returnedTurnId !== "string" || returnedTurnId.length === 0 || returnedTurnId.length > 200
374
- || (earlyTurnId !== undefined && earlyTurnId !== returnedTurnId))
375
- throw new RestrictedCodexStop("codex_protocol_error");
376
- turnId = returnedTurnId;
377
- for (const event of early)
378
- handleTurnEvent(event.method, event.params);
379
- early.length = 0;
380
- phase = "response";
381
- result = await deadline.wait(finished);
382
- deadline.check();
383
- }
384
- }
385
- catch (error) {
386
- const code = error instanceof RestrictedCodexStop ? error.code : "codex_process_failed";
387
- result = { ...restrictedCodexFailure(code === "codex_cleanup_failed" ? code : deadline.code ?? code, dispatched, usage), failurePhase: phase };
388
- }
389
- finally {
390
- // Deadline remains authoritative until acceptance; teardown has its own small grace.
391
- deadline.close();
392
- let cleaned = result.errorCode !== "codex_cleanup_failed";
393
- if (transport) {
394
- const cleanupTurn = turnId ?? earlyTurnId;
395
- cleaned = await transport.close(dispatched && !completed && threadId && cleanupTurn
396
- ? { threadId, turnId: cleanupTurn } : undefined).catch(() => false);
397
- if (!cleaned)
398
- retainUnclosedChild(transport.owned.closed);
399
- }
400
- let authReplaced = false;
401
- if (authLink && cleaned) {
402
- try {
403
- const info = await lstat(authLink);
404
- authReplaced = !info.isSymbolicLink();
405
- if (!authReplaced)
406
- await unlink(authLink);
415
+ else {
416
+ transport.beginRequest(deadline, frameLimit);
417
+ transport.onNotification = onNotification;
407
418
  }
408
- catch (error) {
409
- if (codexRecord(error).code !== "ENOENT")
410
- cleaned = false;
419
+ if (readinessOnly) {
420
+ result = { status: "completed", output: null, usage: null, usageComplete: false, dispatched: false, errorCode: null };
421
+ }
422
+ else {
423
+ phase = "turn/start";
424
+ const input = [{ type: "text", text: request.evidence, text_elements: [] }];
425
+ for (const [index, image] of request.images.entries()) {
426
+ deadline.check();
427
+ const match = CODEX_IMAGE.exec(image.dataUrl);
428
+ const imagePath = path.join(scratch, `evidence-${index}.${match[1] === "jpeg" ? "jpg" : match[1]}`);
429
+ await writeFile(imagePath, Buffer.from(match[2], "base64"), { mode: 0o600 });
430
+ input.push({ type: "text", text: JSON.stringify({ captureEvidenceId: image.evidenceId }), text_elements: [] }, { type: "localImage", path: imagePath });
431
+ }
432
+ deadline.check();
433
+ // Even a lost acknowledgment may have dispatched the request. Never claim zero cost.
434
+ dispatched = true;
435
+ const turn = await transport.rpc("turn/start", { threadId, cwd, approvalPolicy: "never", sandboxPolicy: { type: "readOnly" },
436
+ environments: [], runtimeWorkspaceRoots: [], effort: "low", model: request.model, outputSchema: request.schema, input });
437
+ const returnedTurnId = codexRecord(turn.turn).id;
438
+ if (typeof returnedTurnId !== "string" || returnedTurnId.length === 0 || returnedTurnId.length > 200
439
+ || (earlyTurnId !== undefined && earlyTurnId !== returnedTurnId))
440
+ throw new RestrictedCodexStop("codex_protocol_error");
441
+ turnId = returnedTurnId;
442
+ interrupt = { threadId: threadId, turnId };
443
+ for (const event of early)
444
+ handleTurnEvent(event.method, event.params);
445
+ early.length = 0;
446
+ phase = "response";
447
+ result = await deadline.wait(finished);
448
+ deadline.check();
449
+ previousUsage = latestUsage;
450
+ interrupt = undefined;
411
451
  }
412
452
  }
413
- // An unexpected replacement may contain rotated login state. Do not discard it
414
- // or overwrite the host login. Leave this private control-plane directory for
415
- // recovery and fail closed; no path or credential is included in the result.
416
- if (authReplaced) {
417
- cleaned = false;
418
- if (work)
419
- await preserveUnexpectedAuth(work, sourceEnv).catch(() => undefined);
453
+ catch (error) {
454
+ const code = error instanceof RestrictedCodexStop ? error.code : "codex_process_failed";
455
+ result = { ...restrictedCodexFailure(code === "codex_cleanup_failed" ? code : deadline.code ?? code, dispatched, usage), failurePhase: phase };
420
456
  }
421
- if (work && cleaned && !authReplaced)
422
- await rm(work, { recursive: true, force: true }).catch(() => { cleaned = false; });
423
- if (work && !cleaned && !authReplaced)
424
- await writeRecoveryMarker(work, sourceEnv, "process_cleanup_unconfirmed").catch(() => undefined);
425
- if (!cleaned)
426
- result = { ...restrictedCodexFailure("codex_cleanup_failed", dispatched, usage), failurePhase: "cleanup" };
457
+ finally {
458
+ deadline.close();
459
+ activeDeadline = undefined;
460
+ if (transport)
461
+ transport.onNotification = () => undefined;
462
+ if (result.errorCode !== null) {
463
+ if (result.errorCode === "codex_cleanup_failed")
464
+ cleanupTrusted = false;
465
+ if (!await dispose())
466
+ result = { ...restrictedCodexFailure("codex_cleanup_failed", dispatched, usage), failurePhase: "cleanup" };
467
+ }
468
+ }
469
+ return result.errorCode !== null && result.failurePhase === undefined ? { ...result, failurePhase: phase } : result;
427
470
  }
428
- return result.errorCode !== null && result.failurePhase === undefined ? { ...result, failurePhase: phase } : result;
471
+ return {
472
+ run(request, readinessOnly = false) {
473
+ const error = restrictedCodexRequestError(request);
474
+ if (error)
475
+ return Promise.resolve(restrictedCodexFailure(error));
476
+ if (closed || (identity && (identity.model !== request.model || identity.instructions !== request.instructions)))
477
+ return Promise.resolve(restrictedCodexFailure("invalid_request"));
478
+ if (pending || unclosedChildren.size)
479
+ return Promise.resolve(restrictedCodexFailure("codex_busy"));
480
+ if (!(platform === "linux" && arch === "x64") && !(platform === "darwin" && arch === "arm64"))
481
+ return Promise.resolve(restrictedCodexFailure("codex_unsupported_platform"));
482
+ const task = execute(request, readinessOnly);
483
+ pending = task;
484
+ void task.finally(() => { if (pending === task)
485
+ pending = undefined; }).catch(() => undefined);
486
+ return task;
487
+ },
488
+ close() {
489
+ closed = true;
490
+ activeDeadline?.stop("cancelled");
491
+ return closing ??= (async () => { await pending; return dispose(); })();
492
+ }
493
+ };
429
494
  }
430
495
  export async function checkRestrictedCodexSessionReadiness(input = {}, options = {}) {
431
496
  return runRestrictedCodexSession({ model: RESTRICTED_CODEX_ANALYSIS_MODELS[0], instructions: "Analyze only supplied study evidence.",