import { M as state, O as getEnvString, f as normalizeFieldName, l as REDACTED, m as sanitizeObject, o as logger } from "./logger-CtGm8dC_.js";
import { U as VERSION } from "./fetch-AchwhzAC.js";
import { t as providerRegistry } from "./providerRegistry-HCXJuM3_.js";
import { gt as withGenAISpan, lt as closeTurnSpan, mt as openTurnSpan, pt as getTraceparent, rt as renderVarsInObject } from "./util-C2QcRmEx.js";
import { o as resolveAgenticWorkingDir } from "./agentic-utils-BNNuEjFF.js";
import { r as calculateOpenAIUsageCostFromTokenUsage } from "./billing-C9iWQIlz.js";
import { a as applyApiKeyToCliEnv, i as getCodexSkillRootPrefixes, o as shouldInjectApiKey, r as getCodexSkillMetadataFields, t as buildCodexSkillMetadata } from "./codexSkillMetadata-D_d-NoPI.js";
import { z } from "zod";
import path from "path";
import fs from "fs";
import dedent from "dedent";
import crypto from "crypto";
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
import { spawn } from "child_process";
import readline from "readline";
//#region src/providers/openai/codex-app-server.ts
const MAX_BUFFERED_JSON_RPC_CHARS = 5e6;
const DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
const DEFAULT_STARTUP_TIMEOUT_MS = 3e4;
const MINIMAL_CLI_ENV_KEYS = [
	"PATH",
	"Path",
	"HOME",
	"USER",
	"USERNAME",
	"USERPROFILE",
	"TMPDIR",
	"TMP",
	"TEMP",
	"SHELL",
	"COMSPEC",
	"SystemRoot",
	"PATHEXT",
	"LANG",
	"LC_ALL",
	"TERM"
];
const COMMON_OPTIONAL_PROCESS_ENV_KEYS = [
	"CODEX_HOME",
	"HTTP_PROXY",
	"HTTPS_PROXY",
	"ALL_PROXY",
	"NO_PROXY",
	"SSL_CERT_FILE",
	"SSL_CERT_DIR",
	"REQUESTS_CA_BUNDLE",
	"NODE_EXTRA_CA_CERTS",
	"SSH_AUTH_SOCK",
	"GIT_SSH_COMMAND"
];
const CodexCliEnvValueSchema = z.union([
	z.string(),
	z.number(),
	z.boolean()
]).transform(String);
const CodexAppServerReasoningEffortSchema = z.enum([
	"none",
	"minimal",
	"low",
	"medium",
	"high",
	"xhigh"
]);
const CodexAppServerGranularApprovalPolicySchema = z.object({ granular: z.object({
	sandbox_approval: z.boolean(),
	rules: z.boolean(),
	skill_approval: z.boolean(),
	request_permissions: z.boolean(),
	mcp_elicitations: z.boolean()
}).strict() }).strict();
const CodexAppServerApprovalPolicySchema = z.union([z.enum([
	"never",
	"on-request",
	"on-failure",
	"untrusted"
]), CodexAppServerGranularApprovalPolicySchema]);
const CommandExecutionApprovalDecisionSchema = z.union([
	z.enum([
		"accept",
		"acceptForSession",
		"decline",
		"cancel"
	]),
	z.object({ acceptWithExecpolicyAmendment: z.object({ execpolicy_amendment: z.array(z.string()) }).strict() }).strict(),
	z.object({ applyNetworkPolicyAmendment: z.object({ network_policy_amendment: z.object({
		host: z.string().min(1),
		action: z.enum(["allow", "deny"])
	}).strict() }).strict() }).strict()
]);
const McpElicitationPolicySchema = z.union([z.enum([
	"accept",
	"decline",
	"cancel"
]), z.object({
	action: z.enum([
		"accept",
		"decline",
		"cancel"
	]),
	content: z.unknown().optional(),
	_meta: z.unknown().optional()
}).strict()]);
const CollaborationModeSchema = z.object({
	mode: z.enum(["plan", "default"]),
	settings: z.object({
		model: z.string().min(1),
		reasoning_effort: CodexAppServerReasoningEffortSchema.nullable(),
		developer_instructions: z.string().nullable()
	}).strict()
}).strict();
const ServerRequestPolicySchema = z.object({
	command_execution: CommandExecutionApprovalDecisionSchema.optional(),
	file_change: z.enum([
		"accept",
		"acceptForSession",
		"decline",
		"cancel"
	]).optional(),
	permissions: z.object({
		permissions: z.record(z.string(), z.unknown()).optional(),
		scope: z.enum(["turn", "session"]).optional(),
		strict_auto_review: z.boolean().nullable().optional()
	}).optional(),
	user_input: z.union([z.enum(["empty", "first-option"]), z.record(z.string(), z.union([z.string(), z.array(z.string())]))]).optional(),
	mcp_elicitation: McpElicitationPolicySchema.optional(),
	dynamic_tools: z.record(z.string(), z.object({
		success: z.boolean().optional(),
		text: z.string().optional(),
		contentItems: z.array(z.union([z.object({
			type: z.literal("inputText"),
			text: z.string()
		}), z.object({
			type: z.literal("inputImage"),
			imageUrl: z.string()
		})])).optional()
	})).optional()
}).strict();
const CodexAppServerConfigShape = {
	basePath: z.string().optional(),
	prefix: z.string().optional(),
	suffix: z.string().optional(),
	provider: z.unknown().optional(),
	linkedTargetId: z.string().optional(),
	apiKey: z.string().min(1).optional(),
	base_url: z.string().min(1).optional(),
	working_dir: z.string().min(1).optional(),
	additional_directories: z.array(z.string().min(1)).optional(),
	skip_git_repo_check: z.boolean().optional(),
	codex_path_override: z.string().min(1).optional(),
	model: z.string().min(1).optional(),
	model_provider: z.string().min(1).optional(),
	service_tier: z.enum(["fast", "flex"]).optional(),
	sandbox_mode: z.enum([
		"read-only",
		"workspace-write",
		"danger-full-access"
	]).optional(),
	sandbox_policy: z.record(z.string(), z.unknown()).optional(),
	network_access_enabled: z.boolean().optional(),
	approval_policy: CodexAppServerApprovalPolicySchema.optional(),
	approvals_reviewer: z.enum([
		"user",
		"auto_review",
		"guardian_subagent"
	]).optional(),
	model_reasoning_effort: CodexAppServerReasoningEffortSchema.optional(),
	reasoning_summary: z.enum([
		"auto",
		"concise",
		"detailed",
		"none"
	]).optional(),
	personality: z.enum([
		"none",
		"friendly",
		"pragmatic"
	]).optional(),
	base_instructions: z.string().min(1).optional(),
	developer_instructions: z.string().min(1).optional(),
	collaboration_mode: CollaborationModeSchema.optional(),
	output_schema: z.record(z.string(), z.unknown()).optional(),
	thread_id: z.string().min(1).optional(),
	persist_threads: z.boolean().optional(),
	thread_pool_size: z.number().int().positive().optional(),
	thread_cleanup: z.enum([
		"unsubscribe",
		"archive",
		"none"
	]).optional(),
	ephemeral: z.boolean().optional(),
	persist_extended_history: z.boolean().optional(),
	experimental_raw_events: z.boolean().optional(),
	experimental_api: z.boolean().optional(),
	include_raw_events: z.boolean().optional(),
	cli_config: z.record(z.string(), z.unknown()).optional(),
	cli_env: z.record(z.string(), CodexCliEnvValueSchema).optional(),
	inherit_process_env: z.boolean().optional(),
	reuse_server: z.boolean().optional(),
	deep_tracing: z.boolean().optional(),
	request_timeout_ms: z.number().int().positive().optional(),
	startup_timeout_ms: z.number().int().positive().optional(),
	turn_timeout_ms: z.number().int().positive().optional(),
	server_request_policy: ServerRequestPolicySchema.optional()
};
const CodexAppServerConfigSchema = z.object(CodexAppServerConfigShape).strict();
const CodexAppServerMergedPromptConfigSchema = z.object(CodexAppServerConfigShape).strip();
function createDeferred() {
	let resolve;
	let reject;
	return {
		promise: new Promise((promiseResolve, promiseReject) => {
			resolve = promiseResolve;
			reject = promiseReject;
		}),
		resolve,
		reject
	};
}
function parseCodexAppServerConfig(config, options = {}) {
	const schema = options.stripUnknownKeys ? CodexAppServerMergedPromptConfigSchema : CodexAppServerConfigSchema;
	try {
		return schema.parse(config ?? {});
	} catch (error) {
		if (error instanceof z.ZodError) {
			const issues = error.issues.map((issue) => {
				return `${issue.path.length > 0 ? issue.path.join(".") : "(root)"}: ${issue.message}`;
			}).join("; ");
			throw new Error(`Invalid OpenAI Codex app-server config: ${issues}`);
		}
		throw error;
	}
}
function mergeOptionalRecord(base, override) {
	if (!base && !override) return;
	return {
		...base ?? {},
		...override ?? {}
	};
}
function mergeServerRequestPolicy(base, override) {
	if (!base && !override) return;
	const merged = {
		...base ?? {},
		...override ?? {}
	};
	const permissions = mergeOptionalRecord(base?.permissions, override?.permissions);
	const dynamicTools = mergeOptionalRecord(base?.dynamic_tools, override?.dynamic_tools);
	if (permissions) merged.permissions = permissions;
	else delete merged.permissions;
	if (dynamicTools) merged.dynamic_tools = dynamicTools;
	else delete merged.dynamic_tools;
	return merged;
}
function mergeCodexAppServerConfig(base, override) {
	if (!override) return { ...base };
	return {
		...base,
		...override,
		cli_config: mergeOptionalRecord(base.cli_config, override.cli_config),
		cli_env: mergeOptionalRecord(base.cli_env, override.cli_env),
		server_request_policy: mergeServerRequestPolicy(base.server_request_policy, override.server_request_policy)
	};
}
function getMinimalProcessEnv() {
	const env = {};
	for (const key of MINIMAL_CLI_ENV_KEYS) {
		const value = process.env[key];
		if (typeof value === "string" && value.length > 0) env[key] = value;
	}
	return env;
}
function isPlainObject(value) {
	return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function flattenConfig(value, prefix = "") {
	const entries = [];
	for (const [key, entryValue] of Object.entries(value)) {
		const nextKey = prefix ? `${prefix}.${key}` : key;
		if (isPlainObject(entryValue)) entries.push(...flattenConfig(entryValue, nextKey));
		else if (entryValue !== void 0) entries.push({
			key: nextKey,
			value: entryValue
		});
	}
	return entries;
}
function toTomlLiteral(value) {
	if (typeof value === "string") return JSON.stringify(value);
	if (typeof value === "number" || typeof value === "boolean") return String(value);
	if (Array.isArray(value)) return `[${value.map((item) => toTomlLiteral(item)).join(", ")}]`;
	if (value === null) return "\"\"";
	return JSON.stringify(value);
}
function getJsonRpcErrorMessage(message) {
	if (typeof message.error?.message === "string" && message.error.message) return message.error.message;
	return "Unknown app-server error";
}
var JsonRpcError = class extends Error {
	code;
	constructor(message, code) {
		super(message);
		this.name = "JsonRpcError";
		this.code = code;
	}
};
function isMethodNotFoundError(error) {
	return error instanceof JsonRpcError && error.code === -32601;
}
function createAbortError(message) {
	const error = new Error(message);
	error.name = "AbortError";
	return error;
}
var CodexAppServerConnection = class {
	instanceId;
	process;
	lineInterface;
	nextRequestId = 1;
	pending = /* @__PURE__ */ new Map();
	closed = false;
	stderrChunks = [];
	stderrTotalLength = 0;
	bufferedJsonRpcLines = [];
	closePromise = null;
	constructor(options) {
		this.options = options;
		this.instanceId = options.connectionInstanceId;
		this.process = spawn(options.command, options.args, {
			env: options.env,
			stdio: [
				"pipe",
				"pipe",
				"pipe"
			]
		});
		this.lineInterface = readline.createInterface({ input: this.process.stdout });
		this.lineInterface.on("line", (line) => this.handleLine(line));
		this.process.stderr.on("data", (chunk) => this.recordStderr(chunk));
		this.process.stdin.on("error", (error) => {
			logger.debug("[CodexAppServer] stdin error", { error: error.message });
		});
		this.process.on("error", (error) => this.handleProcessFailure(error));
		this.process.on("exit", (code, signal) => {
			if (this.closed) this.rejectPending(/* @__PURE__ */ new Error("codex app-server process closed"));
			else this.handleProcessFailure(/* @__PURE__ */ new Error(`codex app-server exited with code ${code ?? "null"} signal ${signal ?? "null"}`));
		});
	}
	async initialize(config) {
		await this.request("initialize", {
			clientInfo: {
				name: "promptfoo_codex_app_server",
				title: "Promptfoo Codex App Server Provider",
				version: VERSION
			},
			capabilities: { experimentalApi: config.experimental_api ?? true }
		}, { timeoutMs: this.options.startupTimeoutMs });
		this.notify("initialized", {});
	}
	request(method, params, options = {}) {
		if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("codex app-server connection is closed"));
		if (options.abortSignal?.aborted) return Promise.reject(createAbortError(`codex app-server request aborted: ${method}`));
		const id = this.nextRequestId++;
		const message = {
			method,
			id
		};
		if (params !== void 0) message.params = params;
		const timeoutMs = options.timeoutMs ?? this.options.requestTimeoutMs;
		return new Promise((resolve, reject) => {
			const pendingRequest = {
				method,
				resolve,
				reject,
				abortSignal: options.abortSignal,
				onResponse: options.onResponse
			};
			pendingRequest.timeout = setTimeout(() => {
				const error = /* @__PURE__ */ new Error(`codex app-server request timed out: ${method}`);
				this.pending.delete(id);
				if (options.abortSignal && pendingRequest.abortListener) options.abortSignal.removeEventListener("abort", pendingRequest.abortListener);
				reject(error);
				this.closeAfterRequestTimeout(method, error);
			}, timeoutMs);
			if (options.abortSignal) {
				pendingRequest.abortListener = () => {
					const error = createAbortError(`codex app-server request aborted: ${method}`);
					this.pending.delete(id);
					if (pendingRequest.timeout) clearTimeout(pendingRequest.timeout);
					reject(error);
					logger.debug("[CodexAppServer] JSON-RPC request aborted", {
						error: error.message,
						method
					});
				};
				options.abortSignal.addEventListener("abort", pendingRequest.abortListener, { once: true });
			}
			this.pending.set(id, pendingRequest);
			this.send(message);
		});
	}
	notify(method, params) {
		const message = { method };
		if (params !== void 0) message.params = params;
		this.send(message);
	}
	getStderr() {
		return this.stderrChunks.join("").slice(-1e4);
	}
	async close() {
		if (this.closePromise !== null) return this.closePromise;
		this.closePromise = new Promise((resolve) => {
			this.closed = true;
			this.rejectPending(/* @__PURE__ */ new Error("codex app-server connection closed"));
			this.lineInterface.close();
			const finish = () => resolve();
			const killTimer = setTimeout(() => {
				try {
					if (!this.process.killed) this.process.kill("SIGKILL");
				} catch {}
				finish();
			}, 1e3);
			this.process.once("exit", () => {
				clearTimeout(killTimer);
				finish();
			});
			if (this.process.killed || this.process.exitCode !== null) {
				clearTimeout(killTimer);
				finish();
				return;
			}
			try {
				this.process.stdin.end();
				this.process.kill("SIGTERM");
			} catch {
				clearTimeout(killTimer);
				finish();
			}
		});
		return this.closePromise;
	}
	handleLine(line) {
		const trimmed = line.trim();
		if (!trimmed && this.bufferedJsonRpcLines.length === 0) return;
		const candidateLines = this.bufferedJsonRpcLines.length > 0 ? [...this.bufferedJsonRpcLines, line] : [trimmed];
		const candidate = candidateLines.join("\\n");
		let message;
		try {
			message = JSON.parse(candidate);
		} catch (error) {
			if (this.shouldBufferJsonRpcLine(error, trimmed)) {
				this.bufferedJsonRpcLines = candidateLines;
				if (candidate.length > MAX_BUFFERED_JSON_RPC_CHARS) {
					logger.warn("[CodexAppServer] Dropping oversized partial JSON-RPC message", {
						error,
						bufferedChars: candidate.length
					});
					this.bufferedJsonRpcLines = [];
				}
				return;
			}
			logger.warn("[CodexAppServer] Failed to parse JSON-RPC line", {
				error,
				line: candidate
			});
			this.bufferedJsonRpcLines = [];
			return;
		}
		this.bufferedJsonRpcLines = [];
		this.handleMessage(message);
	}
	shouldBufferJsonRpcLine(error, trimmedLine) {
		if (this.bufferedJsonRpcLines.length > 0) return true;
		if (!trimmedLine.startsWith("{")) return false;
		const message = error instanceof Error ? error.message : String(error);
		return /Unterminated string|Unexpected end of JSON input|Bad control character/i.test(message);
	}
	handleMessage(message) {
		if (message.id !== void 0 && (message.result !== void 0 || message.error !== void 0)) {
			this.handleResponse(message);
			return;
		}
		if (message.id !== void 0 && message.method) {
			this.handleServerRequest(message);
			return;
		}
		if (message.method) {
			this.options.onNotification(message);
			return;
		}
		logger.debug("[CodexAppServer] Ignoring unknown JSON-RPC message shape", { message });
	}
	handleResponse(message) {
		const pendingRequest = this.pending.get(message.id);
		if (!pendingRequest) {
			logger.debug("[CodexAppServer] Received response for unknown request", { id: message.id });
			return;
		}
		this.pending.delete(message.id);
		if (pendingRequest.timeout) clearTimeout(pendingRequest.timeout);
		if (pendingRequest.abortSignal && pendingRequest.abortListener) pendingRequest.abortSignal.removeEventListener("abort", pendingRequest.abortListener);
		if (message.error) {
			pendingRequest.reject(new JsonRpcError(getJsonRpcErrorMessage(message), message.error.code));
			return;
		}
		try {
			pendingRequest.onResponse?.(message.result);
		} catch (error) {
			pendingRequest.reject(error instanceof Error ? error : new Error(String(error)));
			return;
		}
		pendingRequest.resolve(message.result);
	}
	async handleServerRequest(message) {
		try {
			const result = await this.options.onServerRequest(message);
			this.send({
				id: message.id,
				result
			});
		} catch (error) {
			const errorMessage = error instanceof Error ? error.message : String(error);
			try {
				this.send({
					id: message.id,
					error: {
						code: -32e3,
						message: errorMessage
					}
				});
			} catch (sendError) {
				logger.debug("[CodexAppServer] Failed to send error response for server request", {
					error: sendError,
					originalError: errorMessage,
					method: message.method
				});
			}
		}
	}
	recordStderr(chunk) {
		const text = chunk.toString("utf8");
		this.stderrChunks.push(text);
		this.stderrTotalLength += text.length;
		if (this.stderrTotalLength > 2e4) {
			const truncated = this.getStderr();
			this.stderrChunks = [truncated];
			this.stderrTotalLength = truncated.length;
		}
		logger.debug("[CodexAppServer] stderr", { text });
	}
	handleProcessFailure(error) {
		if (this.closed) return;
		this.closed = true;
		this.rejectPending(error);
		logger.error("[CodexAppServer] Process failure", {
			error: error.message,
			stderr: this.getStderr()
		});
		this.options.onClose(error);
	}
	closeAfterRequestTimeout(method, error) {
		if (this.closed) return;
		logger.warn("[CodexAppServer] Closing app-server after JSON-RPC request timeout", {
			error: error.message,
			method
		});
		this.options.onClose(error);
		this.close().catch((closeError) => {
			logger.debug("[CodexAppServer] Error closing app-server after request timeout", { error: closeError });
		});
	}
	rejectPending(error) {
		for (const [id, pendingRequest] of this.pending) {
			if (pendingRequest.timeout) clearTimeout(pendingRequest.timeout);
			if (pendingRequest.abortSignal && pendingRequest.abortListener) pendingRequest.abortSignal.removeEventListener("abort", pendingRequest.abortListener);
			pendingRequest.reject(error);
			this.pending.delete(id);
		}
	}
	send(message) {
		if (this.closed) throw new Error("codex app-server connection is closed");
		this.process.stdin.write(`${JSON.stringify(message)}\n`);
	}
};
var OpenAICodexAppServerProvider = class {
	config;
	env;
	apiKey;
	providerId = "openai:codex-app-server";
	connections = /* @__PURE__ */ new Map();
	connectionPromises = /* @__PURE__ */ new Map();
	initializingConnections = /* @__PURE__ */ new Set();
	threads = /* @__PURE__ */ new Map();
	threadPromises = /* @__PURE__ */ new Map();
	threadPromiseConnectionInstances = /* @__PURE__ */ new Map();
	protectedThreadCounts = /* @__PURE__ */ new Map();
	threadRunQueues = /* @__PURE__ */ new Map();
	activeTurnsByThread = /* @__PURE__ */ new Map();
	activeTurnsByTurn = /* @__PURE__ */ new Map();
	validatedWorkingDirs = /* @__PURE__ */ new Set();
	ignoredProviderEnvWarningShown = false;
	omittedProcessEnvWarningShown = false;
	deepTracingWarningShown = false;
	constructor(options = {}) {
		this.config = parseCodexAppServerConfig(options.config);
		this.env = options.env;
		this.apiKey = this.getApiKey();
		this.providerId = options.id ?? this.providerId;
		providerRegistry.register(this);
	}
	id() {
		return this.providerId;
	}
	getApiKey(config = this.config) {
		return config.apiKey || this.env?.OPENAI_API_KEY || this.env?.CODEX_API_KEY || getEnvString("OPENAI_API_KEY") || getEnvString("CODEX_API_KEY");
	}
	requiresApiKey() {
		return false;
	}
	toString() {
		return "[OpenAI Codex App Server Provider]";
	}
	async cleanup() {
		this.resolveActiveTurns(/* @__PURE__ */ new Error("codex app-server provider cleanup interrupted active turn"));
		this.threads.clear();
		this.threadPromises.clear();
		this.threadPromiseConnectionInstances.clear();
		this.threadRunQueues.clear();
		this.activeTurnsByThread.clear();
		this.activeTurnsByTurn.clear();
		this.protectedThreadCounts.clear();
		this.validatedWorkingDirs.clear();
		const connections = Array.from(new Set([...this.connections.values(), ...this.initializingConnections]));
		this.connections.clear();
		this.connectionPromises.clear();
		this.initializingConnections.clear();
		await Promise.all(connections.map((connection) => connection.close().catch((error) => {
			logger.warn("[CodexAppServer] Error during cleanup", { error });
		})));
	}
	async shutdown() {
		try {
			await this.cleanup();
		} finally {
			providerRegistry.unregister(this);
		}
	}
	async callApi(prompt, context, callOptions) {
		const config = renderVarsInObject(mergeCodexAppServerConfig(this.config, context?.prompt?.config), context?.vars);
		const requestedModel = typeof config.model === "string" && config.model ? config.model : void 0;
		return withGenAISpan(this.buildSpanContext(prompt, context, requestedModel), () => this.callApiInternal(prompt, context, callOptions, config), (response) => this.extractSpanResult(response, requestedModel));
	}
	buildSpanContext(prompt, context, requestedModel) {
		return {
			system: "openai",
			operationName: "chat",
			model: requestedModel ?? "codex-app-server",
			providerId: this.id(),
			evalId: context?.evaluationId || context?.test?.metadata?.evaluationId,
			testIndex: typeof context?.test?.vars?.__testIdx === "number" ? context.test.vars.__testIdx : void 0,
			promptLabel: context?.prompt?.label,
			traceparent: context?.traceparent,
			requestBody: prompt
		};
	}
	extractSpanResult(response, requestedModel) {
		const result = {};
		if (response.tokenUsage) result.tokenUsage = response.tokenUsage;
		if (response.sessionId) result.responseId = response.sessionId;
		if (requestedModel) result.responseModel = requestedModel;
		if (response.cached !== void 0) result.cacheHit = response.cached;
		if (response.output !== void 0) result.responseBody = typeof response.output === "string" ? response.output : JSON.stringify(response.output);
		if (response.metadata?.codexAppServer?.itemCounts) result.additionalAttributes = {
			...result.additionalAttributes,
			"codex.app_server.items.breakdown": JSON.stringify(response.metadata.codexAppServer.itemCounts)
		};
		return result;
	}
	async callApiInternal(prompt, context, callOptions, rawConfig) {
		let config;
		try {
			config = parseCodexAppServerConfig(rawConfig, { stripUnknownKeys: true });
		} catch (error) {
			const errorMessage = error instanceof Error ? error.message : String(error);
			logger.error("Error calling OpenAI Codex app-server", { error: errorMessage });
			return { error: `Error calling OpenAI Codex app-server: ${errorMessage}` };
		}
		if (callOptions?.abortSignal?.aborted) return { error: "OpenAI Codex app-server call aborted before it started" };
		const workingDirectory = this.resolveWorkingDirectory(config);
		const resolvedConfig = {
			approval_policy: "never",
			sandbox_mode: "read-only",
			network_access_enabled: false,
			ephemeral: true,
			thread_cleanup: "unsubscribe",
			reuse_server: true,
			...config,
			working_dir: workingDirectory,
			additional_directories: this.resolveAdditionalDirectories(config)
		};
		const currentTraceparent = getTraceparent();
		const apiKey = this.getApiKey(resolvedConfig);
		const env = this.prepareEnvironment(resolvedConfig, currentTraceparent, apiKey);
		const promptInput = this.parsePromptInput(prompt);
		const connectionKey = this.generateConnectionKey(env, resolvedConfig);
		const useReusableConnection = resolvedConfig.reuse_server !== false && !resolvedConfig.deep_tracing;
		let localConnection;
		try {
			this.validateWorkingDirectory(resolvedConfig.working_dir, resolvedConfig.skip_git_repo_check);
			this.warnOnceForDeepTracingThreadOptions(resolvedConfig);
			const connection = useReusableConnection ? await this.getOrCreateConnection(connectionKey, env, resolvedConfig) : await this.createConnection(connectionKey, env, resolvedConfig);
			if (!useReusableConnection) localConnection = connection;
			const promptCacheBasis = context?.prompt?.raw ?? prompt;
			const threadHandle = await this.getOrCreateThread(connection, connectionKey, promptCacheBasis, resolvedConfig, useReusableConnection, callOptions);
			this.protectThread(threadHandle.threadId);
			let turnStarted = false;
			try {
				const queueKey = this.getThreadRunQueueKey(resolvedConfig, threadHandle);
				return await this.runSerializedThreadTurn(queueKey, callOptions?.abortSignal, async () => {
					turnStarted = true;
					const state = this.createTurnState(connectionKey, connection.instanceId, threadHandle.threadId, promptInput, resolvedConfig, env);
					this.registerTurnState(state);
					try {
						const turnResponse = await connection.request("turn/start", this.buildTurnStartParams(threadHandle.threadId, promptInput, resolvedConfig), {
							abortSignal: callOptions?.abortSignal,
							timeoutMs: this.getRequestTimeoutMs(resolvedConfig),
							onResponse: (response) => {
								const turnId = response?.turn?.id;
								if (typeof turnId === "string") this.updateTurnStateId(state, turnId);
							}
						});
						if (typeof turnResponse?.turn?.id === "string") this.updateTurnStateId(state, turnResponse.turn.id);
						await this.waitForTurnCompletion(connection, state, resolvedConfig, callOptions);
						return this.buildProviderResponse(state, threadHandle, resolvedConfig);
					} finally {
						this.unregisterTurnState(state);
						await this.cleanupThreadAfterTurn(connection, threadHandle, resolvedConfig);
					}
				});
			} finally {
				this.unprotectThread(threadHandle.threadId);
				if (!turnStarted) await this.cleanupThreadAfterTurn(connection, threadHandle, resolvedConfig, { skipIfActiveTurn: true });
				this.scheduleThreadPoolEnforcement(connection, connectionKey, resolvedConfig);
			}
		} catch (error) {
			if (error instanceof Error && error.name === "AbortError" || callOptions?.abortSignal?.aborted) {
				logger.warn("OpenAI Codex app-server call aborted");
				return { error: "OpenAI Codex app-server call aborted" };
			}
			const errorMessage = error instanceof Error ? error.message : String(error);
			logger.error("Error calling OpenAI Codex app-server", { error: errorMessage });
			return { error: `Error calling OpenAI Codex app-server: ${errorMessage}` };
		} finally {
			if (localConnection) await localConnection.close().catch((error) => {
				logger.debug("[CodexAppServer] Error closing local connection", { error });
			});
		}
	}
	resolveWorkingDirectory(config) {
		const basePath = config.basePath || state.basePath || process.cwd();
		if (!config.working_dir) return process.cwd();
		return resolveAgenticWorkingDir(config.working_dir, basePath) ?? process.cwd();
	}
	resolveAdditionalDirectories(config) {
		if (!config.additional_directories?.length) return;
		const basePath = config.basePath || state.basePath || process.cwd();
		return config.additional_directories.map((directory) => path.resolve(basePath, directory));
	}
	prepareEnvironment(config, traceparent, apiKey = this.getApiKey(config)) {
		const inheritProcessEnv = config.inherit_process_env === true;
		const cliEnv = Object.fromEntries(Object.entries(config.cli_env ?? {}).map(([key, value]) => [key, String(value)]));
		const env = {
			...inheritProcessEnv ? process.env : getMinimalProcessEnv(),
			...cliEnv
		};
		const ignoredProviderEnvKeys = Object.keys(this.env ?? {}).filter((key) => key !== "OPENAI_API_KEY" && key !== "CODEX_API_KEY" && !(key in (config.cli_env ?? {}))).sort();
		if (ignoredProviderEnvKeys.length > 0 && !this.ignoredProviderEnvWarningShown) {
			logger.warn("[CodexAppServer] Ignoring promptfoo-level env overrides for the Codex app-server process. Move these keys into config.cli_env if Codex shell commands need them.", { envKeys: ignoredProviderEnvKeys });
			this.ignoredProviderEnvWarningShown = true;
		}
		if (!inheritProcessEnv && !this.omittedProcessEnvWarningShown) {
			const omittedProcessEnvKeys = this.getOmittedOptionalProcessEnvKeys(config, env);
			if (omittedProcessEnvKeys.length > 0) {
				logger.warn("[CodexAppServer] Optional Codex app-server process env vars are not inherited by default. Move these keys into config.cli_env or set inherit_process_env: true if Codex commands need them.", { envKeys: omittedProcessEnvKeys });
				this.omittedProcessEnvWarningShown = true;
			}
		}
		const sortedEnv = {};
		for (const key of Object.keys(env).sort()) if (env[key] !== void 0) sortedEnv[key] = env[key];
		applyApiKeyToCliEnv(sortedEnv, config, apiKey);
		if (config.base_url) {
			sortedEnv.OPENAI_BASE_URL = config.base_url;
			sortedEnv.OPENAI_API_BASE_URL = config.base_url;
		}
		if (config.deep_tracing) {
			if (!sortedEnv.OTEL_EXPORTER_OTLP_ENDPOINT) sortedEnv.OTEL_EXPORTER_OTLP_ENDPOINT = "http://127.0.0.1:4318";
			if (!sortedEnv.OTEL_EXPORTER_OTLP_PROTOCOL) sortedEnv.OTEL_EXPORTER_OTLP_PROTOCOL = "http/json";
			if (!sortedEnv.OTEL_SERVICE_NAME) sortedEnv.OTEL_SERVICE_NAME = "codex-app-server";
			if (!sortedEnv.OTEL_TRACES_EXPORTER) sortedEnv.OTEL_TRACES_EXPORTER = "otlp";
			if (traceparent) sortedEnv.TRACEPARENT = traceparent;
		} else delete sortedEnv.TRACEPARENT;
		return sortedEnv;
	}
	getOmittedOptionalProcessEnvKeys(config, env) {
		const shouldWarnForSshEnv = config.network_access_enabled === true;
		return COMMON_OPTIONAL_PROCESS_ENV_KEYS.filter((key) => typeof process.env[key] === "string" && !(key in env) && (shouldWarnForSshEnv || key !== "SSH_AUTH_SOCK" && key !== "GIT_SSH_COMMAND"));
	}
	buildAppServerArgs(config) {
		const args = [
			"app-server",
			"--listen",
			"stdio://"
		];
		const cliConfig = this.getResolvedCliConfig(config);
		for (const { key, value } of flattenConfig(cliConfig)) args.push("-c", `${key}=${toTomlLiteral(value)}`);
		return args;
	}
	getResolvedCliConfig(config) {
		return { ...config.cli_config ?? {} };
	}
	getRequestTimeoutMs(config) {
		return config.request_timeout_ms ?? DEFAULT_REQUEST_TIMEOUT_MS;
	}
	async getOrCreateConnection(connectionKey, env, config) {
		const existing = this.connections.get(connectionKey);
		if (existing) return existing;
		const pending = this.connectionPromises.get(connectionKey);
		if (pending) return pending;
		let connectionPromise;
		connectionPromise = this.createConnection(connectionKey, env, config).then((connection) => {
			if (this.connectionPromises.get(connectionKey) !== connectionPromise) {
				connection.close().catch((error) => {
					logger.debug("[CodexAppServer] Error closing stale initialized connection", { error });
				});
				throw new Error("codex app-server connection was closed during cleanup");
			}
			this.connections.set(connectionKey, connection);
			return connection;
		}).finally(() => {
			this.connectionPromises.delete(connectionKey);
		});
		this.connectionPromises.set(connectionKey, connectionPromise);
		return connectionPromise;
	}
	async createConnection(connectionKey, env, config) {
		const connectionInstanceId = `${connectionKey}:${crypto.randomUUID()}`;
		const connection = new CodexAppServerConnection({
			connectionInstanceId,
			command: config.codex_path_override ?? "codex",
			args: this.buildAppServerArgs(config),
			env,
			requestTimeoutMs: this.getRequestTimeoutMs(config),
			startupTimeoutMs: config.startup_timeout_ms ?? DEFAULT_STARTUP_TIMEOUT_MS,
			onNotification: (message) => this.handleNotification(message),
			onServerRequest: (message) => this.handleServerRequest(message, config),
			onClose: (error) => this.handleConnectionClose(connectionKey, connectionInstanceId, error)
		});
		this.initializingConnections.add(connection);
		try {
			await connection.initialize(config);
			const apiKey = this.getApiKey(config);
			if (shouldInjectApiKey(config, apiKey)) try {
				await connection.request("account/login/start", {
					type: "apiKey",
					apiKey
				}, { timeoutMs: this.getRequestTimeoutMs(config) });
			} catch (error) {
				if (!isMethodNotFoundError(error)) throw error;
				logger.debug("[CodexAppServer] account/login/start is unavailable; continuing with API key env auth");
			}
			return connection;
		} catch (error) {
			await connection.close().catch((closeError) => {
				logger.debug("[CodexAppServer] Error closing app-server after failed initialization", { error: closeError });
			});
			throw error;
		} finally {
			this.initializingConnections.delete(connection);
		}
	}
	handleConnectionClose(connectionKey, connectionInstanceId, error) {
		logger.warn("[CodexAppServer] Connection closed", {
			connectionKey,
			error: error.message
		});
		if (this.connections.get(connectionKey)?.instanceId === connectionInstanceId) {
			this.connections.delete(connectionKey);
			this.connectionPromises.delete(connectionKey);
			for (const [threadCacheKey, handle] of this.threads) if (handle.connectionKey === connectionKey) this.threads.delete(threadCacheKey);
			for (const [threadCacheKey] of this.threadPromises) if (this.threadPromiseConnectionInstances.get(threadCacheKey) === connectionInstanceId) {
				this.threadPromises.delete(threadCacheKey);
				this.threadPromiseConnectionInstances.delete(threadCacheKey);
			}
		}
		this.resolveActiveTurns(error, (state) => state.connectionInstanceId === connectionInstanceId);
	}
	resolveActiveTurns(error, predicate = () => true) {
		for (const state of new Set(this.activeTurnsByThread.values())) {
			if (!predicate(state)) continue;
			state.error = error.message;
			state.completed.resolve();
		}
	}
	generateConnectionKey(env, config) {
		const stableEnv = { ...env };
		delete stableEnv.TRACEPARENT;
		const keyData = {
			env: stableEnv,
			codex_path_override: config.codex_path_override,
			cli_config: this.getResolvedCliConfig(config),
			experimental_api: config.experimental_api
		};
		return `openai:codex-app-server:connection:${crypto.createHash("sha256").update(JSON.stringify(keyData)).digest("hex")}`;
	}
	generateThreadCacheKey(connectionKey, promptCacheBasis, config) {
		const keyData = {
			connectionKey,
			prompt: promptCacheBasis,
			working_dir: config.working_dir,
			additional_directories: config.additional_directories,
			model: config.model,
			model_provider: config.model_provider,
			service_tier: config.service_tier,
			sandbox_mode: config.sandbox_mode,
			sandbox_policy: config.sandbox_policy,
			network_access_enabled: config.network_access_enabled,
			approval_policy: config.approval_policy,
			approvals_reviewer: config.approvals_reviewer,
			model_reasoning_effort: config.model_reasoning_effort,
			reasoning_summary: config.reasoning_summary,
			personality: config.personality,
			base_instructions: config.base_instructions,
			developer_instructions: config.developer_instructions,
			collaboration_mode: config.collaboration_mode,
			output_schema: config.output_schema,
			base_url: config.base_url,
			ephemeral: config.ephemeral,
			experimental_raw_events: config.experimental_raw_events,
			persist_extended_history: config.persist_extended_history
		};
		return `openai:codex-app-server:thread:${crypto.createHash("sha256").update(JSON.stringify(keyData)).digest("hex")}`;
	}
	async getOrCreateThread(connection, connectionKey, promptCacheBasis, config, allowThreadPersistence, callOptions) {
		const canPersistThread = allowThreadPersistence && config.persist_threads === true;
		if (config.thread_id) {
			const cacheKey = canPersistThread ? `${connectionKey}:thread_id:${config.thread_id}` : void 0;
			const cachedOrPending = this.getCachedOrPendingThread(cacheKey);
			if (cachedOrPending !== void 0) return this.waitForThreadHandle(cachedOrPending, callOptions?.abortSignal);
			this.throwIfThreadWaitAborted(callOptions?.abortSignal);
			const threadPromise = this.cacheThreadPromise(cacheKey, connection.instanceId, async () => {
				const response = await connection.request("thread/resume", this.buildThreadResumeParams(config.thread_id, config), { timeoutMs: this.getRequestTimeoutMs(config) });
				const handle = {
					connectionKey,
					threadId: response?.thread?.id ?? config.thread_id,
					response,
					cacheKey,
					persistent: canPersistThread
				};
				if (cacheKey) this.threads.set(cacheKey, handle);
				return handle;
			});
			return cacheKey ? this.waitForThreadHandle(threadPromise, callOptions?.abortSignal) : this.waitForThreadHandle(threadPromise, callOptions?.abortSignal, { onAbortResolvedThread: (threadHandle) => this.cleanupThreadAfterTurn(connection, threadHandle, config, { skipIfProtected: true }) });
		}
		const cacheKey = canPersistThread ? this.generateThreadCacheKey(connectionKey, promptCacheBasis, config) : void 0;
		const cachedOrPending = this.getCachedOrPendingThread(cacheKey);
		if (cachedOrPending !== void 0) return this.waitForThreadHandle(cachedOrPending, callOptions?.abortSignal);
		this.throwIfThreadWaitAborted(callOptions?.abortSignal);
		const threadPromise = this.cacheThreadPromise(cacheKey, connection.instanceId, async () => {
			const poolSize = config.thread_pool_size ?? 1;
			if (cacheKey && this.threads.size >= poolSize) await this.evictOldestInactiveCachedThread(connection, connectionKey, this.getRequestTimeoutMs(config));
			const response = await connection.request("thread/start", this.buildThreadStartParams(config), { timeoutMs: this.getRequestTimeoutMs(config) });
			const threadId = response?.thread?.id;
			if (typeof threadId !== "string" || !threadId) throw new Error("codex app-server did not return a thread id");
			const handle = {
				connectionKey,
				threadId,
				response,
				cacheKey,
				persistent: canPersistThread
			};
			if (cacheKey) this.threads.set(cacheKey, handle);
			return handle;
		});
		return cacheKey ? this.waitForThreadHandle(threadPromise, callOptions?.abortSignal) : this.waitForThreadHandle(threadPromise, callOptions?.abortSignal, { onAbortResolvedThread: (threadHandle) => this.cleanupThreadAfterTurn(connection, threadHandle, config, { skipIfProtected: true }) });
	}
	getCachedOrPendingThread(cacheKey) {
		if (!cacheKey) return;
		return this.threads.get(cacheKey) ?? this.threadPromises.get(cacheKey);
	}
	cacheThreadPromise(cacheKey, connectionInstanceId, createThread) {
		if (!cacheKey) return createThread();
		let threadPromise;
		threadPromise = createThread().finally(() => {
			if (this.threadPromises.get(cacheKey) === threadPromise) {
				this.threadPromises.delete(cacheKey);
				this.threadPromiseConnectionInstances.delete(cacheKey);
			}
		});
		this.threadPromises.set(cacheKey, threadPromise);
		this.threadPromiseConnectionInstances.set(cacheKey, connectionInstanceId);
		return threadPromise;
	}
	throwIfThreadWaitAborted(abortSignal) {
		if (abortSignal?.aborted) throw createAbortError("Codex app-server thread wait aborted");
	}
	async waitForThreadHandle(thread, abortSignal, options = {}) {
		const threadPromise = Promise.resolve(thread);
		if (!abortSignal) return threadPromise;
		let abortCleanupScheduled = false;
		const scheduleAbortCleanup = () => {
			if (abortCleanupScheduled) return;
			abortCleanupScheduled = true;
			threadPromise.then(async (threadHandle) => {
				if (options.onAbortResolvedThread) await options.onAbortResolvedThread(threadHandle);
			}).catch((error) => {
				logger.debug("[CodexAppServer] Thread request finished with error after abort", { error });
			});
		};
		const createThreadAbortError = () => createAbortError(options.abortMessage ?? "Codex app-server thread wait aborted");
		if (abortSignal.aborted) {
			scheduleAbortCleanup();
			throw createThreadAbortError();
		}
		let abortListener;
		const abortPromise = new Promise((_, reject) => {
			abortListener = () => {
				scheduleAbortCleanup();
				reject(createThreadAbortError());
			};
			abortSignal.addEventListener("abort", abortListener, { once: true });
		});
		try {
			return await Promise.race([threadPromise, abortPromise]);
		} finally {
			if (abortListener) abortSignal.removeEventListener("abort", abortListener);
		}
	}
	scheduleThreadPoolEnforcement(fallbackConnection, fallbackConnectionKey, config) {
		if (!config.persist_threads || config.thread_id) return;
		this.enforceThreadPoolLimit(fallbackConnection, fallbackConnectionKey, config).catch((error) => {
			logger.debug("[CodexAppServer] Error enforcing thread pool limit", { error });
		});
	}
	async enforceThreadPoolLimit(fallbackConnection, fallbackConnectionKey, config) {
		const poolSize = config.thread_pool_size ?? 1;
		while (this.threads.size > poolSize) if (!await this.evictOldestInactiveCachedThread(fallbackConnection, fallbackConnectionKey, this.getRequestTimeoutMs(config))) return;
	}
	async evictOldestInactiveCachedThread(fallbackConnection, fallbackConnectionKey, timeoutMs) {
		for (const [cacheKey, handle] of this.threads) {
			if (this.isThreadProtected(handle.threadId)) continue;
			await this.evictCachedThread(fallbackConnection, fallbackConnectionKey, cacheKey, timeoutMs);
			return true;
		}
		logger.debug("[CodexAppServer] Thread pool is full, but all cached threads are active");
		return false;
	}
	async evictCachedThread(fallbackConnection, fallbackConnectionKey, cacheKey, timeoutMs) {
		const evicted = this.threads.get(cacheKey);
		this.threads.delete(cacheKey);
		if (!evicted) return;
		const connection = this.connections.get(evicted.connectionKey) ?? (evicted.connectionKey === fallbackConnectionKey ? fallbackConnection : void 0);
		if (!connection) {
			logger.debug("[CodexAppServer] Evicted cached thread without an active connection", { threadId: evicted.threadId });
			return;
		}
		try {
			await connection.request("thread/unsubscribe", { threadId: evicted.threadId }, { timeoutMs });
		} catch (error) {
			logger.warn("[CodexAppServer] Failed to unsubscribe evicted cached thread", {
				error,
				threadId: evicted.threadId
			});
		}
	}
	buildThreadStartParams(config) {
		return {
			...config.model ? { model: config.model } : {},
			...config.model_provider ? { modelProvider: config.model_provider } : {},
			...config.service_tier ? { serviceTier: config.service_tier } : {},
			...config.working_dir ? { cwd: config.working_dir } : {},
			approvalPolicy: config.approval_policy ?? "never",
			...config.approvals_reviewer ? { approvalsReviewer: config.approvals_reviewer } : {},
			sandbox: config.sandbox_mode ?? "read-only",
			...Object.keys(config.cli_config ?? {}).length > 0 ? { config: config.cli_config } : {},
			serviceName: "promptfoo",
			...config.base_instructions ? { baseInstructions: config.base_instructions } : {},
			...config.developer_instructions ? { developerInstructions: config.developer_instructions } : {},
			...config.personality ? { personality: config.personality } : {},
			...config.base_url ? { config: {
				...config.cli_config ?? {},
				base_url: config.base_url
			} } : {},
			ephemeral: config.ephemeral ?? true,
			experimentalRawEvents: config.experimental_raw_events ?? false,
			persistExtendedHistory: config.persist_extended_history ?? false
		};
	}
	buildThreadResumeParams(threadId, config) {
		return {
			threadId,
			...config.model ? { model: config.model } : {},
			...config.model_provider ? { modelProvider: config.model_provider } : {},
			...config.service_tier ? { serviceTier: config.service_tier } : {},
			...config.working_dir ? { cwd: config.working_dir } : {},
			approvalPolicy: config.approval_policy ?? "never",
			...config.approvals_reviewer ? { approvalsReviewer: config.approvals_reviewer } : {},
			sandbox: config.sandbox_mode ?? "read-only",
			...Object.keys(config.cli_config ?? {}).length > 0 ? { config: config.cli_config } : {},
			...config.base_instructions ? { baseInstructions: config.base_instructions } : {},
			...config.developer_instructions ? { developerInstructions: config.developer_instructions } : {},
			...config.personality ? { personality: config.personality } : {},
			persistExtendedHistory: config.persist_extended_history ?? false
		};
	}
	buildTurnStartParams(threadId, input, config) {
		return {
			threadId,
			input,
			...config.working_dir ? { cwd: config.working_dir } : {},
			approvalPolicy: config.approval_policy ?? "never",
			...config.approvals_reviewer ? { approvalsReviewer: config.approvals_reviewer } : {},
			...config.sandbox_policy || config.network_access_enabled !== void 0 ? { sandboxPolicy: this.buildSandboxPolicy(config) } : {},
			...config.model ? { model: config.model } : {},
			...config.service_tier ? { serviceTier: config.service_tier } : {},
			...config.model_reasoning_effort ? { effort: config.model_reasoning_effort } : {},
			...config.reasoning_summary ? { summary: config.reasoning_summary } : {},
			...config.personality ? { personality: config.personality } : {},
			...config.collaboration_mode ? { collaborationMode: config.collaboration_mode } : {},
			...config.output_schema ? { outputSchema: config.output_schema } : {}
		};
	}
	buildSandboxPolicy(config) {
		if (config.sandbox_policy) return config.sandbox_policy;
		const networkAccess = config.network_access_enabled === true;
		switch (config.sandbox_mode ?? "read-only") {
			case "danger-full-access": return { type: "dangerFullAccess" };
			case "workspace-write": return {
				type: "workspaceWrite",
				writableRoots: [config.working_dir, ...config.additional_directories ?? []].filter(Boolean),
				readOnlyAccess: { type: "fullAccess" },
				networkAccess,
				excludeTmpdirEnvVar: false,
				excludeSlashTmp: false
			};
			default: return {
				type: "readOnly",
				access: { type: "fullAccess" },
				networkAccess
			};
		}
	}
	parsePromptInput(prompt) {
		let parsedPrompt;
		try {
			parsedPrompt = JSON.parse(prompt);
		} catch {
			return [{
				type: "text",
				text: prompt,
				text_elements: []
			}];
		}
		if (!Array.isArray(parsedPrompt) || parsedPrompt.length === 0) return [{
			type: "text",
			text: prompt,
			text_elements: []
		}];
		const input = [];
		for (const item of parsedPrompt) {
			if (!this.isPromptInputItem(item)) return [{
				type: "text",
				text: prompt,
				text_elements: []
			}];
			input.push(this.normalizePromptInputItem(item));
		}
		return input;
	}
	isPromptInputItem(item) {
		if (!item || typeof item !== "object" || Array.isArray(item)) return false;
		if ("type" in item && item.type === "text") return "text" in item && typeof item.text === "string";
		if ("type" in item && item.type === "image") return "url" in item && typeof item.url === "string";
		if ("type" in item && (item.type === "local_image" || item.type === "localImage")) return "path" in item && typeof item.path === "string";
		if ("type" in item && (item.type === "skill" || item.type === "mention")) return "name" in item && typeof item.name === "string" && "path" in item && typeof item.path === "string";
		return false;
	}
	normalizePromptInputItem(item) {
		switch (item.type) {
			case "text": return {
				type: "text",
				text: item.text,
				text_elements: []
			};
			case "image": return {
				type: "image",
				url: item.url
			};
			case "local_image":
			case "localImage": return {
				type: "localImage",
				path: item.path
			};
			case "skill": return {
				type: "skill",
				name: item.name,
				path: item.path
			};
			case "mention": return {
				type: "mention",
				name: item.name,
				path: item.path
			};
		}
	}
	formatPromptInputForTrace(input) {
		return input.map((item) => {
			switch (item.type) {
				case "text": return item.text;
				case "image": return `[image: ${item.url}]`;
				case "localImage": return `[local_image: ${item.path}]`;
				case "skill": return `[skill: ${item.name} ${item.path}]`;
				case "mention": return `[mention: ${item.name} ${item.path}]`;
			}
		}).join("\n");
	}
	createTurnState(connectionKey, connectionInstanceId, threadId, promptInput, config, appServerEnv) {
		return {
			connectionKey,
			connectionInstanceId,
			threadId,
			config,
			appServerEnv,
			promptInput,
			items: [],
			itemStarts: [],
			notifications: [],
			notificationCount: 0,
			serverRequests: [],
			agentMessageDeltas: [],
			agentMessageDeltasByItemId: /* @__PURE__ */ new Map(),
			commandExecutionOutputDeltasByItemId: /* @__PURE__ */ new Map(),
			completed: createDeferred(),
			activeSpans: /* @__PURE__ */ new Map(),
			itemStartTimes: /* @__PURE__ */ new Map(),
			lastEventTime: Date.now(),
			turnCount: 0,
			activeTurnSpan: void 0,
			activeTurnIndex: 0
		};
	}
	registerTurnState(state) {
		this.activeTurnsByThread.set(state.threadId, state);
		if (state.turnId) this.activeTurnsByTurn.set(state.turnId, state);
	}
	updateTurnStateId(state, turnId) {
		if (state.turnId && state.turnId !== turnId) this.activeTurnsByTurn.delete(state.turnId);
		state.turnId = turnId;
		this.activeTurnsByTurn.set(turnId, state);
	}
	unregisterTurnState(state) {
		this.activeTurnsByThread.delete(state.threadId);
		if (state.turnId) this.activeTurnsByTurn.delete(state.turnId);
		this.endUnclosedItemSpans(state);
	}
	protectThread(threadId) {
		this.protectedThreadCounts.set(threadId, (this.protectedThreadCounts.get(threadId) ?? 0) + 1);
	}
	unprotectThread(threadId) {
		const count = this.protectedThreadCounts.get(threadId);
		if (!count || count <= 1) {
			this.protectedThreadCounts.delete(threadId);
			return;
		}
		this.protectedThreadCounts.set(threadId, count - 1);
	}
	isThreadProtected(threadId) {
		return this.activeTurnsByThread.has(threadId) || (this.protectedThreadCounts.get(threadId) ?? 0) > 0;
	}
	getTurnState(threadId, turnId) {
		if (turnId) return this.activeTurnsByTurn.get(turnId);
		if (threadId) return this.activeTurnsByThread.get(threadId);
	}
	handleNotification(message) {
		const params = message.params ?? {};
		const state = this.getTurnState(params.threadId, params.turnId);
		if (!state) {
			logger.debug("[CodexAppServer] Notification without active turn", { method: message.method });
			return;
		}
		state.notificationCount += 1;
		if (state.config.include_raw_events) state.notifications.push(message);
		const eventTime = Date.now();
		switch (message.method) {
			case "turn/started":
				if (typeof params.turn?.id === "string") this.updateTurnStateId(state, params.turn.id);
				this.startTurnSpan(state, eventTime, true);
				break;
			case "item/started":
				if (!state.activeTurnSpan) this.startTurnSpan(state, eventTime);
				this.handleItemStarted(state, params.item, eventTime);
				break;
			case "item/completed":
				if (!state.activeTurnSpan) this.startTurnSpan(state, eventTime);
				this.handleItemCompleted(state, params.item, eventTime);
				break;
			case "item/agentMessage/delta":
				this.handleAgentMessageDelta(state, params);
				break;
			case "item/commandExecution/outputDelta":
				this.handleCommandExecutionOutputDelta(state, params);
				break;
			case "thread/tokenUsage/updated":
				state.rawTokenUsage = params.tokenUsage;
				state.tokenUsage = this.buildTokenUsage(params.tokenUsage);
				break;
			case "turn/completed":
				state.turn = params.turn;
				if (params.turn?.status === "failed") state.error = params.turn?.error?.message ?? "Codex app-server turn failed";
				if (!state.activeTurnSpan) this.startTurnSpan(state, state.lastEventTime);
				this.endTurnSpan(state, eventTime, state.error);
				state.completed.resolve();
				break;
			case "error":
				if (params.willRetry === true) {
					logger.debug("[CodexAppServer] Retryable error received", {
						error: params.error?.message,
						threadId: params.threadId,
						turnId: params.turnId
					});
					break;
				}
				state.error = params.error?.message ?? "Codex app-server error";
				if (!state.activeTurnSpan) this.startTurnSpan(state, state.lastEventTime);
				this.endTurnSpan(state, eventTime, state.error);
				state.completed.resolve();
				break;
		}
		state.lastEventTime = eventTime;
	}
	handleItemStarted(state, item, eventTime) {
		if (!item) return;
		state.itemStarts.push(item);
		if (!item.id) return;
		const itemId = String(item.id);
		const span = this.startItemSpan(item, itemId, void 0, state.activeTurnIndex > 0 ? state.activeTurnIndex : void 0);
		state.activeSpans.set(itemId, span);
		state.itemStartTimes.set(itemId, eventTime);
	}
	handleItemCompleted(state, item, eventTime) {
		if (!item) return;
		const completedItem = this.withCommandExecutionOutput(state, item);
		if (completedItem?.type === "commandExecution" && typeof completedItem.id === "string" && typeof completedItem.aggregatedOutput === "string") {
			const existingOutput = state.commandExecutionOutputDeltasByItemId.get(completedItem.id);
			if (typeof existingOutput !== "string" || completedItem.aggregatedOutput.startsWith(existingOutput)) state.commandExecutionOutputDeltasByItemId.set(completedItem.id, completedItem.aggregatedOutput);
		}
		state.items.push(completedItem);
		const itemId = completedItem.id ? String(completedItem.id) : crypto.randomUUID();
		const span = state.activeSpans.get(itemId) ?? this.startItemSpan(completedItem, itemId, state.lastEventTime, state.activeTurnIndex > 0 ? state.activeTurnIndex : void 0);
		const startTime = state.itemStartTimes.get(itemId) ?? state.lastEventTime;
		this.applyItemCompletionAttributes(span, completedItem, eventTime, startTime);
		span.end();
		state.activeSpans.delete(itemId);
		state.itemStartTimes.delete(itemId);
	}
	handleAgentMessageDelta(state, params) {
		if (typeof params.delta !== "string") return;
		state.agentMessageDeltas.push(params.delta);
		if (typeof params.itemId === "string") {
			const existing = state.agentMessageDeltasByItemId.get(params.itemId) ?? "";
			state.agentMessageDeltasByItemId.set(params.itemId, existing + params.delta);
		}
	}
	handleCommandExecutionOutputDelta(state, params) {
		if (typeof params.itemId !== "string" || typeof params.delta !== "string") return;
		const existing = state.commandExecutionOutputDeltasByItemId.get(params.itemId) ?? "";
		const aggregatedOutput = existing + params.delta;
		state.commandExecutionOutputDeltasByItemId.set(params.itemId, aggregatedOutput);
		const completedItem = state.items.find((item) => item?.type === "commandExecution" && String(item.id) === params.itemId);
		if (completedItem && (typeof completedItem.aggregatedOutput !== "string" || completedItem.aggregatedOutput === existing)) completedItem.aggregatedOutput = aggregatedOutput;
	}
	withCommandExecutionOutput(state, item) {
		if (item?.type !== "commandExecution" || typeof item.id !== "string") return item;
		const aggregatedOutput = state.commandExecutionOutputDeltasByItemId.get(item.id);
		if (typeof aggregatedOutput !== "string") return item;
		if (typeof item.aggregatedOutput === "string" && !aggregatedOutput.startsWith(item.aggregatedOutput)) return item;
		return {
			...item,
			aggregatedOutput
		};
	}
	async handleServerRequest(message, config) {
		const params = message.params ?? {};
		const state = this.getTurnState(params.threadId ?? params.conversationId, params.turnId);
		const record = {
			id: message.id,
			method: message.method ?? "unknown",
			params: this.sanitizeForMetadata(params)
		};
		try {
			const response = this.buildServerRequestResponse(message, state?.config ?? config);
			record.response = this.sanitizeForMetadata(response);
			state?.serverRequests.push(record);
			return response;
		} catch (error) {
			record.error = error instanceof Error ? error.message : String(error);
			state?.serverRequests.push(record);
			throw error;
		}
	}
	buildServerRequestResponse(message, config) {
		const policy = config.server_request_policy ?? {};
		switch (message.method) {
			case "item/commandExecution/requestApproval": return { decision: policy.command_execution ?? "decline" };
			case "execCommandApproval": return this.buildLegacyApprovalResponse(policy.command_execution);
			case "item/fileChange/requestApproval": return { decision: policy.file_change ?? "decline" };
			case "applyPatchApproval": return this.buildLegacyApprovalResponse(policy.file_change);
			case "item/permissions/requestApproval": return {
				permissions: policy.permissions?.permissions ?? {},
				scope: policy.permissions?.scope ?? "turn",
				...policy.permissions?.strict_auto_review === void 0 ? {} : { strictAutoReview: policy.permissions.strict_auto_review }
			};
			case "item/tool/requestUserInput": return this.buildUserInputResponse(message.params, policy.user_input ?? "empty");
			case "mcpServer/elicitation/request": return this.buildMcpElicitationResponse(policy.mcp_elicitation);
			case "item/tool/call": return this.buildDynamicToolResponse(message.params, policy.dynamic_tools);
			case "account/chatgptAuthTokens/refresh": throw new Error("ChatGPT auth token refresh requests are not supported by promptfoo");
			default: throw new Error(`Unsupported codex app-server request: ${message.method ?? "unknown"}`);
		}
	}
	buildLegacyApprovalResponse(decision = "decline") {
		if (typeof decision !== "string") return { decision: "denied" };
		switch (decision) {
			case "accept": return { decision: "approved" };
			case "acceptForSession": return { decision: "approved_for_session" };
			case "cancel": return { decision: "abort" };
			case "decline": return { decision: "denied" };
		}
	}
	buildMcpElicitationResponse(policy = "decline") {
		if (typeof policy === "string") return {
			action: policy,
			content: null,
			_meta: null
		};
		return {
			action: policy.action,
			content: policy.content ?? null,
			_meta: policy._meta ?? null
		};
	}
	buildUserInputResponse(params, policy) {
		const answers = {};
		const questions = Array.isArray(params?.questions) ? params.questions : [];
		for (const question of questions) {
			if (!question?.id || typeof question.id !== "string") continue;
			if (policy === "empty") {
				answers[question.id] = { answers: [] };
				continue;
			}
			if (policy === "first-option") {
				const firstLabel = Array.isArray(question.options) && typeof question.options[0]?.label === "string" ? question.options[0].label : "";
				answers[question.id] = { answers: firstLabel ? [firstLabel] : [] };
				continue;
			}
			const configuredAnswer = policy[question.id];
			answers[question.id] = { answers: Array.isArray(configuredAnswer) ? configuredAnswer : configuredAnswer ? [configuredAnswer] : [] };
		}
		return { answers };
	}
	buildDynamicToolResponse(params, tools) {
		const configured = typeof params?.tool === "string" ? tools?.[params.tool] : void 0;
		if (!configured) return {
			contentItems: [{
				type: "inputText",
				text: "No dynamic tool response configured."
			}],
			success: false
		};
		return {
			contentItems: configured.contentItems ?? [{
				type: "inputText",
				text: configured.text ?? ""
			}],
			success: configured.success ?? true
		};
	}
	async waitForTurnCompletion(connection, state, config, callOptions) {
		const interruptTurn = () => {
			if (!state.turnId) return;
			connection.request("turn/interrupt", {
				threadId: state.threadId,
				turnId: state.turnId
			}, { timeoutMs: 5e3 }).catch((error) => {
				logger.debug("[CodexAppServer] Error interrupting turn", { error });
			});
		};
		let timeout;
		let abortListener;
		const timeoutPromise = config.turn_timeout_ms ? new Promise((_, reject) => {
			timeout = setTimeout(() => {
				interruptTurn();
				reject(/* @__PURE__ */ new Error(`codex app-server turn timed out after ${config.turn_timeout_ms}ms`));
			}, config.turn_timeout_ms);
		}) : void 0;
		const abortPromise = callOptions?.abortSignal ? new Promise((_, reject) => {
			abortListener = () => {
				interruptTurn();
				reject(createAbortError("OpenAI Codex app-server call aborted"));
			};
			callOptions.abortSignal?.addEventListener("abort", abortListener, { once: true });
		}) : void 0;
		try {
			await Promise.race([
				state.completed.promise,
				timeoutPromise,
				abortPromise
			].filter((promise) => Boolean(promise)));
		} finally {
			if (timeout) clearTimeout(timeout);
			if (abortListener && callOptions?.abortSignal) callOptions.abortSignal.removeEventListener("abort", abortListener);
		}
		if (state.error) throw new Error(state.error);
	}
	async cleanupThreadAfterTurn(connection, threadHandle, config, options = {}) {
		if (threadHandle.persistent || config.thread_cleanup === "none") return;
		if (options.skipIfProtected && this.isThreadProtected(threadHandle.threadId)) return;
		if (options.skipIfActiveTurn && this.activeTurnsByThread.has(threadHandle.threadId)) return;
		if (config.thread_id && (this.protectedThreadCounts.get(threadHandle.threadId) ?? 0) > 1) return;
		if (config.thread_id && config.thread_cleanup === "archive") return;
		try {
			if (config.thread_cleanup === "archive") await connection.request("thread/archive", { threadId: threadHandle.threadId }, { timeoutMs: this.getRequestTimeoutMs(config) });
			else await connection.request("thread/unsubscribe", { threadId: threadHandle.threadId }, { timeoutMs: this.getRequestTimeoutMs(config) });
		} catch (error) {
			logger.warn("[CodexAppServer] Error cleaning up thread after turn", {
				threadId: threadHandle.threadId,
				error
			});
		}
	}
	getThreadRunQueueKey(config, threadHandle) {
		if (config.thread_id) return `thread_id:${threadHandle.threadId}`;
		if (config.deep_tracing) return;
		if (threadHandle.persistent && threadHandle.cacheKey) return threadHandle.cacheKey;
	}
	async runSerializedThreadTurn(queueKey, abortSignal, executeTurn) {
		if (!queueKey) return executeTurn();
		const previousRun = this.threadRunQueues.get(queueKey) ?? Promise.resolve();
		let releaseCurrentRun = () => {};
		const currentRun = new Promise((resolve) => {
			releaseCurrentRun = resolve;
		});
		const queuedRun = previousRun.catch(() => void 0).then(() => currentRun);
		this.threadRunQueues.set(queueKey, queuedRun);
		queuedRun.finally(() => {
			if (this.threadRunQueues.get(queueKey) === queuedRun) this.threadRunQueues.delete(queueKey);
		});
		try {
			await this.waitForPreviousThreadRun(previousRun, abortSignal);
			return await executeTurn();
		} finally {
			releaseCurrentRun();
		}
	}
	async waitForPreviousThreadRun(previousRun, abortSignal) {
		const previousRunDone = previousRun.catch(() => void 0);
		if (!abortSignal) {
			await previousRunDone;
			return;
		}
		if (abortSignal.aborted) throw createAbortError("Codex app-server thread turn wait aborted");
		let onAbort;
		const abortPromise = new Promise((_, reject) => {
			onAbort = () => reject(createAbortError("Codex app-server thread turn wait aborted"));
			abortSignal.addEventListener("abort", onAbort, { once: true });
		});
		try {
			await Promise.race([previousRunDone, abortPromise]);
		} finally {
			if (onAbort) abortSignal.removeEventListener("abort", onAbort);
		}
	}
	buildProviderResponse(state, threadHandle, config) {
		const output = this.getFinalOutput(state);
		const normalizedItems = state.items.map((item) => this.normalizeItemForMetadata(item));
		const rawItems = state.items.map((item) => this.normalizeItemForRaw(item));
		const rawTokenUsage = this.sanitizeForMetadata(state.rawTokenUsage);
		const raw = {
			finalResponse: output,
			output,
			thread: this.sanitizeForMetadata(threadHandle.response?.thread),
			turn: this.sanitizeForMetadata(state.turn),
			items: rawItems,
			usage: this.buildSdkUsage(state.rawTokenUsage),
			tokenUsage: rawTokenUsage,
			serverRequests: state.serverRequests,
			...config.include_raw_events ? { notifications: this.sanitizeForMetadata(state.notifications) } : {}
		};
		const metadata = this.buildResponseMetadata(state, threadHandle, config, normalizedItems);
		return {
			output,
			tokenUsage: state.tokenUsage,
			cost: this.calculateResponseCost(state.tokenUsage, config.model),
			metadata,
			raw: JSON.stringify(raw),
			sessionId: threadHandle.threadId
		};
	}
	getFinalOutput(state) {
		const completedAgentMessages = state.items.filter((item) => item?.type === "agentMessage" && typeof item.text === "string").map((item) => item.text);
		if (completedAgentMessages.length > 0) return completedAgentMessages[completedAgentMessages.length - 1];
		const deltaMessages = Array.from(state.agentMessageDeltasByItemId.values()).filter(Boolean);
		if (deltaMessages.length > 0) return deltaMessages[deltaMessages.length - 1];
		return state.agentMessageDeltas.join("");
	}
	buildResponseMetadata(state, threadHandle, config, normalizedItems) {
		const skillMetadata = this.buildSkillMetadata(state.items, this.getSkillRootPrefixes(config, state.appServerEnv));
		return {
			codexAppServer: {
				threadId: threadHandle.threadId,
				turnId: state.turnId,
				model: config.model,
				modelProvider: config.model_provider,
				cwd: config.working_dir,
				sandboxMode: config.sandbox_mode ?? "read-only",
				approvalPolicy: config.approval_policy ?? "never",
				itemCounts: this.getItemCounts(state.items),
				items: normalizedItems,
				serverRequests: state.serverRequests,
				notificationCount: state.notificationCount
			},
			...getCodexSkillMetadataFields(skillMetadata)
		};
	}
	normalizeItemForMetadata(item) {
		const base = {
			id: item?.id,
			type: item?.type,
			status: item?.status
		};
		switch (item?.type) {
			case "commandExecution": return this.sanitizeForMetadata({
				...base,
				command: item.command,
				cwd: item.cwd,
				exitCode: item.exitCode,
				durationMs: item.durationMs,
				aggregatedOutput: item.aggregatedOutput
			});
			case "fileChange": return this.sanitizeForMetadata({
				...base,
				changes: item.changes
			});
			case "mcpToolCall": return this.sanitizeForMetadata({
				...base,
				server: item.server,
				tool: item.tool,
				arguments: item.arguments,
				result: item.result,
				error: item.error,
				durationMs: item.durationMs
			});
			case "dynamicToolCall": return this.sanitizeForMetadata({
				...base,
				tool: item.tool,
				arguments: item.arguments,
				success: item.success,
				contentItems: item.contentItems,
				durationMs: item.durationMs
			});
			case "webSearch": return this.sanitizeForMetadata({
				...base,
				query: item.query,
				action: item.action
			});
			case "agentMessage": return this.sanitizeForMetadata({
				...base,
				text: item.text
			});
			case "reasoning": return this.sanitizeForMetadata({
				...base,
				summary: item.summary,
				content: item.content
			});
			default: return this.sanitizeForMetadata(base);
		}
	}
	normalizeItemForRaw(item) {
		const base = {
			id: item?.id,
			type: item?.type,
			status: item?.status
		};
		switch (item?.type) {
			case "commandExecution": return this.sanitizeForMetadata({
				...base,
				type: "command_execution",
				command: item.command,
				cwd: item.cwd,
				exit_code: item.exitCode,
				duration_ms: item.durationMs,
				aggregated_output: item.aggregatedOutput
			});
			case "fileChange": return this.sanitizeForMetadata({
				...base,
				type: "file_change",
				changes: item.changes
			});
			case "mcpToolCall": return this.sanitizeForMetadata({
				...base,
				type: "mcp_tool_call",
				server: item.server,
				tool: item.tool,
				arguments: item.arguments,
				result: item.result,
				error: item.error,
				duration_ms: item.durationMs
			});
			case "dynamicToolCall": return this.sanitizeForMetadata({
				...base,
				type: "dynamic_tool_call",
				tool: item.tool,
				arguments: item.arguments,
				success: item.success,
				content_items: item.contentItems,
				duration_ms: item.durationMs
			});
			case "webSearch": return this.sanitizeForMetadata({
				...base,
				type: "web_search",
				query: item.query,
				action: item.action
			});
			case "agentMessage": return this.sanitizeForMetadata({
				...base,
				type: "agent_message",
				text: item.text
			});
			case "reasoning": return this.sanitizeForMetadata({
				...base,
				type: "reasoning",
				text: this.extractReasoningText(item),
				summary: item.summary,
				content: item.content
			});
			case "todoList": return this.sanitizeForMetadata({
				...base,
				type: "todo_list",
				items: item.items
			});
			case "error": return this.sanitizeForMetadata({
				...base,
				type: "error",
				message: item.message
			});
			case "userMessage": return this.sanitizeForMetadata({
				...base,
				type: "user_message",
				content: item.content
			});
			default: return this.sanitizeForMetadata(base);
		}
	}
	extractReasoningText(item) {
		if (typeof item?.text === "string") return item.text;
		const blocks = [item?.summary, item?.content].find(Array.isArray);
		if (!blocks) return;
		const parts = blocks.flatMap((block) => {
			if (typeof block === "string") return [block];
			const text = block?.text;
			return typeof text === "string" && text.length > 0 ? [text] : [];
		});
		return parts.length > 0 ? parts.join("\n") : void 0;
	}
	resolveRawUsage(rawTokenUsage) {
		const usage = rawTokenUsage?.last ?? rawTokenUsage?.total ?? rawTokenUsage;
		if (!usage) return;
		const input = usage.inputTokens ?? usage.input_tokens;
		const output = usage.outputTokens ?? usage.output_tokens;
		if (typeof input !== "number" || typeof output !== "number") return;
		return {
			input,
			output,
			cached: usage.cachedInputTokens ?? usage.cached_input_tokens ?? 0,
			reasoning: usage.reasoningOutputTokens ?? usage.reasoning_output_tokens ?? 0
		};
	}
	buildSdkUsage(rawTokenUsage) {
		const usage = this.resolveRawUsage(rawTokenUsage);
		if (!usage) return;
		return {
			input_tokens: usage.input,
			cached_input_tokens: usage.cached,
			output_tokens: usage.output,
			reasoning_output_tokens: usage.reasoning
		};
	}
	buildTokenUsage(rawTokenUsage) {
		const usage = this.resolveRawUsage(rawTokenUsage);
		if (!usage) return;
		return {
			prompt: usage.input,
			completion: usage.output,
			total: usage.input + usage.output,
			cached: usage.cached
		};
	}
	calculateResponseCost(tokenUsage, model) {
		return calculateOpenAIUsageCostFromTokenUsage(model, tokenUsage);
	}
	getItemCounts(items) {
		const counts = {};
		for (const item of items) {
			const type = typeof item?.type === "string" ? item.type : "unknown";
			counts[type] = (counts[type] ?? 0) + 1;
		}
		return counts;
	}
	validateWorkingDirectory(workingDir, skipGitCheck = false) {
		const cacheKey = `${workingDir}:${skipGitCheck}`;
		if (this.validatedWorkingDirs.has(cacheKey)) return;
		let stats;
		try {
			stats = fs.statSync(workingDir);
		} catch (err) {
			throw new Error(`Working directory ${workingDir} does not exist or isn't accessible: ${err.message}`);
		}
		if (!stats.isDirectory()) throw new Error(`Working directory ${workingDir} is not a directory`);
		if (!skipGitCheck && !this.isInsideGitRepository(workingDir)) throw new Error(dedent`Working directory ${workingDir} is not inside a Git repository.

        Codex app-server requires a Git repository by default to prevent unrecoverable errors.

        To bypass this check, set skip_git_repo_check: true in your provider config.`);
		this.validatedWorkingDirs.add(cacheKey);
	}
	isInsideGitRepository(workingDir) {
		return this.findGitRepositoryRoot(workingDir) !== void 0;
	}
	findGitRepositoryRoot(workingDir) {
		let currentDir = path.resolve(workingDir);
		while (true) {
			if (fs.existsSync(path.join(currentDir, ".git"))) return currentDir;
			const parentDir = path.dirname(currentDir);
			if (parentDir === currentDir) return;
			currentDir = parentDir;
		}
	}
	warnOnceForDeepTracingThreadOptions(config) {
		if (!config.deep_tracing || this.deepTracingWarningShown || !config.persist_threads && (config.thread_pool_size ?? 0) <= 1) return;
		logger.warn("[CodexAppServer] deep_tracing requires a fresh app-server process per call. Persistent thread pooling options (persist_threads, thread_pool_size) are ignored when deep_tracing is enabled. Explicit thread_id values are still resumed and serialized.");
		this.deepTracingWarningShown = true;
	}
	startItemSpan(item, itemId, startTime, turnIndex) {
		return trace.getTracer("promptfoo.codex-app-server").startSpan(this.getSpanNameForItem(item), {
			kind: SpanKind.INTERNAL,
			...startTime === void 0 ? {} : { startTime },
			attributes: {
				"codex.app_server.item.id": itemId,
				"codex.app_server.item.type": item?.type ?? "unknown",
				...typeof turnIndex === "number" ? { "gen_ai.turn.index": turnIndex } : {},
				...this.getAttributesForItem(item)
			}
		});
	}
	startTurnSpan(state, eventTime, clearPreviousUsage = false) {
		if (clearPreviousUsage && state.turnCount > 0) {
			state.rawTokenUsage = void 0;
			state.tokenUsage = void 0;
		}
		openTurnSpan(state, {
			tracer: trace.getTracer("promptfoo.codex-app-server"),
			eventTime,
			system: "openai",
			logLabel: "CodexAppServer"
		});
	}
	endTurnSpan(state, eventTime, errorMessage) {
		const usage = this.resolveRawUsage(state.rawTokenUsage);
		const attributes = {};
		if (usage) {
			attributes["gen_ai.usage.input_tokens"] = usage.input;
			attributes["gen_ai.usage.output_tokens"] = usage.output;
			if (usage.cached) attributes["gen_ai.usage.cached_tokens"] = usage.cached;
			if (usage.reasoning) attributes["gen_ai.usage.reasoning_tokens"] = usage.reasoning;
		}
		closeTurnSpan(state, {
			eventTime,
			attributes,
			errorMessage,
			logLabel: "CodexAppServer"
		});
	}
	applyItemCompletionAttributes(span, item, eventTime, startTime) {
		for (const [key, value] of Object.entries(this.getCompletionAttributesForItem(item))) span.setAttribute(key, value);
		span.setAttribute("codex.app_server.duration_ms", eventTime - startTime);
		if (this.isItemError(item)) span.setStatus({
			code: SpanStatusCode.ERROR,
			message: this.getItemErrorMessage(item)
		});
		else span.setStatus({ code: SpanStatusCode.OK });
	}
	endUnclosedItemSpans(state) {
		for (const [itemId, span] of state.activeSpans) {
			logger.warn("[CodexAppServer] Item span not properly closed", { itemId });
			span.setStatus({
				code: SpanStatusCode.ERROR,
				message: "Span not properly closed"
			});
			span.end();
		}
		state.activeSpans.clear();
		state.itemStartTimes.clear();
		if (state.activeTurnSpan) {
			logger.warn("[CodexAppServer] Turn span not properly closed");
			closeTurnSpan(state, {
				errorMessage: "Turn span not properly closed",
				logLabel: "CodexAppServer"
			});
		}
	}
	getSpanNameForItem(item) {
		switch (item?.type) {
			case "commandExecution": return `exec ${typeof item.command === "string" ? item.command.split(" ")[0] || "command" : "command"}`;
			case "fileChange": return "file change";
			case "mcpToolCall": return `mcp ${item.server ?? "unknown"}/${item.tool ?? "unknown"}`;
			case "dynamicToolCall": return `tool ${item.tool ?? "unknown"}`;
			case "agentMessage": return "agent response";
			case "reasoning": return "reasoning";
			case "webSearch": return "web search";
			default: return `codex.app_server.${item?.type ?? "unknown"}`;
		}
	}
	getAttributesForItem(item) {
		const attrs = {};
		if (item?.type === "commandExecution" && typeof item.command === "string") attrs["codex.command"] = this.sanitizeTraceText(item.command, "Codex app-server command trace attribute") ?? "";
		if (item?.type === "mcpToolCall") {
			if (typeof item.server === "string") attrs["codex.mcp.server"] = item.server;
			if (typeof item.tool === "string") attrs["codex.mcp.tool"] = item.tool;
		}
		if (item?.type === "dynamicToolCall" && typeof item.tool === "string") attrs["codex.tool.name"] = item.tool;
		if (item?.type === "webSearch" && typeof item.query === "string") attrs["codex.search.query"] = this.sanitizeTraceText(item.query, "Codex app-server web search trace attribute") ?? "";
		return attrs;
	}
	getCompletionAttributesForItem(item) {
		const attrs = {};
		if (typeof item?.status === "string") attrs["codex.status"] = item.status;
		if (item?.type === "commandExecution") {
			if (typeof item.exitCode === "number") attrs["codex.exit_code"] = item.exitCode;
			if (typeof item.aggregatedOutput === "string") attrs["codex.output"] = this.sanitizeTraceText(item.aggregatedOutput, "Codex app-server command output trace attribute") ?? "";
		}
		if (item?.type === "agentMessage" && typeof item.text === "string") attrs["codex.message"] = this.sanitizeTraceText(item.text, "Codex app-server message trace attribute") ?? "";
		return attrs;
	}
	isItemError(item) {
		return item?.status === "failed" || item?.status === "declined" || item?.error !== void 0 || item?.type === "commandExecution" && typeof item.exitCode === "number" && item.exitCode !== 0;
	}
	getItemErrorMessage(item) {
		return (typeof item?.error?.message === "string" ? item.error.message : null) || (item?.type === "commandExecution" && item.exitCode !== 0 ? `Command exited with code ${item.exitCode}` : null) || "Item failed";
	}
	getSkillRootPrefixes(config, appServerEnv) {
		const resolvedWorkingDir = config.working_dir ? path.resolve(config.working_dir).replace(/\\/g, "/") : void 0;
		return getCodexSkillRootPrefixes({
			codexHome: appServerEnv.CODEX_HOME,
			gitRepositoryRoot: resolvedWorkingDir ? this.findGitRepositoryRoot(resolvedWorkingDir) : void 0,
			homeDir: appServerEnv.HOME || appServerEnv.USERPROFILE,
			workingDir: resolvedWorkingDir
		});
	}
	buildSkillMetadata(items, skillRootPrefixes) {
		return buildCodexSkillMetadata(items, skillRootPrefixes, {
			getCommand: (item) => item?.type === "commandExecution" && typeof item.command === "string" && item.command.trim() ? item.command : void 0,
			isSuccessfulCommand: (item) => this.isSuccessfulCommandExecution(item)
		});
	}
	isSuccessfulCommandExecution(item) {
		if (item?.type !== "commandExecution") return false;
		if (typeof item.status === "string" && item.status !== "completed") return false;
		if (typeof item.exitCode === "number" && item.exitCode !== 0) return false;
		return true;
	}
	sanitizeTraceText(value, context) {
		const sanitized = this.redactTracePii(sanitizeObject(value, { context }));
		if (typeof sanitized === "string") return sanitized;
		if (sanitized === void 0 || sanitized === null) return;
		try {
			return JSON.stringify(sanitized);
		} catch (error) {
			logger.debug("[CodexAppServer] Failed to stringify sanitized trace text", { error });
			return;
		}
	}
	sanitizeForMetadata(value) {
		return this.redactTracePii(sanitizeObject(value, { context: "Codex app-server metadata" }));
	}
	redactTracePii(value) {
		if (typeof value === "string") return value.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, REDACTED).replace(/\b(?:sk-(?:proj-)?[A-Za-z0-9_-]{20,}|sk-ant-[A-Za-z0-9_-]{20,}|AKIA[A-Z0-9]{16}|AIza[A-Za-z0-9_-]{35}|Bearer\s+[A-Za-z0-9._~+/-]{20,}|Basic\s+[A-Za-z0-9+/=]{20,})\b/g, REDACTED).replace(/\b(api[_-]?key|token|password|secret|authorization|auth)\s*([=:])(\s*)(["']?)[^\s"'`]+(\4)/gi, (_match, key, separator, spacing, quote) => `${key}${separator}${spacing}${quote}${REDACTED}${quote}`);
		if (Array.isArray(value)) return value.map((item) => this.redactTracePii(item));
		if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, normalizeFieldName(key).includes("email") ? REDACTED : this.redactTracePii(entryValue)]));
		return value;
	}
};
//#endregion
export { OpenAICodexAppServerProvider };

//# sourceMappingURL=codex-app-server-BLzCQgvz.js.map