UNPKG

acpx

Version:

Headless CLI client for the Agent Client Protocol (ACP) — talk to coding agents from the command line

2,864 lines • 106 kB
import { S as normalizeAgentCommandInput, a as createAgentRegistry, i as DEFAULT_AGENT_NAME } from "./agent-registry-CxpAF3nI.js";
import { A as findSession, B as assertPersistedKeyPolicy, C as textPrompt, Dt as extractAcpError, Et as normalizeOutputError, L as readSessionRecord, Ot as isAcpResourceNotFoundError, R as resolveSessionRecord, U as serializeSessionRecordForDisk, V as parseSessionRecord, bt as writePrivateJsonFile, t as tryCancelOnRunningOwner } from "./ipc-CQUNQ8QQ.js";
import { $ as resolveRequestedConfigOption, A as applyInitialModelSelection, B as reconcileAgentSessionId, C as setSessionModeOnOwner, D as connectAndLoadSession, E as withConnectedSession, F as persistSessionOptions, G as recordSessionUpdate, I as sessionOptionsFromRecord, J as AcpClient, K as cloneSessionAcpxState, L as applyConversation, M as applyRequestedModelIfAdvertised, N as setDesiredModeId, O as applyConfigOptionSelection, P as normalizeSessionOptionShape, Q as isRequestedModelUnsupportedError, R as applyLifecycleSnapshotToRecord, T as setSessionModelOnOwner, U as recordClientOperation, V as cloneSessionConversation, W as recordPromptSubmission, X as REQUESTED_MODEL_UNSUPPORTED_REASONS, Y as REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, Z as RequestedModelUnsupportedError, _ as cancelSessionPrompt, c as LiveSessionCheckpoint, dt as TimeoutError, et as resolveRequestedModelId, ft as assertControlAuthority, h as ensureSession, j as applyModelSelection, k as applyConfigOptionsToRecord, l as runPromptTurn, mt as withTimeout, nt as snapshotEnvNames, q as advertisedModelState, r as sendSession, s as DISCARD_OUTPUT_FORMATTER, v as closeSession, x as setSessionConfigOptionOnOwner, z as createInitialSessionRecord } from "./queue-owner-runtime-ucHvXJ33.js";
import { t as watchSession } from "./watch-CXy2A5VY.js";
import { fileURLToPath } from "node:url";
import path from "node:path";
import fs from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { isDeepStrictEqual } from "node:util";
// src/runtime/public/errors.ts
var AcpRuntimeError = class extends Error {
	code;
	cause;
	constructor(code, message, options) {
		super(message);
		this.name = "AcpRuntimeError";
		this.code = code;
		this.cause = options?.cause;
	}
};
function isAcpRuntimeError(value) {
	return value instanceof AcpRuntimeError;
}
// src/runtime/engine/controls.ts
function advertisedConfigOptionIds(record) {
	const configOptions = record?.acpx?.config_options;
	if (!configOptions) return;
	return new Set(configOptions.map((option) => option.id).filter((id) => typeof id === "string" && id.trim().length > 0));
}
/** Caller-owned controls and the config option keys the session last advertised. */
function capabilitiesFromRecord(record) {
	const advertisedIds = advertisedConfigOptionIds(record);
	return {
		controls: [
			"session/set_mode",
			"session/set_model",
			"session/set_config_option",
			"session/status"
		],
		...advertisedIds?.size ? { configOptionKeys: [...advertisedIds] } : {}
	};
}
function resolveSupportedConfigOptionId(record, configId) {
	const advertisedIds = advertisedConfigOptionIds(record);
	if (!advertisedIds) return configId;
	if (advertisedIds.has(configId)) return configId;
	if (configId === "thinking" && advertisedIds.has("effort")) return "effort";
	const supported = [...advertisedIds].toSorted();
	const supportedText = supported.length > 0 ? supported.join(", ") : "none";
	throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `ACP session ${record.acpxRecordId} does not advertise config option '${configId}'. Supported config options: ${supportedText}.`);
}
// src/runtime/public/shared.ts
function isRecord(value) {
	return typeof value === "object" && value !== null && !Array.isArray(value);
}
function asTrimmedString(value) {
	return typeof value === "string" ? value.trim() : "";
}
function asString(value) {
	return typeof value === "string" ? value : void 0;
}
function asOptionalString(value) {
	return asTrimmedString(value) || void 0;
}
function deriveAgentFromSessionKey(sessionKey, fallbackAgent) {
	const match = sessionKey.match(/^agent:([^:]+):/i);
	return (match?.[1] ? asTrimmedString(match[1]) : "") || fallbackAgent;
}
// src/runtime/public/events.ts
const TOOL_OUTPUT_SUMMARY_MAX_CHARS = 500;
function asOptionalFiniteNumber(value) {
	return typeof value === "number" && Number.isFinite(value) ? value : void 0;
}
function resolveStatusTextForTag(params) {
	const resolver = STATUS_TEXT_RESOLVERS[params.tag];
	return resolver ? resolver(params.payload) : null;
}
const STATUS_TEXT_RESOLVERS = {
	available_commands_update: availableCommandsStatusText,
	current_mode_update: currentModeStatusText,
	config_option_update: configOptionStatusText,
	session_info_update: sessionInfoStatusText
};
function availableCommandsStatusText(payload) {
	const commands = Array.isArray(payload.availableCommands) ? payload.availableCommands : [];
	return commands.length > 0 ? `available commands updated (${commands.length})` : "available commands updated";
}
function currentModeStatusText(payload) {
	const mode = asTrimmedString(payload.currentModeId) || asTrimmedString(payload.modeId) || asTrimmedString(payload.mode);
	return mode ? `mode updated: ${mode}` : "mode updated";
}
function configOptionStatusText(payload) {
	const id = asTrimmedString(payload.id) || asTrimmedString(payload.configOptionId);
	const value = asTrimmedString(payload.currentValue) || asTrimmedString(payload.value) || asTrimmedString(payload.optionValue);
	if (id && value) return `config updated: ${id}=${value}`;
	return id ? `config updated: ${id}` : "config updated";
}
function sessionInfoStatusText(payload) {
	return asTrimmedString(payload.summary) || asTrimmedString(payload.message) || "session updated";
}
function planStatusText(payload) {
	const content = asTrimmedString((Array.isArray(payload.entries) ? payload.entries : []).find((entry) => isRecord(entry))?.content);
	return content ? `plan: ${content}` : null;
}
function isPlanEntryStatus(value) {
	return value === "pending" || value === "in_progress" || value === "completed";
}
function isPlanEntryPriority(value) {
	return value === "high" || value === "medium" || value === "low";
}
function planUpdateEvent(payload) {
	const raw = payload.entries;
	const entries = normalizePlanEntries(raw);
	if (Array.isArray(raw) && raw.length === 0) return {
		type: "status",
		text: "plan updated",
		tag: "plan",
		entries: []
	};
	const text = planStatusText(payload) ?? (entries.length > 0 ? `plan: ${entries[0]?.content}` : null);
	if (!text) return null;
	return {
		type: "status",
		text,
		tag: "plan",
		...entries.length > 0 ? { entries } : {}
	};
}
function normalizePlanEntries(value) {
	if (!Array.isArray(value)) return [];
	const entries = [];
	for (const entry of value) {
		const normalized = normalizePlanEntry(entry);
		if (normalized) entries.push(normalized);
	}
	return entries;
}
function normalizePlanEntry(value) {
	if (!isRecord(value)) return;
	const content = asTrimmedString(value.content);
	const status = asTrimmedString(value.status);
	if (!content || !isPlanEntryStatus(status)) return;
	const priority = asTrimmedString(value.priority);
	return {
		content,
		status,
		...isPlanEntryPriority(priority) ? { priority } : {}
	};
}
/**
* Documented allowlist for text_delta.meta origin fields.
* Only these keys may be copied from ACP update `_meta`.
* Unknown keys (including secret-like producer-controlled names) are dropped.
*/
const ORIGIN_META_KEYS = [
	"origin",
	"kind",
	"source"
];
/**
* Preserve a fail-closed subset of ACP update origin fields for text_delta
* consumers. Empty or unusable values are omitted rather than inferred.
*/
function extractTextDeltaOrigin(payload) {
	const messageId = asOptionalString(payload.messageId);
	const meta = sanitizeOriginMeta(payload._meta);
	return {
		...messageId ? { messageId } : {},
		...meta ? { meta } : {}
	};
}
/**
* Copy only allowlisted string origin keys from wire `_meta`.
* Nested objects, arrays, non-strings, and unknown keys are dropped.
*/
function sanitizeOriginMeta(value) {
	if (!isRecord(value)) return;
	const out = {};
	for (const key of ORIGIN_META_KEYS) {
		const entry = value[key];
		if (typeof entry !== "string") continue;
		const trimmed = entry.trim();
		if (!trimmed) continue;
		out[key] = trimmed;
	}
	return Object.keys(out).length > 0 ? out : void 0;
}
function resolveTextChunk(params) {
	const origin = extractTextDeltaOrigin(params.payload);
	const contentRaw = params.payload.content;
	if (isRecord(contentRaw)) {
		const contentType = asTrimmedString(contentRaw.type);
		if (contentType && contentType !== "text") return null;
		const text = asString(contentRaw.text);
		if (text && text.length > 0) return {
			type: "text_delta",
			text,
			stream: params.stream,
			tag: params.tag,
			...origin
		};
	}
	const text = asString(params.payload.text);
	if (!text || text.length === 0) return null;
	return {
		type: "text_delta",
		text,
		stream: params.stream,
		tag: params.tag,
		...origin
	};
}
function readFirstString(record, keys) {
	for (const key of keys) {
		const value = asOptionalString(record[key]);
		if (value) return value;
	}
}
function readFirstStringArray(record, keys) {
	for (const key of keys) {
		const value = record[key];
		if (!Array.isArray(value)) continue;
		const entries = value.map((entry) => asOptionalString(entry)).filter((entry) => entry !== void 0);
		if (entries.length > 0) return entries;
	}
}
function summarizeToolInput(rawInput) {
	if (rawInput == null) return;
	if (typeof rawInput === "string" || typeof rawInput === "number" || typeof rawInput === "boolean") return String(rawInput);
	if (!isRecord(rawInput)) return;
	const command = readFirstString(rawInput, [
		"command",
		"cmd",
		"program"
	]);
	const args = readFirstStringArray(rawInput, ["args", "arguments"]);
	if (command) return [command, ...args ?? []].join(" ");
	return readFirstString(rawInput, [
		"path",
		"file",
		"filePath",
		"filepath",
		"target",
		"uri",
		"url",
		"query",
		"pattern",
		"text",
		"search"
	]);
}
function truncateToolSummary(value) {
	if (value.length <= TOOL_OUTPUT_SUMMARY_MAX_CHARS) return value;
	return `${value.slice(0, 499)}…`;
}
function readToolContentText(value) {
	const record = isRecord(value) ? value : void 0;
	if (!record) return;
	if (record.type === "content") return readToolContentText(record.content);
	return toolContentTextReader(String(record.type))?.(record);
}
const TOOL_CONTENT_TEXT_READERS = {
	text: (record) => asString(record.text),
	audio: (record) => `[audio] ${asOptionalString(record.mimeType) || "audio"}`,
	resource_link: (record) => asOptionalString(record.title) || asOptionalString(record.name) || asOptionalString(record.uri),
	resource: (record) => {
		const resource = isRecord(record.resource) ? record.resource : void 0;
		return asString(resource?.text) || asOptionalString(resource?.uri);
	},
	diff: (record) => `diff ${asOptionalString(record.path) || "file"}`,
	terminal: (record) => {
		const terminalId = asOptionalString(record.terminalId) || asOptionalString(record.id);
		return terminalId ? `[terminal] ${terminalId}` : "[terminal]";
	}
};
function toolContentTextReader(type) {
	return Object.hasOwn(TOOL_CONTENT_TEXT_READERS, type) ? TOOL_CONTENT_TEXT_READERS[type] : void 0;
}
function summarizeToolContent(content) {
	if (!Array.isArray(content)) return;
	const fragments = content.map((entry) => readToolContentText(entry)?.trim()).filter((entry) => Boolean(entry));
	if (fragments.length === 0) return;
	return truncateToolSummary([...new Set(fragments)].join("\n"));
}
function summarizeToolOutput(rawOutput) {
	if (rawOutput == null) return;
	if (isScalarToolOutput(rawOutput)) return truncateToolSummary(String(rawOutput));
	const record = isRecord(rawOutput) ? rawOutput : void 0;
	if (!record) return;
	return truncateToolSummary(readFirstString(record, [
		"text",
		"message",
		"error",
		"stdout",
		"stderr",
		"content"
	]) ?? "") || void 0;
}
function isScalarToolOutput(value) {
	return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
}
function readToolKind(value) {
	const kind = asOptionalString(value);
	return kind && TOOL_KINDS.has(kind) ? kind : void 0;
}
const TOOL_KINDS = /* @__PURE__ */ new Set([
	"read",
	"edit",
	"delete",
	"move",
	"search",
	"execute",
	"fetch",
	"think",
	"switch_mode",
	"other"
]);
function createToolCallEvent(params) {
	const title = asTrimmedString(params.payload.title) || "tool call";
	const status = asTrimmedString(params.payload.status);
	const inputSummary = summarizeToolInput(params.payload.rawInput);
	const outputSummary = summarizeToolContent(params.payload.content) ?? summarizeToolOutput(params.payload.rawOutput);
	const toolCallId = asOptionalString(params.payload.toolCallId);
	const kind = readToolKind(params.payload.kind);
	const summaryText = status ? `${title} (${status})` : title;
	const detailSummary = params.tag === "tool_call_update" ? outputSummary ?? inputSummary : inputSummary ?? outputSummary;
	const event = {
		type: "tool_call",
		text: detailSummary ? `${summaryText}: ${detailSummary}` : summaryText,
		tag: params.tag,
		title
	};
	assignToolCallEventMetadata(event, params.payload, {
		toolCallId,
		status,
		kind
	});
	return event;
}
function assignToolCallEventMetadata(event, payload, values) {
	if (event.type !== "tool_call") return;
	if (values.toolCallId) event.toolCallId = values.toolCallId;
	if (values.status) event.status = values.status;
	if (values.kind) event.kind = values.kind;
	assignForwardedToolPayload(event, payload);
}
function assignForwardedToolPayload(event, payload) {
	const snapshot = JSON.parse(JSON.stringify(payload));
	if (Array.isArray(snapshot.locations)) event.locations = snapshot.locations;
	if (Object.prototype.hasOwnProperty.call(snapshot, "rawInput")) event.rawInput = snapshot.rawInput;
	if (Object.prototype.hasOwnProperty.call(snapshot, "rawOutput")) event.rawOutput = snapshot.rawOutput;
	if (Array.isArray(snapshot.content)) event.content = snapshot.content;
}
function runtimeEventFromMessage(message) {
	if (!isRecord(message)) return null;
	if (asTrimmedString(message.method) !== "session/update") return message.type === "client_operation" ? clientOperationEvent(message) : null;
	const params = message.params;
	if (!isRecord(params) || !isRecord(params.update)) return null;
	const payload = params.update;
	const parser = promptEventParser(asTrimmedString(payload.sessionUpdate));
	return parser ? parser(payload) : null;
}
const PROMPT_EVENT_PARSERS = {
	tool_call: (payload) => createToolCallEvent({
		payload,
		tag: "tool_call"
	}),
	tool_call_update: (payload) => createToolCallEvent({
		payload,
		tag: "tool_call_update"
	}),
	agent_message_chunk: (payload) => resolveTextChunk({
		payload,
		stream: "output",
		tag: "agent_message_chunk"
	}),
	agent_thought_chunk: (payload) => resolveTextChunk({
		payload,
		stream: "thought",
		tag: "agent_thought_chunk"
	}),
	usage_update: usageUpdateEvent,
	available_commands_update: availableCommandsUpdateEvent,
	current_mode_update: (payload) => statusUpdateEvent("current_mode_update", payload),
	config_option_update: (payload) => statusUpdateEvent("config_option_update", payload),
	session_info_update: (payload) => statusUpdateEvent("session_info_update", payload),
	plan: planUpdateEvent
};
function promptEventParser(type) {
	return Object.hasOwn(PROMPT_EVENT_PARSERS, type) ? PROMPT_EVENT_PARSERS[type] : void 0;
}
function usageUpdateEvent(payload) {
	const used = asOptionalFiniteNumber(payload.used);
	const size = asOptionalFiniteNumber(payload.size);
	const meta = isRecord(payload._meta) ? payload._meta : void 0;
	return buildUsageUpdateEvent({
		used,
		size,
		cost: normalizeUsageCost(payload.cost),
		breakdown: normalizeUsageBreakdown(meta?.usage)
	});
}
function buildUsageUpdateEvent(parts) {
	const { used, size, cost, breakdown } = parts;
	return {
		type: "status",
		text: used != null && size != null ? `usage updated: ${used}/${size}` : "usage updated",
		tag: "usage_update",
		...used != null ? { used } : {},
		...size != null ? { size } : {},
		...cost ? { cost } : {},
		...breakdown ? { breakdown } : {}
	};
}
function availableCommandsUpdateEvent(payload) {
	const raw = Array.isArray(payload.availableCommands) ? payload.availableCommands : [];
	const availableCommands = [];
	for (const entry of raw) {
		if (!isRecord(entry)) continue;
		const name = asTrimmedString(entry.name);
		if (!name) continue;
		const description = asTrimmedString(entry.description);
		availableCommands.push({
			name,
			...description ? { description } : {},
			hasInput: entry.input != null
		});
	}
	return {
		type: "status",
		text: availableCommands.length > 0 ? `available commands updated (${availableCommands.length})` : "available commands updated",
		tag: "available_commands_update",
		availableCommands
	};
}
function normalizeUsageCost(value) {
	if (!isRecord(value)) return;
	const amount = asOptionalFiniteNumber(value.amount);
	const currency = asTrimmedString(value.currency);
	if (amount == null && !currency) return;
	return {
		...amount != null ? { amount } : {},
		...currency ? { currency } : {}
	};
}
const USAGE_BREAKDOWN_FIELDS = [
	["inputTokens", ["inputTokens", "input_tokens"]],
	["outputTokens", ["outputTokens", "output_tokens"]],
	["cachedReadTokens", [
		"cachedReadTokens",
		"cacheReadInputTokens",
		"cache_read_input_tokens"
	]],
	["cachedWriteTokens", [
		"cachedWriteTokens",
		"cacheCreationInputTokens",
		"cache_creation_input_tokens"
	]],
	["thoughtTokens", ["thoughtTokens", "thought_tokens"]],
	["totalTokens", ["totalTokens", "total_tokens"]]
];
function normalizeUsageBreakdown(value) {
	if (!isRecord(value)) return;
	const breakdown = {};
	for (const [key, aliases] of USAGE_BREAKDOWN_FIELDS) {
		const v = firstFiniteNumber(value, aliases);
		if (v != null) breakdown[key] = v;
	}
	return Object.keys(breakdown).length > 0 ? breakdown : void 0;
}
function firstFiniteNumber(record, keys) {
	for (const key of keys) {
		const value = asOptionalFiniteNumber(record[key]);
		if (value != null) return value;
	}
}
function statusUpdateEvent(tag, payload) {
	const text = resolveStatusTextForTag({
		tag,
		payload
	});
	if (!text) return null;
	return {
		type: "status",
		text,
		tag
	};
}
function clientOperationEvent(payload) {
	return {
		type: "status",
		text: [
			asTrimmedString(payload.method) || "operation",
			asTrimmedString(payload.status),
			asTrimmedString(payload.summary)
		].filter(Boolean).join(" ")
	};
}
// src/runtime/public/probe.ts
/**
* Inspect session model metadata without a runtime store or prompt. The agent
* may create native session history; only its owned transport is closed.
*/
async function inspectAgentModels(options) {
	options.signal?.throwIfAborted();
	const timeoutMs = options.timeoutMs ?? 12e4;
	if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) throw new RangeError("Model inspection timeoutMs must be positive and at most 2147483647");
	const deadline = new AbortController();
	const signal = options.signal ? AbortSignal.any([options.signal, deadline.signal]) : deadline.signal;
	const assertActive = () => signal.throwIfAborted();
	const client = new AcpClient({
		...normalizeAgentCommandInput(options.agentCommand),
		cwd: options.cwd,
		agentProcessEnv: options.agentProcessEnv,
		agentProcessEnvAllow: options.agentProcessEnvAllow,
		agentProcessEnvBlock: options.agentProcessEnvBlock,
		permissionMode: "deny-all",
		nonInteractivePermissions: "deny",
		fs: false,
		terminal: false,
		processLifecycle: {
			onBeforeSpawn: assertActive,
			onSpawned: assertActive
		}
	});
	const timer = setTimeout(() => deadline.abort(new TimeoutError(timeoutMs)), timeoutMs);
	let onAbort;
	const aborted = new Promise((_resolve, reject) => {
		onAbort = () => reject(signal.reason);
		signal.addEventListener("abort", onAbort, { once: true });
	});
	const inspection = (async () => {
		assertActive();
		await client.start();
		assertActive();
		const { models } = await client.createSession();
		assertActive();
		return models ? {
			currentModelId: models.currentModelId,
			availableModelIds: models.availableModels.map((model) => model.modelId),
			availableModels: models.availableModels
		} : void 0;
	})();
	try {
		return await Promise.race([inspection, aborted]);
	} finally {
		clearTimeout(timer);
		signal.removeEventListener("abort", onAbort);
		try {
			if (signal.aborted) await client.close();
		} finally {
			await inspection.catch(() => {});
			await client.close();
		}
	}
}
function isPrimitiveDetail(value) {
	return value == null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol";
}
function formatFunctionDetail(value) {
	return value.name ? `[Function ${value.name}]` : "[Function]";
}
function serializeRuntimeDetail(value) {
	const seen = /* @__PURE__ */ new WeakSet();
	return JSON.stringify(value, (_key, nested) => {
		if (nested instanceof Error) return nested.message || nested.name;
		if (nested && typeof nested === "object") {
			if (seen.has(nested)) return "[Circular]";
			seen.add(nested);
		}
		return nested;
	}) ?? "undefined";
}
function formatRuntimeDetail(value) {
	if (value instanceof Error) return value.message || value.name;
	if (typeof value === "string") return value;
	if (isPrimitiveDetail(value)) return String(value);
	if (typeof value === "function") return formatFunctionDetail(value);
	try {
		return serializeRuntimeDetail(value);
	} catch {
		return "unserializable object";
	}
}
function normalizeRuntimeDetails(details) {
	return details?.map((detail) => formatRuntimeDetail(detail));
}
async function probeRuntime(options, deps = {}) {
	const agentName = options.probeAgent?.trim() || "codex";
	const agentCommand = normalizeAgentCommandInput(options.agentRegistry.resolve(agentName));
	const client = createProbeClient(options, agentName, agentCommand, deps);
	try {
		await client.start();
		return {
			ok: true,
			message: "embedded ACP runtime ready",
			details: [
				`agent=${agentName}`,
				`command=${agentCommand.agentCommand}`,
				`cwd=${options.cwd}`,
				...client.initializeResult?.protocolVersion ? [`protocolVersion=${client.initializeResult.protocolVersion}`] : []
			]
		};
	} catch (error) {
		return {
			ok: false,
			message: "embedded ACP runtime probe failed",
			details: [
				`agent=${agentName}`,
				`command=${agentCommand.agentCommand}`,
				`cwd=${options.cwd}`,
				formatRuntimeDetail(error)
			]
		};
	} finally {
		await client.close().catch(() => {});
	}
}
function createProbeClient(options, agentName, agentCommand, deps) {
	const clientOptions = {
		...agentCommand,
		cwd: options.cwd,
		agentProcessEnv: options.agentProcessEnv,
		agentProcessEnvAllow: options.agentProcessEnvAllow,
		agentProcessEnvBlock: options.agentProcessEnvBlock,
		mcpServers: typeof options.mcpServers === "function" ? [] : [...options.mcpServers ?? []],
		permissionMode: options.permissionMode,
		nonInteractivePermissions: options.nonInteractivePermissions,
		permissionPolicy: options.permissionPolicy,
		fs: false,
		terminal: false,
		processLifecycle: options.processLifecycle,
		processLaunchScope: {
			kind: "runtime-probe",
			agent: agentName
		},
		verbose: options.verbose
	};
	return deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
}
// src/runtime/engine/reuse-policy.ts
function shouldReuseExistingRecord(record, params) {
	if (record.acpx?.reset_on_next_ensure === true) return false;
	if (path.resolve(record.cwd) !== path.resolve(params.cwd)) return false;
	if (record.agentCommand !== params.agentCommand) return false;
	if (!sameArgv(record.agentArgv, params.agentArgv)) return false;
	if (params.resumeSessionId && record.acpSessionId !== params.resumeSessionId) return false;
	return true;
}
function sameArgv(left, right) {
	const leftValues = left ?? [];
	const rightValues = right ?? [];
	return leftValues.length === rightValues.length && leftValues.every((value, index) => value === rightValues[index]);
}
// src/runtime/engine/status.ts
function runtimeStatusFromRecord(record) {
	return {
		...record.lastRequestId ? { lastRequestId: record.lastRequestId } : {},
		summary: statusSummary(record),
		acpxRecordId: record.acpxRecordId,
		backendSessionId: record.acpSessionId,
		agentSessionId: record.agentSessionId,
		...buildModelsField(record),
		...buildUsageField(record),
		...buildAvailableCommandsField(record),
		details: {
			cwd: record.cwd,
			lastUsedAt: record.lastUsedAt,
			closed: record.closed === true,
			...record.acpx?.config_options !== void 0 ? { configOptions: structuredClone(record.acpx.config_options) } : {}
		}
	};
}
function statusSummary(record) {
	return [
		`session=${record.acpxRecordId}`,
		`backendSessionId=${record.acpSessionId}`,
		record.agentSessionId ? `agentSessionId=${record.agentSessionId}` : null,
		record.pid != null ? `pid=${record.pid}` : null,
		record.closed ? "closed" : "open"
	].filter(Boolean).join(" ");
}
function buildModelsField(record) {
	const { available_models: available = [], current_model_id: currentModelId } = record.acpx ?? {};
	if (available.length === 0 && currentModelId === void 0) return {};
	return { models: {
		...currentModelId !== void 0 ? { currentModelId } : {},
		availableModelIds: [...available],
		...buildModelNamesField(record.acpx)
	} };
}
function buildModelNamesField(state) {
	const models = advertisedModelState(state);
	return models && (models.configId || state?.available_model_names !== void 0) ? { availableModels: models.availableModels } : {};
}
function tokenUsageToBreakdown(usage) {
	if (!usage) return;
	const breakdown = {};
	assignUsageBreakdownField(breakdown, "inputTokens", usage.input_tokens);
	assignUsageBreakdownField(breakdown, "outputTokens", usage.output_tokens);
	assignUsageBreakdownField(breakdown, "cachedReadTokens", usage.cache_read_input_tokens);
	assignUsageBreakdownField(breakdown, "cachedWriteTokens", usage.cache_creation_input_tokens);
	assignUsageBreakdownField(breakdown, "thoughtTokens", usage.thought_tokens);
	assignUsageBreakdownField(breakdown, "totalTokens", usage.total_tokens);
	return Object.keys(breakdown).length > 0 ? breakdown : void 0;
}
function assignUsageBreakdownField(breakdown, key, value) {
	if (value !== void 0) breakdown[key] = value;
}
function buildUsageField(record) {
	const cumulative = tokenUsageToBreakdown(record.cumulative_token_usage);
	const perRequestEntries = Object.entries(record.request_token_usage ?? {}).map(([id, value]) => [id, tokenUsageToBreakdown(value)]).filter((entry) => entry[1] !== void 0);
	const perRequest = perRequestEntries.length > 0 ? Object.fromEntries(perRequestEntries) : void 0;
	const cost = record.cumulative_cost;
	const usage = {
		...cumulative ? { cumulative } : {},
		...cost ? { cost } : {},
		...perRequest ? { perRequest } : {}
	};
	return Object.keys(usage).length > 0 ? { usage } : {};
}
function buildAvailableCommandsField(record) {
	const commands = record.acpx?.available_commands;
	if (!commands || commands.length === 0) return {};
	const availableCommands = commands.map((command) => runtimeAvailableCommand(command)).filter((command) => command !== void 0);
	return availableCommands.length > 0 ? { availableCommands } : {};
}
function runtimeAvailableCommand(command) {
	if (typeof command === "string") {
		const name = command.trim();
		return name ? { name } : void 0;
	}
	const record = commandRecord(command);
	if (!record) return;
	const name = asOptionalString(record.name);
	if (!name) return;
	const runtimeCommand = { name };
	const description = asOptionalString(record.description);
	if (description) runtimeCommand.description = description;
	if (typeof record.has_input === "boolean") runtimeCommand.hasInput = record.has_input;
	return runtimeCommand;
}
function commandRecord(value) {
	if (!value || typeof value !== "object" || Array.isArray(value)) return;
	return value;
}
// src/runtime/engine/turn.ts
function createDeferred() {
	let resolve;
	let reject;
	return {
		promise: new Promise((res, rej) => {
			resolve = res;
			reject = rej;
		}),
		resolve,
		reject
	};
}
var AsyncEventQueue = class {
	items = [];
	waits = [];
	closed = false;
	push(item) {
		if (this.closed) return;
		const waiter = this.waits.shift();
		if (waiter) {
			waiter.resolve(item);
			return;
		}
		this.items.push(item);
	}
	close() {
		if (this.closed) return;
		this.closed = true;
		for (const waiter of this.waits.splice(0)) waiter.resolve(null);
	}
	clear() {
		this.items.length = 0;
	}
	async next() {
		if (this.items.length > 0) return this.items.shift() ?? null;
		if (this.closed) return null;
		const waiter = createDeferred();
		this.waits.push(waiter);
		return await waiter.promise;
	}
	async *iterate() {
		try {
			while (true) {
				const next = await this.next();
				if (!next) return;
				yield next;
			}
		} finally {
			this.close();
			this.clear();
		}
	}
};
function toPromptInput(text, attachments) {
	if (!attachments || attachments.length === 0) return text;
	const blocks = [];
	if (text) blocks.push({
		type: "text",
		text
	});
	for (const attachment of attachments) {
		if (attachment.mediaType.startsWith("image/")) {
			blocks.push({
				type: "image",
				mimeType: attachment.mediaType,
				data: attachment.data
			});
			continue;
		}
		if (attachment.mediaType.startsWith("audio/")) {
			blocks.push({
				type: "audio",
				mimeType: attachment.mediaType,
				data: attachment.data
			});
			continue;
		}
		throw new AcpRuntimeError("ACP_TURN_FAILED", `Unsupported ACP runtime attachment media type: ${attachment.mediaType}`);
	}
	return blocks.length > 0 ? blocks : textPrompt(text);
}
function legacyTerminalEventFromTurnResult(result) {
	if (result.status === "failed") return {
		type: "error",
		message: result.error.message,
		...result.error.code ? { code: result.error.code } : {},
		...result.error.detailCode ? { detailCode: result.error.detailCode } : {},
		...result.error.retryable === void 0 ? {} : { retryable: result.error.retryable }
	};
	return {
		type: "done",
		...result.stopReason ? { stopReason: result.stopReason } : {},
		...result._meta === void 0 ? {} : { _meta: result._meta }
	};
}
// src/runtime/engine/manager.ts
async function settleAttempt(run) {
	try {
		return {
			ok: true,
			value: await run()
		};
	} catch (error) {
		return {
			ok: false,
			error
		};
	}
}
function firstFailedAttempt(attempts) {
	return attempts.find((attempt) => !attempt.ok);
}
function isoNow() {
	return (/* @__PURE__ */ new Date()).toISOString();
}
function isUnsupportedSessionCloseError(error) {
	const acp = extractAcpError(error);
	if (!acp) return false;
	if (acp.code === -32601 || acp.code === -32602) return true;
	if (acp.code !== -32603 || !acp.data || typeof acp.data !== "object") return false;
	const details = acp.data.details;
	return typeof details === "string" && details.toLowerCase().includes("invalid params");
}
function createRecordId(sessionKey, mode) {
	if (mode === "persistent") return sessionKey;
	return `${sessionKey}:oneshot:${randomUUID()}`;
}
function resumePolicyForSessionMode(mode) {
	return mode === "persistent" ? "same-session-only" : "allow-new";
}
async function startRuntimeClient(client, agent, timeoutMs) {
	try {
		await withTimeout(client.start(), timeoutMs);
	} catch (error) {
		if (!(error instanceof TimeoutError)) throw error;
		throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", `ACP agent ${path.basename(agent.agentArgv?.[0] ?? agent.agentCommand.split(/\s+/)[0])} did not complete ACP initialization within ${timeoutMs}ms. Check that the configured command starts an ACP server.`, { cause: error });
	}
}
async function withSessionSetupTimeout(method, request, timeoutMs) {
	try {
		return await withTimeout(request, timeoutMs);
	} catch (error) {
		if (!(error instanceof TimeoutError)) throw error;
		throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", `ACP ${method} did not complete within ${timeoutMs}ms. Retry session setup and check that the configured ACP agent is responding.`, { cause: error });
	}
}
async function createOrLoadRuntimeSession(client, resumeSessionId, cwd, timeoutMs) {
	if (resumeSessionId) {
		if (client.supportsResumeSession()) {
			const resumed = await withSessionSetupTimeout("session/resume", client.resumeSession(resumeSessionId, cwd), timeoutMs);
			return {
				sessionId: resumeSessionId,
				agentSessionId: resumed.agentSessionId,
				sessionResult: resumed
			};
		}
		if (!client.supportsLoadSession()) throw new Error(`Agent does not support session/resume or session/load; cannot resume session ${resumeSessionId}`);
		const loaded = await withSessionSetupTimeout("session/load", client.loadSession(resumeSessionId, cwd), timeoutMs);
		return {
			sessionId: resumeSessionId,
			agentSessionId: loaded.agentSessionId,
			sessionResult: loaded
		};
	}
	const created = await withSessionSetupTimeout("session/new", client.createSession(cwd), timeoutMs);
	return {
		sessionId: created.sessionId,
		agentSessionId: created.agentSessionId,
		sessionResult: created
	};
}
var AcpRuntimeManager = class {
	options;
	deps;
	exactSessionRecordIds = /* @__PURE__ */ new Set();
	activeControllers = /* @__PURE__ */ new Map();
	controlSessionRecords = /* @__PURE__ */ new Map();
	retainedSessionOwners = /* @__PURE__ */ new Map();
	retiringSessionOwners = /* @__PURE__ */ new Set();
	pendingOneShotRecordIds = /* @__PURE__ */ new Map();
	ensureSessionLocks = /* @__PURE__ */ new Map();
	runtimeOperationLocks = /* @__PURE__ */ new Map();
	sessionTasks = /* @__PURE__ */ new Map();
	closingActiveRecords = /* @__PURE__ */ new Set();
	liveClients = /* @__PURE__ */ new Set();
	pendingTasks = /* @__PURE__ */ new Set();
	shuttingDown = false;
	shutdownTask;
	observeAcpMessage = (direction, message) => {
		try {
			Promise.resolve(this.options.onAcpMessage?.(direction, message)).catch(() => {});
		} catch {}
	};
	constructor(options, deps = {}) {
		this.options = options;
		this.deps = deps;
	}
	createClient(options) {
		this.assertOpen();
		const clientOptions = {
			...options,
			onAcpMessage: options.processLaunchScope?.kind === "runtime-session" ? this.observeAcpMessage : void 0,
			agentProcessEnv: this.options.agentProcessEnv,
			agentProcessEnvAllow: this.options.agentProcessEnvAllow,
			agentProcessEnvBlock: this.options.agentProcessEnvBlock,
			processLifecycle: {
				...options.processLifecycle,
				onBeforeSpawn: async (launch) => {
					this.assertOpen();
					await options.processLifecycle?.onBeforeSpawn?.(launch);
					this.assertOpen();
				},
				onSpawned: async (process) => {
					this.assertOpen();
					await options.processLifecycle?.onSpawned?.(process);
					this.assertOpen();
				}
			}
		};
		const client = this.deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
		this.liveClients.add(client);
		return client;
	}
	async probe() {
		let client;
		try {
			return await probeRuntime(this.options, { clientFactory: (options) => client = this.createClient(options) });
		} finally {
			if (client) this.liveClients.delete(client);
		}
	}
	assertOpen() {
		if (this.shuttingDown) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
	}
	shutdown() {
		if (!this.shutdownTask) {
			this.shuttingDown = true;
			this.shutdownTask = this.shutdownOwnedClients();
		}
		return this.shutdownTask;
	}
	async shutdownOwnedClients() {
		const closed = await Promise.allSettled([...this.liveClients].map(async (client) => {
			client.requestCancelActivePrompt().catch(() => {});
			await this.closeClient(client);
		}));
		await Promise.allSettled([
			...this.ensureSessionLocks.values(),
			...this.runtimeOperationLocks.values(),
			...this.pendingTasks
		]);
		const owners = /* @__PURE__ */ new Set([...this.retainedSessionOwners.values(), ...this.retiringSessionOwners]);
		const stopped = await Promise.allSettled([...owners].map(async (owner) => {
			this.removeRetainedSessionOwner(owner);
			const retirement = owner.retirement;
			if (retirement) await retirement;
			else {
				const failure = await this.stopSessionOwner(owner);
				if (failure) throw failure.error;
			}
			this.retiringSessionOwners.delete(owner);
		}));
		const errors = [];
		for (const result of [...closed, ...stopped]) if (result.status === "rejected") errors.push(result.reason);
		if (errors.length) throw new AggregateError(errors, "ACP runtime shutdown failed.");
	}
	trackTask(task) {
		this.pendingTasks.add(task);
		const remove = () => {
			this.pendingTasks.delete(task);
		};
		task.then(remove, remove);
		return task;
	}
	queueSessionTask(recordId, run, cancel) {
		const tasks = this.sessionTasks.get(recordId) ?? /* @__PURE__ */ new Set();
		this.sessionTasks.set(recordId, tasks);
		const previous = [...tasks].at(-1)?.completion;
		const result = (async () => {
			await previous?.catch(() => {});
			return await run();
		})();
		const task = {
			completion: result.then(() => {}),
			cancel
		};
		task.completion.catch(() => {});
		tasks.add(task);
		const remove = () => {
			tasks.delete(task);
			if (tasks.size === 0) this.sessionTasks.delete(recordId);
		};
		result.then(remove, remove);
		return this.trackTask(result);
	}
	async cancelSessionTasks(recordId) {
		const tasks = [...this.sessionTasks.get(recordId) ?? []];
		await Promise.all(tasks.flatMap((task) => task.cancel ? [task.cancel()] : []));
		return tasks;
	}
	async closeClient(client) {
		await client.close();
		this.liveClients.delete(client);
	}
	createSessionOwner(input) {
		const owner = {
			...input,
			checkpoint: new LiveSessionCheckpoint({ save: async () => this.saveSessionOwner(owner) }),
			bufferSessionUpdates: false,
			pendingSessionUpdates: []
		};
		input.client.setEventHandlers({
			onAcpMessage: this.observeAcpMessage,
			onSessionUpdate: (notification) => this.routeOwnedSessionUpdate(owner, notification),
			onClientOperation: (operation) => this.routeOwnedClientOperation(owner, operation)
		});
		return owner;
	}
	async saveSessionOwner(owner) {
		if (!owner.recordId || owner.pendingResetRecord) return;
		const active = owner.activeTurn?.turn;
		const target = active ?? owner.projection;
		if (!target) return;
		const { record, conversation } = target;
		if (active?.connected) record.acpx = active.acpxState;
		record.lastUsedAt = isoNow();
		applyConversation(record, conversation);
		applyLifecycleSnapshotToRecord(record, owner.client.getAgentLifecycleSnapshot());
		await this.refreshClosedState(record);
		await this.options.sessionStore.save(record);
	}
	routeOwnedSessionUpdate(owner, notification) {
		const active = owner.activeTurn;
		if (active) {
			const { task, turn } = active;
			if (turn.connected) {
				turn.acpxState = recordSessionUpdate(turn.conversation, turn.acpxState, notification);
				owner.checkpoint.request();
			} else turn.record.acpx = recordSessionUpdate(turn.conversation, turn.record.acpx, notification);
			this.emitRuntimeTurnEvent(task, {
				jsonrpc: "2.0",
				method: "session/update",
				params: notification
			});
			return;
		}
		if (owner.bufferSessionUpdates) {
			owner.pendingSessionUpdates.push(notification);
			return;
		}
		const projection = owner.projection;
		if (!projection) {
			owner.pendingSessionUpdates.push(notification);
			return;
		}
		projection.record.acpx = recordSessionUpdate(projection.conversation, projection.record.acpx, notification);
		owner.checkpoint.request();
	}
	routeOwnedClientOperation(owner, operation) {
		const active = owner.activeTurn;
		if (!active) return;
		const { task, turn } = active;
		if (turn.connected) {
			turn.acpxState = recordClientOperation(turn.conversation, turn.acpxState, operation);
			owner.checkpoint.request();
		} else turn.record.acpx = recordClientOperation(turn.conversation, turn.record.acpx, operation);
		this.emitRuntimeTurnEvent(task, {
			type: "client_operation",
			...operation
		});
	}
	attachIdleProjection(owner, record, conversation = cloneSessionConversation(record), acpxState = record.acpx) {
		record.acpx = acpxState;
		owner.projection = {
			record,
			conversation
		};
		owner.activeTurn = void 0;
		this.drainPendingSessionUpdates(owner);
	}
	drainPendingSessionUpdates(owner) {
		for (const notification of owner.pendingSessionUpdates.splice(0)) this.routeOwnedSessionUpdate(owner, notification);
	}
	async flushSessionOwner(owner) {
		await owner.client.waitForSessionUpdatesIdle?.().catch(() => {});
		await owner.checkpoint.flush();
	}
	removeRetainedSessionOwner(owner) {
		if (owner.recordId && this.retainedSessionOwners.get(owner.recordId) === owner) this.retainedSessionOwners.delete(owner.recordId);
		if (owner.mode === "oneshot" && owner.recordId && this.pendingOneShotRecordIds.get(owner.sessionKey) === owner.recordId) this.pendingOneShotRecordIds.delete(owner.sessionKey);
	}
	async readRetainedSessionOwner(record, options) {
		const owner = this.retainedSessionOwners.get(record.acpxRecordId);
		if (!owner) return;
		await this.flushSessionOwner(owner);
		const projectedRecord = owner.projection?.record;
		if (projectedRecord && projectedRecord !== record) Object.assign(record, structuredClone(projectedRecord));
		if (!owner.client.hasReusableSession(record.acpSessionId)) {
			this.removeRetainedSessionOwner(owner);
			await this.stopSessionOwner(owner);
			return;
		}
		if (options.consume) this.removeRetainedSessionOwner(owner);
		return owner;
	}
	async closeRetainedSessionOwner(recordId) {
		const owner = this.retainedSessionOwners.get(recordId);
		if (!owner) return;
		this.removeRetainedSessionOwner(owner);
		await this.stopSessionOwner(owner);
	}
	async stopSessionOwner(owner) {
		const flushAttempt = await settleAttempt(async () => this.flushSessionOwner(owner));
		const closeAttempt = await settleAttempt(async () => this.closeClient(owner.client));
		if (!closeAttempt.ok) this.retiringSessionOwners.add(owner);
		const finalFlushAttempt = await settleAttempt(async () => owner.checkpoint.flush());
		const clearAttempt = await settleAttempt(() => owner.client.clearEventHandlers());
		const settleRecordAttempt = await settleAttempt(async () => {
			if (closeAttempt.ok) await this.settleRetiredSessionRecord(owner);
		});
		if (!settleRecordAttempt.ok) this.retiringSessionOwners.add(owner);
		return firstFailedAttempt([
			flushAttempt,
			closeAttempt,
			finalFlushAttempt,
			clearAttempt,
			settleRecordAttempt
		]);
	}
	async retrySessionCleanup(recordId) {
		for (const owner of this.retiringSessionOwners) if (owner.recordId === recordId) {
			owner.retirement ??= this.finalizeSessionConnection({
				client: owner.client,
				owner
			}).then(async () => {
				await this.settleRetiredSessionRecord(owner);
				this.retiringSessionOwners.delete(owner);
			}).finally(() => {
				owner.retirement = void 0;
			});
			await owner.retirement;
		}
	}
	async refreshClosedState(record) {
		if (!this.closingActiveRecords.has(record.acpxRecordId)) return record.closed === true;
		const latest = await this.options.sessionStore.load(record.acpxRecordId).catch(() => void 0);
		record.closed = true;
		record.closedAt = latest?.closedAt ?? record.closedAt ?? isoNow();
		if (latest?.acpx?.reset_on_next_ensure === true) record.acpx = {
			...record.acpx,
			reset_on_next_ensure: true
		};
		return true;
	}
	async retainPersistentSessionOwnerAfterTurn(input) {
		const { record, owner } = input;
		if (!this.canRetainPersistentSessionOwner(owner, record)) return false;
		const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
		this.retainedSessionOwners.set(record.acpxRecordId, owner);
		if (previousOwner && previousOwner !== owner) {
			this.removeRetainedSessionOwner(previousOwner);
			await this.stopSessionOwner(previousOwner);
		}
		return true;
	}
	canRetainPersistentSessionOwner(owner, record) {
		return !this.shuttingDown && owner.mode === "persistent" && !record.closed && !(owner.client.hasUnresolvedPrompt?.() ?? false) && owner.client.hasReusableSession(record.acpSessionId);
	}
	resolveMcpServers(session) {
		const servers = this.options.mcpServers;
		const { sessionKey, cwd, agentCommand, agentArgv } = session;
		return [...typeof servers === "function" ? servers({
			sessionKey,
			cwd,
			agentCommand,
			agentArgv: agentArgv ? [...agentArgv] : void 0
		}) : servers ?? []];
	}
	resolveSessionPermissions(session) {
		const { sessionKey, cwd, agentCommand, agentArgv } = session;
		const { permissionMode = this.options.permissionMode, nonInteractivePermissions = this.options.nonInteractivePermissions, permissionPolicy = this.options.permissionPolicy, onPermissionRequest = this.options.onPermissionRequest } = this.options.sessionPermissions?.({
			sessionKey,
			cwd,
			agentCommand,
			agentArgv: agentArgv ? [...agentArgv] : void 0
		}) ?? {};
		return {
			permissionMode,
			nonInteractivePermissions,
			permissionPolicy,
			onPermissionRequest
		};
	}
	resolveClientCapabilities() {
		return {
			fs: this.options.fs,
			terminal: this.options.terminal
		};
	}
	async withRuntimeControlSession(record, sessionMode, run, replayOptions, authority) {
		await this.retrySessionCleanup(record.acpxRecordId);
		assertControlAuthority(authority);
		const owner = await this.readRetainedSessionOwner(record, { consume: false });
		assertControlAuthority(authority);
		if (owner) {
			const ownedRecord = owner.projection?.record ?? record;
			try {
				const value = await run({
					client: owner.client,
					sessionId: ownedRecord.acpSessionId,
					record: ownedRecord
				});
				this.refreshOwnedRecordLifecycle(owner, ownedRecord);
				await owner.checkpoint.checkpoint();
				return value;
			} finally {
				await this.flushSessionOwner(owner);
			}
		}
		let controlClient;
		let controlledRecord = record;
		const checkpoint = new LiveSessionCheckpoint({ save: async () => this.options.sessionStore.save(controlledRecord) });
		try {
			const result = await settleAttempt(async () => withConnectedSession({
				sessionRecordId: record.acpxRecordId,
				loadRecord: async (sessionRecordId) => {
					controlledRecord = await this.requireRecord(sessionRecordId);
					this.controlSessionRecords.set(sessionRecordId, {
						record: controlledRecord,
						checkpoint
					});
					return controlledRecord;
				},
				saveRecord: async () => checkpoint.checkpoint(),
				createClient: (options) => controlClient = this.createClient({
					...options,
					processLifecycle: this.options.processLifecycle,
					processLaunchScope: {
						kind: "runtime-session",
						sessionKey: record.name ?? record.acpxRecordId
					}
				}),
				mcpServers: this.resolveMcpServers({
					...record,
					sessionKey: record.name ?? record.acpxRecordId
				}),
				...this.resolveSessionPermissions({
					...record,
					sessionKey: record.name ?? record.acpxRecordId
				}),
				...this.resolveClientCapabilities(),
				elicitationModes: this.options.elicitationModes,
				verbose: this.options.verbose,
				timeoutMs: this.options.timeoutMs,
				resumePolicy: resumePolicyForSessionMode(sessionMode),
				strictResume: this.exactSessionRecordIds.has(record.acpxRecordId),
				replacingMode: replayOptions?.replacingMode,
				replacingConfigOption: replayOptions?.replacingConfigOption,
				authority,
				run
			}));
			this.controlSessionRecords.delete(record.acpxRecordId);
			const flushed = await settleAttempt(async () => checkpoint.flush());
			if (!result.ok) throw result.error;
			if (!flushed.ok) throw flushed.error;
			return result.value.value;
		} finally {
			this.controlSessionRecords.delete(record.acpxRecordId);
			if (controlClient) this.liveClients.delete(controlClient);
		}
	}
	refreshOwnedRecordLifecycle(owner, record) {
		record.lastUsedAt = isoNow();
		record.closed = false;
		record.closedAt = void 0;
		record.protocolVersion = owner.client.initializeResult?.protocolVersion;
		record.agentCapabilities = owner.client.initializeResult?.agentCapabilities;
		applyLifecycleSnapshotToRecord(record, owner.client.getAgentLifecycleSnapshot());
	}
	async ensureSession(input) {
		return await this.withEnsureSessionLock(input, async () => {
			const record = await this.ensureSessionWithOwnership(input);
			if (input.resumeSessionId && !this.exactSessionRecordIds.has(record.acpxRecordId)) {
				this.assertRuntimeSessionIdle(record.acpxRecordId);
				this.exactSessionRecordIds.add(record.acpxRecordId);
			}
			return record;
		});
	}
	async withEnsureSessionLock(input, run) {
		const key = `${input.mode}\0${input.sessionKey}`;
		return await this.withManagerLock(this.ensureSessionLocks, key, run);
	}
	async withManagerLock(locks, key, run, authority) {
		this.assertOpen();
		assertControlAuthority(authority);
		return await this.serializeManagerOperation(locks, key, async () => {
			this.assertOpen();
			assertControlAuthority(authority);
			return await run();
		});
	}
	async serializeManagerOperation(locks, key, run) {
		const previous = locks.get(key) ?? Promise.resolve();
		let release;
		const gate = new Promise((resolve) => {
			release = resolve;
		});
		const tail = previous.then(() => gate);
		locks.set(key, tail);
		await previous;
		try {
			return await run();
		} finally {
			release();
			if (locks.get(key) === tail) locks.delete(key);
		}
	}
	async ensureSessionWithOwnership(input) {
		const cwd = path.resolve(input.cwd?.trim() || this.options.cwd);
		const { agentCommand, agentArgv } = normalizeAgentCommandInput(this.options.agentRegistry.resolve(input.agent));
		const agent = {
			cwd,
			agentCommand,
			agentArgv
		};
		if (input.mode !== "persistent") return await this.acquireRuntimeSession(input, agent);
		const compatible = await this.reuseCompatiblePersistentSession(input, agent);
		if (compatible) return compatible;
		this.assertRuntimeSessionIdle(input.sessionKey);
		return await this.withManagerLock(this.runtimeOperationLocks, input.sessionKey, async () => this.acquireRuntimeSession(input, agent));
	}
	async reuseCompatiblePersistentSession(input, agent) {
		await this.retrySessionCleanup(input.sessionKey);
		const controlled = this.controlSessionRecords.get(input.sessionKey);
		const record = controlled ? controlled.record : await this.findSession(input.sessionKey);
		if (!record || this.closingActiveRecords.has(input.sessionKey) || !shouldReuseExistingRecord(record, {
			...agent,
			resumeSessionId: input.resumeSessionId
		})) return;
		if (!record.closed) return record;
		if (!controlled || this.sessionTasks.has(input.sessionKey)) return;
		return await this.reuseRuntimeSession({ record }, controlled.checkpoint);
	}
	async acquireRuntimeSession(input, agent) {
		const existing = await this.loadExistingRuntimeSession(input);
		if (existing && this.canReuseRuntimeSession(input, agent, existing)) return await this.reuseRuntimeSession(existing);
		await this.closeConflictingPersistentSession(input, existing?.owner);
		return await this.createOwnedRuntimeSession(input, agent);
	}
	async loadExistingRuntimeSession(input) {
		const existingRecordId = input.mode === "persistent" ? input.sessionKey : this.pendingOneShotRecordIds.get(input.sessionKey);
		if (!existingRecordId) return;
		await this.retrySessionCleanup(existingRecordId);
		let record = await this.findSession(existingRecordId);
		if (!record) return;
		const owner = this.retainedSessionOwners.get(record.acpxRecordId);
		if (owner) {
			await this.flushSessionOwner(owner);
			record = owner.projection?.record ?? record;
		}
		return {
			record,
			owner
		};
	}
	async findSession(sessionKey) {
		return await this.options.sessionStore.load(sessionKey);
	}
	canReuseRuntimeSession(input, agent, existing) {
		if (!shouldReuseExistingRecord(existing.record, {
			cwd: agent.cwd,
			agentCommand: agent.agentCommand,
			agentArgv: agent.agentArgv,
			resumeSessionId: input.resumeSessionId
		})) return false;
		if (input.mode === "persistent") return true;
		return Boolean(existing.owner && !existing.record.closed && this.pendingOneShotRecordIds.get(input.sessionKey) === existing.record.acpxRecordId && this.retainedSessionOwners.get(existing.record.acpxRecordId) === existing.owner && isDeepStrictEqual(sessionOptionsFromRecord(existing.record), normalizeSessionOptionShape(input.sessionOptions)));
	}
	assertRuntimeSessionIdle(recordId) {
		if (this.sessionTasks.has(recordId) || this.runtimeOperationLocks.has(recordId)) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", `Cannot ensure session ${recordId} with incompatible or closing state while its work is unfinished. Wait for existing work to finish or use a different session key.`);
	}
	async reuseRuntimeSession({ record, owner }, checkpoint = owner?.checkpoint) {
		if (!record.closed && !this.closingActiveRecords.has(record.acpxRecordId)) return record;
		record.closed = false;
		record.closedAt = void 0;
		this.closingActiveRecords.delete(record.acpxRecordId);
		if (checkpoint) await checkpoint.checkpoint();
		else await this.options.sessionStore.save(record);
		return record;
	}
	async closeConflictingPersistentSession(input, owner) {
		if (input.mode === "persistent" && owner?.recordId) {
			this.removeRetainedSessionOwner(owner);
			this.retiringSessionOwners.add(owner);
			await this.retrySessionCleanup(owner.recordId);
		}
	}
	async createOwnedRuntimeSession(input, agent) {
		const { cwd, agentCommand, agentArgv } = agent;
		const client = this.createClient({
			agentCommand,
			agentArgv,
			cwd,
			mcpServers: this.resolveMcpServers({
				...agent,
				sessionKey: input.sessionKey
			}),
			...this.resolveSessionPermissions({
				...agent,
				sessionKey: input.sessionKey
			}),
			...this.resolveClientCapabilities(),
			elicitationModes: this.options.elicitationModes,
			processLifecycle: this.options.processLifecycle,
			processLaunchScope: {
				kind: "runtime-session",
				sessionKey: input.sessionKey
			},
			verbose: this.options.verbose,
			sessionOptions: input.sessionOptions
		});
		const owner = this.createSessionOwner({
			client,
			sessionKey: input.sessionKey,
			mode: input.mode
		});
		try {
			await startRuntimeClient(client, agent, this.options.timeoutMs);
			this.assertOpen();
			const session = await createOrLoadRuntimeSession(client, input.resumeSessionId, cwd, this.options.timeoutMs);
			const record = await this.prepareInitialRuntimeRecord({
				input,
				client,
				owner,
				agentCommand,
				agentArgv,
				cwd,
				session
			});
			await this.retainInitializedSessionOwner(owner, record);
			if (!input.resumeSessionId) this.exactSessionRecordIds.delete(record.acpxRecordId);
			return record;
		} catch (error) {
			await settleAttempt(async () => this.abandonInitializingSessionOwner(owner));
			throw error;
		}
	}
	async abandonInitializingSessionOwner(owner) {
		const record = owner.recordId ? owner.projection?.record : void 0;
		owner.pendingResetRecord = record;
		await settleAttempt(() => owner.client.clearEventHandlers());
		const closeAttempt = await settleAttempt(async () => this.closeClient(owner.client));
		if (!record) return;
		this.retiringSessionOwners.add(owner);
		if (closeAttempt.ok) {
			await this.settleRetiredSessionRecord(owner);
			this.retiringSessionOwners.delete(owner);
		}
	}
	async settleRetiredSessionRecord(owner) {
		const record = owner.pendingResetRecord;
		if (!record) return;
		applyLifecycleSnapshotToRecord(record, owner.client.getAgentLifecycleSnapshot());
		await this.saveClosedRuntimeRecord(record, true);
		owner.pendingResetRecord = void 0;
		owner.recordId = void 0;
	}
	async prepareInitialRuntimeRecord(params) {
		const { input, client, owner, agentCommand, agentArgv, cwd, session } = params;
		const record = createInitialSessionRecord({
			recordId: createRecordId(input.sessionKey, input.mode),
			name: input.sessionKey,
			sessionId: session.sessionId,
			sessionOrigin: input.resumeSessionId ? "resume" : "new",
			agentCommand,
			agentArgv,
			cwd,
			agentSessionId: session.agentSessionId
		});
		this.closingActiveRecords.delete(record.acpxRecordId);
		record.protocolVersion = client.initializeResult?.protocolVersion;
		record.agentCapabilities = client.initializeResult?.agentCapabilities;
		this.attachIdleProjection(owner, record);
		applyConfigOptionsToRecord(record, session.sessionResult);
		const modelApplication = await applyRequestedModelIfAdvertised({
			client,
			sessionId: session.sessionId,
			requestedModel: input.sessionOptions?.model,
			models: session.sessionResult.models,
			agentCommand,
			timeoutMs: this.options.timeoutMs
		});
		applyInitialModelSelection(record, session.sessionResult.models, modelApplication);
		applyLifecycleSnapshotToRecord(record, client.getAgentLifecycleSnapshot());
		persistSessionOptions(record, input.sessionOptions);
		return record;
	}
	async retainInitializedSessionOwner(owner, record) {
		owner.recordId = record.acpxRecordId;
		await owner.checkpoint.checkpoint();
		this.assertOpen();
		const previousOwner = this.retainedSessionOwners.get(record.acpxRecordId);
		this.retainedSessionOwners.set(record.acpxRecordId, owner);
		if (owner.mode === "oneshot") this.pendingOneShotRecordIds.set(owner.sessionKey, record.acpxRecordId);
		if (previousOwner && previousOwner !== owner) {
			this.removeRetainedSessionOwner(previousOwner);
			await this.stopSessionOwner(previousOwner);
		}
	}
	startTurn(input) {
		this.assertOpen();
		let promptInput;
		try {
			promptInput = toPromptInput(input.text, input.attachments);
		} catch (error) {
			this.trackTask(this.closeRetainedOneShotHandle(input.handle).catch(() => {}));
			throw error;
		}
		const queue = new AsyncEventQueue();
		const result = createDeferred();
		const promptStarted = createDeferred();
		promptStarted.promise.catch(() => {});
		const sessionReady = createDeferred();
		sessionReady.promise.catch(() => {});
		let resultSettled = false;
		const state = {
			pendingCancel: false,
			turnActive: true,
			activeController: null
		};
		let streamClosed = false;
		const settleResult = (next) => {
			if (resultSettled) return;
			resultSettled = true;
			result.resolve(next);
		};
		const closeStream = () => {
			if (streamClosed) return;
			streamClosed = true;
			queue.clear();
			queue.close();
		};
		const requestCancel = async () => {
			if (state.activeController) return await state.activeController.requestCancelActivePrompt();
			if (!state.turnActive) return false;
			state.pendingCancel = true;
			return true;
		};
		const abortHandler = () => {
			requestCancel().catch(() => {});
		};
		if (input.signal && !input.signal.aborted) input.signal.addEventListener("abort", abortHandler, { once: true });
		this.queueSessionTask(input.handle.acpxRecordId ?? input.handle.sessionKey, async () => this.runRuntimeTurnTask({
			input,
			promptInput,
			queue,
			promptStarted,
			sessionReady,
			state,
			settleResult,
			abortHandler
		}), requestCancel);
		return {
			requestId: input.requestId,
			promptStarted: promptStarted.promise,
			events: queue.iterate(),
			result: result.promise,
			cancel: async () => {
				await requestCancel();
			},
			closeStream: async () => {
				closeStream();
			}
		};
	}
	async closeRetainedOneShotHandle(handle) {
		const recordId = handle.acpxRecordId ?? handle.sessionKey;
		await this.serializeManagerOperation(this.runtimeOperationLocks, recordId, async () => {
			if (this.retainedSessionOwners.get(recordId)?.mode === "oneshot") await this.closeRetainedSessionOwner(recordId);
		});
	}
	async runRuntimeTurnTask(task) {
		let turn;
		let terminalResult;
		try {
			if (this.cancelRuntimeTurnBeforePrompt(task)) {
				await this.closeRetainedOneShotHandle(task.input.handle);
				terminalResult = {
					status: "cancelled",
					stopReason: "cancelled"
				};
			} else {
				turn = await this.prepareRuntimeTurn(task);
				const { sessionId } = await this.connectRuntimeTurn(task, turn);
				this.assertOpen();
				await this.resolveRuntimeTurnReady(task, turn);
				if (this.cancelRuntimeTurnBeforePrompt(task)) terminalResult = {
					status: "cancelled",
					stopReason: "cancelled"
				};
				else {
					const response = await this.runRuntimePrompt(task, turn, sessionId);
					this.updateCompletedRuntimeTurn(turn);
					terminalResult = {
						status: response.stopReason === "cancelled" ? "cancelled" : "completed",
						...response.stopReason ? { stopReason: response.stopReason } : {},
						...response._meta === void 0 ? {} : { _meta: response._meta }
					};
				}
			}
		} catch (error) {
			terminalResult = this.handleRuntimeTurnFailure(task, error);
		}
		const finalization = await settleAttempt(async () => this.serializeManagerOperation(this.runtimeOperationLocks, task.input.handle.acpxRecordId ?? task.input.handle.sessionKey, async () => this.finalizeRuntimeTurn(task, turn)));
		if (!finalization.ok) {
			task.settleResult(this.failRuntimeTurn(task, finalization.error));
			throw finalization.error;
		}
		task.settleResult(terminalResult);
	}
	async runRuntimePrompt(task, turn, sessionId) {
		return await runPromptTurn({
			client: turn.client,
			sessionId,
			prompt: task.promptInput,
			timeoutMs: task.input.timeoutMs ?? this.options.timeoutMs,
			conversation: turn.conversation,
			promptMessageId: turn.promptMessageId,
			onPromptRequestWritten: () => task.promptStarted.resolve(),
			onElicitation: task.input.onElicitation,
			onPermissionRequest: task.input.onPermissionRequest,
			authority: task.input
		});
	}
	async prepareRuntimeTurn(task) {
		const recordId = task.input.handle.acpxRecordId ?? task.input.handle.sessionKey;
		return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => {
			await this.retrySessionCleanup(recordId);
			return await this.prepareRuntimeTurnWithOwnership(task);
		});
	}
	async prepareRuntimeTurnWithOwnership(task) {
		const { record, retainedOwner, conversation, acpxState, promptMessageId } = await this.prepareRuntimeTurnState(task);
		try {
			const client = retainedOwner?.client ?? this.createTurnClient(record);
			const owner = this.resolveRuntimeTurnOwner(task, record, client, retainedOwner);
			const turn = this.createRunningRuntimeTurn({
				record,
				conversation,
				acpxState,
				client,
				owner,
				connected: retainedOwner !== void 0,
				promptMessageId
			});
			this.activateRuntimeTurn(task, turn);
			return turn;
		} catch (error) {
			this.restoreBufferedSessionOwner(retainedOwner);
			throw error;
		}
	}
	async prepareRuntimeTurnState(task) {
		const { record, retainedOwner } = await this.acquireRuntimeTurnState(task);
		const conversation = cloneSessionConversation(record);
		const acpxState = cloneSessionAcpxState(record.acpx);
		try {
			const promptStartedAt = isoNow();
			const promptMessageId = recordPromptSubmission(conversation, task.promptInput, promptStartedAt);
			record.lastPromptAt = promptStartedAt;
			record.lastUsedAt = promptStartedAt;
			record.acpx = acpxState;
			applyConversation(record, conversation);
			await this.options.sessionStore.save(record);
			return {
				record,
				retainedOwner,
				conversation,
				acpxState,
				promptMessageId
			};
		} catch (error) {
			this.restoreBufferedSessionOwner(retainedOwner);
			throw error;
		}
	}
	async acquireRuntimeTurnState(task) {
		const recordId = task.input.handle.acpxRecordId ?? task.input.handle.sessionKey;
		let record = await this.requireRecord(recordId);
		const retainedOwner = await this.readRetainedSessionOwner(record, { consume: false });
		if (!retainedOwner) return { record };
		retainedOwner.bufferSessionUpdates = true;
		try {
			await retainedOwner.checkpoint.flush();
			const projection = retainedOwner.projection;
			if (projection) record = structuredClone(projection.record);
			return {
				record,
				retainedOwner
			};
		} catch (error) {
			this.restoreBufferedSessionOwner(retainedOwner);
			throw error;
		}
	}
	restoreBufferedSessionOwner(owner) {
		if (!owner) return;
		owner.bufferSessionUpdates = false;
		this.drainPendingSessionUpdates(owner);
	}
	resolveRuntimeTurnOwner(task, record, client, retainedOwner) {
		return retainedOwner ?? this.createSessionOwner({
			client,
			sessionKey: record.name ?? task.input.handle.sessionKey,
			mode: task.input.sessionMode
		});
	}
	createRunningRuntimeTurn(input) {
		const { record, conversation, acpxState, client, owner, connected, promptMessageId } = input;
		return {
			record,
			conversation,
			acpxState,
			client,
			owner,
			connected,
			promptMessageId,
			activeSessionId: record.acpSessionId
		};
	}
	activateRuntimeTurn(task, turn) {
		const { owner, record } = turn;
		this.removeRetainedSessionOwner(owner);
		owner.recordId = record.acpxRecordId;
		task.state.activeController = this.buildRuntimeTurnController(task, turn);
		this.activeControllers.set(record.acpxRecordId, task.state.activeController);
		owner.projection = void 0;
		owner.activeTurn = {
			task,
			turn
		};
		owner.bufferSessionUpdates = false;
		this.drainPendingSessionUpdates(owner);
	}
	createTurnClient(record) {
		return this.createClient({
			agentCommand: record.agentCommand,
			agentArgv: record.agentArgv,
			cwd: record.cwd,
			mcpServers: this.resolveMcpServers({
				...record,
				sessionKey: record.name ?? record.acpxRecordId
			}),
			...this.resolveSessionPermissions({
				...record,
				sessionKey: record.name ?? record.acpxRecordId
			}),
			...this.resolveClientCapabilities(),
			elicitationModes: this.options.elicitationModes,
			processLifecycle: this.options.processLifecycle,
			processLaunchScope: {
				kind: "runtime-session",
				sessionKey: record.name ?? record.acpxRecordId
			},
			verbose: this.options.verbose,
			sessionOptions: sessionOptionsFromRecord(record)
		});
	}
	buildRuntimeTurnController(task, turn) {
		return {
			hasActivePrompt: () => turn.client.hasActivePrompt(),
			requestCancelActivePrompt: async () => await this.requestRuntimeTurnCancel(task, turn),
			setSessionMode: async (modeId, authority) => {
				await this.waitForRuntimeControlSession(task, turn);
				await turn.client.setSessionMode(turn.activeSessionId, modeId, authority);
				const nextState = cloneSessionAcpxState(turn.acpxState) ?? {};
				nextState.desired_mode_id = modeId;
				turn.acpxState = nextState;
				await turn.owner.checkpoint.checkpoint();
			},
			setSessionModel: async (modelId, authority) => {
				await this.waitForRuntimeControlSession(task, turn);
				const models = advertisedModelState(turn.acpxState);
				const resolvedModelId = resolveRequestedModelId({
					requestedModel: modelId,
					models,
					agentCommand: turn.record.agentCommand
				});
				const response = await turn.client.setSessionModel(turn.activeSessionId, modelId, models, authority);
				turn.acpxState = applyModelSelection(turn.acpxState, modelId, response, resolvedModelId);
				await turn.owner.checkpoint.checkpoint();
				return response;
			},
			setSessionConfigOption: async (configId, value, authority) => {
				return (await task.state.activeController.setResolvedSessionConfigOption(configId, value, authority)).response;
			},
			setResolvedSessionConfigOption: async (configId, value, authority) => await this.setRuntimeResolvedSessionConfigOption(task, turn, configId, value, authority)
		};
	}
	async waitForRuntimeControlSession(task, turn) {
		if (turn.client.hasActivePrompt()) return;
		await task.sessionReady.promise;
	}
	async requestRuntimeTurnCancel(task, turn) {
		if (turn.client.hasActivePrompt()) return await turn.client.requestCancelActivePrompt();
		if (!task.state.turnActive) return false;
		task.state.pendingCancel = true;
		return true;
	}
	async setRuntimeResolvedSessionConfigOption(task, turn, configId, value, authority) {
		await this.waitForRuntimeControlSession(task, turn);
		const resolvedConfigId = resolveSupportedConfigOptionId({
			...turn.record,
			acpx: turn.acpxState ?? void 0
		}, configId);
		const models = advertisedModelState(turn.acpxState);
		const { modelConfigId, resolvedValue } = resolveRequestedConfigOption({
			configId: resolvedConfigId,
			value,
			models,
			agentCommand: turn.record.agentCommand
		});
		const response = await turn.client.setSessionConfigOption(turn.activeSessionId, resolvedConfigId, value, models, authority);
		turn.acpxState = applyConfigOptionSelection(turn.acpxState, resolvedConfigId, value, response, modelConfigId, resolvedValue);
		await turn.owner.checkpoint.checkpoint();
		return {
			configId: resolvedConfigId,
			response
		};
	}
	emitRuntimeTurnEvent(task, payload) {
		const parsed = runtimeEventFromMessage(payload);
		if (!parsed) return;
		task.queue.push(parsed);
	}
	async connectRuntimeTurn(task, turn) {
		if (turn.connected) return {
			sessionId: turn.record.acpSessionId,
			resumed: false,
			loadError: void 0
		};
		const loaded = await this.connectRuntimeTurnClient(task, turn);
		turn.acpxState = cloneSessionAcpxState(turn.record.acpx);
		turn.connected = true;
		return loaded;
	}
	async connectRuntimeTurnClient(task, turn) {
		return await connectAndLoadSession({
			client: turn.client,
			record: turn.record,
			resumePolicy: resumePolicyForSessionMode(task.input.sessionMode),
			strictResume: this.exactSessionRecordIds.has(turn.record.acpxRecordId),
			timeoutMs: this.options.timeoutMs,
			promptRecorded: true,
			activeController: task.state.activeController,
			onClientAvailable: () => this.publishRuntimeTurnController(task, turn),
			onConnectedRecord: (connectedRecord) => {
				connectedRecord.lastPromptAt = isoNow();
			},
			onSessionIdResolved: (sessionIdValue) => {
				turn.activeSessionId = sessionIdValue;
			}
		});
	}
	publishRuntimeTurnController(task, turn) {
		const controller = task.state.activeController;
		if (controller) this.activeControllers.set(turn.record.acpxRecordId, controller);
	}
	async resolveRuntimeTurnReady(task, turn) {
		task.sessionReady.resolve();
		turn.record.lastRequestId = task.input.requestId;
		turn.record.lastPromptAt = isoNow();
		turn.record.closed = false;
		turn.record.closedAt = void 0;
		turn.record.lastUsedAt = isoNow();
		await turn.owner.checkpoint.checkpoint();
	}
	cancelRuntimeTurnBeforePrompt(task) {
		if (!task.state.pendingCancel && !task.input.signal?.aborted) return false;
		task.state.pendingCancel = false;
		task.promptStarted.reject(/* @__PURE__ */ new Error("ACP turn cancelled before prompt submission."));
		return true;
	}
	updateCompletedRuntimeTurn(turn) {
		turn.record.acpSessionId = turn.activeSessionId;
		reconcileAgentSessionId(turn.record, turn.record.agentSessionId);
		turn.record.protocolVersion = turn.client.initializeResult?.protocolVersion;
		turn.record.agentCapabilities = turn.client.initializeResult?.agentCapabilities;
	}
	handleRuntimeTurnFailure(task, error) {
		if (!task.input.signal?.aborted || error !== task.input.signal.reason) return this.failRuntimeTurn(task, error);
		task.promptStarted.reject(error);
		task.sessionReady.reject(error);
		return {
			status: "cancelled",
			stopReason: "cancelled"
		};
	}
	failRuntimeTurn(task, error) {
		task.promptStarted.reject(error);
		task.sessionReady.reject(error);
		const normalized = normalizeOutputError(error, { origin: "runtime" });
		return {
			status: "failed",
			error: {
				message: normalized.message,
				...normalized.code ? { code: normalized.code } : {},
				...normalized.detailCode ? { detailCode: normalized.detailCode } : {},
				...normalized.retryable !== void 0 ? { retryable: normalized.retryable } : {}
			}
		};
	}
	async finalizeRuntimeTurn(task, turn) {
		task.state.turnActive = false;
		const abortHandlerAttempt = await settleAttempt(() => task.input.signal?.removeEventListener("abort", task.abortHandler));
		const recordAttempt = await settleAttempt(async () => turn ? await this.finalizeRuntimeTurnRecord(turn) : false);
		let failure = firstFailedAttempt([abortHandlerAttempt, recordAttempt]);
		let pooled = recordAttempt.ok ? recordAttempt.value : false;
		if (failure) {
			this.discardRetainedRuntimeTurnOwner(turn);
			pooled = false;
		}
		const closeAttempt = await settleAttempt(async () => this.closeRuntimeTurnClient(turn, pooled));
		this.cleanupRuntimeTurn(task, turn);
		failure ??= firstFailedAttempt([closeAttempt]);
		if (failure) throw failure.error;
	}
	discardRetainedRuntimeTurnOwner(turn) {
		if (turn) {
			this.removeRetainedSessionOwner(turn.owner);
			this.attachFinalRuntimeProjection(turn);
		}
	}
	attachFinalRuntimeProjection(turn) {
		const current = turn.connected ? turn.acpxState : turn.record.acpx;
		const state = turn.record.acpx?.reset_on_next_ensure === true ? {
			...current,
			reset_on_next_ensure: true
		} : current;
		this.attachIdleProjection(turn.owner, turn.record, turn.conversation, state);
	}
	async closeRuntimeTurnClient(turn, pooled) {
		if (!turn || pooled) return;
		const failure = firstFailedAttempt([
			await settleAttempt(() => turn.client.clearEventHandlers()),
			await settleAttempt(async () => this.closeClient(turn.client)),
			await settleAttempt(async () => this.flushSessionOwner(turn.owner))
		]);
		if (failure) {
			this.retiringSessionOwners.add(turn.owner);
			throw failure.error;
		}
	}
	cleanupRuntimeTurn(task, turn) {
		if (turn) {
			this.activeControllers.delete(turn.record.acpxRecordId);
			this.closingActiveRecords.delete(turn.record.acpxRecordId);
		}
		task.queue.close();
	}
	async finalizeRuntimeTurnRecord(turn) {
		if (!turn.connected) turn.acpxState = cloneSessionAcpxState(turn.record.acpx);
		await turn.owner.checkpoint.checkpoint();
		this.attachFinalRuntimeProjection(turn);
		if (turn.record.closed || !turn.connected) return false;
		return await this.retainPersistentSessionOwnerAfterTurn({
			record: turn.record,
			owner: turn.owner
		});
	}
	async *runTurn(input) {
		const turn = this.startTurn(input);
		yield* turn.events;
		yield legacyTerminalEventFromTurnResult(await turn.result);
	}
	getStatus(handle) {
		this.assertOpen();
		return this.trackTask(this.readStatus(handle));
	}
	async readStatus(handle) {
		const recordId = handle.acpxRecordId ?? handle.sessionKey;
		const owner = this.retainedSessionOwners.get(recordId);
		if (owner) await this.flushSessionOwner(owner);
		return runtimeStatusFromRecord(await this.requireRecord(recordId));
	}
	async setMode(handle, mode, sessionMode = "persistent", authority) {
		const recordId = handle.acpxRecordId ?? handle.sessionKey;
		await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setModeWithOwnership(handle, mode, sessionMode, authority), authority);
	}
	async setModeWithOwnership(handle, mode, sessionMode, authority) {
		const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
		assertControlAuthority(authority);
		const controller = this.activeControllers.get(record.acpxRecordId);
		if (controller) {
			await controller.setSessionMode(mode, authority);
			return;
		}
		await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
			await client.setSessionMode(sessionId, mode, authority);
			setDesiredModeId(connectedRecord, mode);
		}, { replacingMode: true }, authority);
	}
	async setModel(handle, model, sessionMode = "persistent", authority) {
		const recordId = handle.acpxRecordId ?? handle.sessionKey;
		await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setModelWithOwnership(handle, model, sessionMode, authority), authority);
	}
	async setModelWithOwnership(handle, model, sessionMode, authority) {
		const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
		assertControlAuthority(authority);
		const controller = this.activeControllers.get(record.acpxRecordId);
		if (controller) {
			await controller.setSessionModel(model, authority);
			return;
		}
		await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
			const models = advertisedModelState(connectedRecord.acpx);
			const resolvedModelId = resolveRequestedModelId({
				requestedModel: model,
				models,
				agentCommand: connectedRecord.agentCommand
			});
			const response = await client.setSessionModel(sessionId, model, models, authority);
			connectedRecord.acpx = applyModelSelection(connectedRecord.acpx, model, response, resolvedModelId);
		}, { replacingConfigOption: { key: "model" } }, authority);
	}
	async setConfigOption(handle, key, value, sessionMode = "persistent", authority) {
		const recordId = handle.acpxRecordId ?? handle.sessionKey;
		return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.setConfigOptionWithOwnership(handle, key, value, sessionMode, authority), authority);
	}
	async setConfigOptionWithOwnership(handle, key, value, sessionMode, authority) {
		const record = await this.requireRecord(handle.acpxRecordId ?? handle.sessionKey);
		assertControlAuthority(authority);
		const controller = this.activeControllers.get(record.acpxRecordId);
		if (controller) {
			const { response } = await controller.setResolvedSessionConfigOption(key, value, authority);
			return response;
		}
		return await this.withRuntimeControlSession(record, sessionMode, async ({ client, sessionId, record: connectedRecord }) => {
			const configId = resolveSupportedConfigOptionId(connectedRecord, key);
			const models = advertisedModelState(connectedRecord.acpx);
			const { modelConfigId, resolvedValue } = resolveRequestedConfigOption({
				configId,
				value,
				models,
				agentCommand: connectedRecord.agentCommand
			});
			const response = await client.setSessionConfigOption(sessionId, configId, value, models, authority);
			connectedRecord.acpx = applyConfigOptionSelection(connectedRecord.acpx, configId, value, response, modelConfigId, resolvedValue);
			return response;
		}, { replacingConfigOption: {
			key,
			resolve: (connectedRecord) => resolveSupportedConfigOptionId(connectedRecord, key)
		} }, authority);
	}
	async cancel(handle) {
		await this.activeControllers.get(handle.acpxRecordId ?? handle.sessionKey)?.requestCancelActivePrompt();
	}
	prepareFreshSession(handle) {
		this.assertOpen();
		const recordId = handle.acpxRecordId ?? handle.sessionKey;
		const cancelled = this.cancelSessionTasks(recordId);
		cancelled.catch(() => {});
		return this.queueSessionTask(recordId, async () => {
			const tasks = await cancelled;
			await Promise.all(tasks.map((task) => task.completion));
			return await this.withManagerLock(this.runtimeOperationLocks, recordId, async () => this.closeRuntimeSession(handle, "prepare-fresh"));
		});
	}
	close(handle, options = {}) {
		this.assertOpen();
		const recordId = handle.acpxRecordId ?? handle.sessionKey;
		return this.trackTask(this.withManagerLock(this.runtimeOperationLocks, recordId, async () => {
			return await this.closeRuntimeSession(handle, options.discardPersistentState === true ? "discard" : "release");
		}));
	}
	async closeRuntimeSession(handle, intent) {
		const recordId = handle.acpxRecordId ?? handle.sessionKey;
		await this.retrySessionCleanup(recordId);
		const record = await this.resolveRuntimeRecordForClose(recordId);
		this.markActiveRuntimeRecordClosing(record);
		if (intent !== "prepare-fresh") await this.cancelSessionTasks(recordId);
		const closeFailure = await this.closeRuntimeRecordOwnership(record, intent);
		if (intent === "discard") {
			if (closeFailure) throw closeFailure.error;
			return;
		}
		const saveAttempt = await settleAttempt(async () => this.saveClosedRuntimeRecord(record, intent !== "release"));
		const failure = closeFailure ?? firstFailedAttempt([saveAttempt]);
		if (failure) throw failure.error;
	}
	async saveClosedRuntimeRecord(record, reset) {
		record.closed = true;
		record.closedAt = isoNow();
		if (reset) record.acpx = {
			...record.acpx,
			reset_on_next_ensure: true
		};
		await this.options.sessionStore.save(record);
	}
	async resolveRuntimeRecordForClose(recordId) {
		const retainedOwner = this.retainedSessionOwners.get(recordId);
		if (retainedOwner) await this.flushSessionOwner(retainedOwner);
		return retainedOwner?.projection?.record ?? await this.requireRecord(recordId);
	}
	markActiveRuntimeRecordClosing(record) {
		if (this.activeControllers.has(record.acpxRecordId)) this.closingActiveRecords.add(record.acpxRecordId);
	}
	async closeRuntimeRecordOwnership(record, intent) {
		if (intent === "discard") return await this.closeBackendSession(record);
		else if (intent === "prepare-fresh") {
			const owner = this.retainedSessionOwners.get(record.acpxRecordId);
			if (owner) {
				this.removeRetainedSessionOwner(owner);
				this.retiringSessionOwners.add(owner);
			}
			await this.retrySessionCleanup(record.acpxRecordId);
		} else await this.closeRetainedSessionOwner(record.acpxRecordId);
	}
	async closeBackendSession(record) {
		const connection = await this.acquireBackendCloseConnection(record);
		const requestAttempt = await settleAttempt(async () => {
			try {
				await this.requestBackendSessionClose(record, connection);
			} catch (error) {
				this.handleBackendSessionCloseError(record, error);
			}
		});
		const owner = connection.owner ?? this.createSessionOwner({
			client: connection.client,
			sessionKey: record.acpxRecordId,
			mode: "persistent"
		});
		owner.recordId = record.acpxRecordId;
		const finalizeAttempt = await settleAttempt(async () => this.finalizeSessionConnection(connection));
		owner.pendingResetRecord = record;
		this.retiringSessionOwners.add(owner);
		if (!finalizeAttempt.ok) throw finalizeAttempt.error;
		const saveAttempt = await settleAttempt(async () => this.settleRetiredSessionRecord(owner));
		if (saveAttempt.ok) this.retiringSessionOwners.delete(owner);
		return firstFailedAttempt([requestAttempt, saveAttempt]);
	}
	async finalizeSessionConnection(connection) {
		const failure = firstFailedAttempt([
			await settleAttempt(async () => {
				if (connection.owner) await this.flushSessionOwner(connection.owner);
			}),
			await settleAttempt(() => connection.owner?.client.clearEventHandlers()),
			await settleAttempt(async () => this.closeClient(connection.client)),
			await settleAttempt(async () => connection.owner?.checkpoint.flush())
		]);
		if (failure) throw failure.error;
	}
	async acquireBackendCloseConnection(record) {
		const owner = await this.readRetainedSessionOwner(record, { consume: true });
		if (owner) return {
			client: owner.client,
			owner
		};
		return { client: this.createTurnClient(record) };
	}
	async requestBackendSessionClose(record, connection) {
		if (!connection.owner) await withTimeout(connection.client.start(), this.options.timeoutMs);
		if (!connection.client.supportsCloseSession()) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`);
		await withTimeout(connection.client.closeSession(record.acpSessionId), this.options.timeoutMs);
	}
	handleBackendSessionCloseError(record, error) {
		if (isAcpResourceNotFoundError(error)) return;
		if (isUnsupportedSessionCloseError(error)) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", `Agent does not support session/close for ${record.acpxRecordId}.`, { cause: error });
		throw error;
	}
	async requireRecord(sessionId) {
		const record = await this.options.sessionStore.load(sessionId);
		if (!record) throw new Error(`ACP session not found: ${sessionId}`);
		return record;
	}
};
// src/runtime/public/file-session-store.ts
function safeSessionId(sessionId) {
	return encodeURIComponent(sessionId);
}
var FileSessionStore = class {
	stateDir;
	constructor(stateDir) {
		this.stateDir = stateDir;
	}
	get sessionDir() {
		return path.join(this.stateDir, "sessions");
	}
	filePath(sessionId) {
		return path.join(this.sessionDir, `${safeSessionId(sessionId)}.json`);
	}
	async ensureDir() {
		await fs.mkdir(this.sessionDir, {
			recursive: true,
			mode: 448
		});
	}
	async load(sessionId) {
		await this.ensureDir();
		let payload;
		try {
			payload = await fs.readFile(this.filePath(sessionId), "utf8");
		} catch (error) {
			if (error.code === "ENOENT") return;
			throw error;
		}
		let parsed;
		try {
			parsed = JSON.parse(payload);
		} catch {
			return;
		}
		const record = parseSessionRecord(parsed);
		return record?.acpxRecordId === sessionId ? record : void 0;
	}
	async save(record) {
		const persisted = serializeSessionRecordForDisk(record);
		assertPersistedKeyPolicy(persisted);
		await writePrivateJsonFile(this.filePath(record.acpxRecordId), persisted);
	}
};
function createFileSessionStore(options) {
	return new FileSessionStore(path.resolve(options.stateDir));
}
// src/runtime/public/handle-state.ts
const ACPX_RUNTIME_HANDLE_PREFIX = "acpx:v2:";
function encodeAcpxRuntimeHandleState(state) {
	const payload = Buffer.from(JSON.stringify(state), "utf8").toString("base64url");
	return `${ACPX_RUNTIME_HANDLE_PREFIX}${payload}`;
}
function decodeAcpxRuntimeHandleState(runtimeSessionName) {
	const trimmed = runtimeSessionName.trim();
	if (!trimmed.startsWith(ACPX_RUNTIME_HANDLE_PREFIX)) return null;
	try {
		const raw = Buffer.from(trimmed.slice(8), "base64url").toString("utf8");
		const parsed = JSON.parse(raw);
		const name = asOptionalString(parsed.name);
		const agent = asOptionalString(parsed.agent);
		const cwd = asOptionalString(parsed.cwd);
		const mode = asOptionalString(parsed.mode);
		if (!name || !agent || !cwd || mode !== "persistent" && mode !== "oneshot") return null;
		return {
			name,
			agent,
			cwd,
			mode,
			acpxRecordId: asOptionalString(parsed.acpxRecordId),
			backendSessionId: asOptionalString(parsed.backendSessionId),
			agentSessionId: asOptionalString(parsed.agentSessionId)
		};
	} catch {
		return null;
	}
}
function writeHandleState(handle, state) {
	handle.runtimeSessionName = encodeAcpxRuntimeHandleState(state);
	handle.cwd = state.cwd;
	handle.acpxRecordId = state.acpxRecordId;
	handle.backendSessionId = state.backendSessionId;
	handle.agentSessionId = state.agentSessionId;
}
// src/runtime/shared.ts
const SHARED_BACKEND = "acpx-shared";
function invalidOption(message) {
	throw new AcpRuntimeError("ACP_INVALID_RUNTIME_OPTION", message);
}
function sharedHandle(record) {
	return {
		backend: SHARED_BACKEND,
		sessionKey: record.name ?? "",
		runtimeSessionName: record.acpxRecordId,
		acpxRecordId: record.acpxRecordId,
		backendSessionId: record.acpSessionId,
		agentSessionId: record.agentSessionId,
		cwd: record.cwd
	};
}
function sharedRecordId(handle) {
	if (handle.backend !== SHARED_BACKEND || !handle.acpxRecordId) return invalidOption("A shared runtime requires a handle from a shared session.");
	return handle.acpxRecordId;
}
/**
* Shared controls are owner-only. Falling back to a direct connection would
* start a second adapter connection for a session another process owns, and
* that path may create a fresh provider session when the saved one no longer
* loads. Shared turns already refuse that trade; controls refuse it too.
*/
function requireOwnerDispatch(result, control) {
	if (result === void 0) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", `No running owner holds this shared session, so ${control} was not sent. Submit a turn to start an owner, or apply the change with the equivalent acpx command, which connects directly instead of starting one.`);
	return result;
}
function turnFailure(error) {
	const normalized = normalizeOutputError(error, {
		defaultCode: "RUNTIME",
		origin: "queue"
	});
	if (normalized.detailCode === "QUEUE_REQUEST_CANCELLED") return {
		status: "cancelled",
		stopReason: "cancelled"
	};
	return {
		status: "failed",
		error: {
			message: normalized.message,
			code: normalized.code,
			detailCode: normalized.detailCode,
			retryable: normalized.retryable
		}
	};
}
/** A client of the CLI's local session owner. Shutdown detaches this client. */
var SharedAcpRuntime = class {
	registry;
	options;
	disconnect = new AbortController();
	pending = /* @__PURE__ */ new Set();
	controlChains = /* @__PURE__ */ new Map();
	constructor(options) {
		for (const name of [
			"sessionStore",
			"agentProcessEnv",
			"agentProcessEnvAllow",
			"agentProcessEnvBlock",
			"mcpServers",
			"sessionPermissions",
			"processLifecycle",
			"onPermissionRequest",
			"onAcpMessage",
			"elicitationModes",
			"fs",
			"terminal"
		]) if (Object.hasOwn(options, name)) invalidOption(`${name} belongs to the in-process runtime and is not supported by shared sessions.`);
		this.options = {
			...options,
			cwd: path.resolve(options.cwd),
			authCredentials: options.authCredentials ? { ...options.authCredentials } : void 0
		};
		this.registry = options.agentRegistry ?? createAgentRegistry();
	}
	assertOpen() {
		if (this.disconnect.signal.aborted) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "Shared ACP runtime is shut down.");
	}
	track(task) {
		this.pending.add(task);
		task.then(() => this.pending.delete(task), () => this.pending.delete(task));
		return task;
	}
	sessionOptions(input) {
		if (!input.agent.trim()) invalidOption("ACP agent id is required.");
		return {
			...normalizeAgentCommandInput(this.registry.resolve(input.agent)),
			cwd: path.resolve(input.cwd ?? this.options.cwd),
			name: input.sessionKey.trim() || void 0
		};
	}
	async findSession(input) {
		this.assertOpen();
		const record = await findSession(this.sessionOptions(input));
		return record ? sharedHandle(record) : void 0;
	}
	ensureSession(input) {
		this.assertOpen();
		if (input.mode !== "persistent") invalidOption("Shared sessions require persistent mode; use the in-process runtime for oneshot sessions.");
		const session = this.sessionOptions(input);
		return this.track(ensureSession({
			...this.options,
			...session,
			walkBoundary: session.cwd,
			sessionOptions: input.sessionOptions,
			resumeSessionId: input.resumeSessionId,
			handleProcessInterrupts: false,
			signal: this.disconnect.signal
		}).then(({ record }) => {
			this.assertOpen();
			return sharedHandle(record);
		}));
	}
	startTurn(input) {
		this.assertOpen();
		if (input.mode !== "prompt" || input.assertActive || input.onPermissionRequest || input.onElicitation) invalidOption("Shared turns support prompt mode and the client's static permission policy; in-process callbacks and steering are unsupported.");
		if (!input.requestId.trim()) invalidOption("A shared turn requires a requestId.");
		const sessionId = sharedRecordId(input.handle);
		const prompt = toPromptInput(input.text, input.attachments);
		const events = new AsyncEventQueue();
		const promptStarted = createDeferred();
		promptStarted.promise.catch(() => {});
		let accepted = false;
		let settled = false;
		let cancelRequested = input.signal?.aborted === true;
		let cancellation;
		const cancel = async () => {
			cancelRequested = true;
			if (!accepted || settled) return;
			cancellation ??= tryCancelOnRunningOwner({
				sessionId,
				targetRequestId: input.requestId
			}).then(() => {});
			await cancellation;
		};
		const onAbort = () => {
			cancel().catch(() => {});
		};
		input.signal?.addEventListener("abort", onAbort, { once: true });
		const run = async () => {
			if (cancelRequested) return {
				status: "cancelled",
				stopReason: "cancelled"
			};
			if ((await resolveSessionRecord(sessionId)).closed) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "Shared session is closed; ensure it again before submitting a turn.");
			if (cancelRequested) return {
				status: "cancelled",
				stopReason: "cancelled"
			};
			const result = await sendSession({
				...this.options,
				sessionId,
				requestId: input.requestId,
				prompt: typeof prompt === "string" ? textPrompt(prompt) : prompt,
				resumePolicy: "same-session-only",
				requireSharedRuntime: true,
				signal: this.disconnect.signal,
				timeoutMs: input.timeoutMs ?? this.options.timeoutMs,
				queueOwnerArgs: [fileURLToPath(import.meta.resolve("acpx/dist/cli.js")), "__queue-owner"],
				onQueueAccepted: () => {
					accepted = true;
					if (cancelRequested) cancel().catch(() => {});
				},
				onPromptStarted: () => promptStarted.resolve(),
				outputFormatter: {
					...DISCARD_OUTPUT_FORMATTER,
					onAcpMessage: (message) => {
						const event = runtimeEventFromMessage(message);
						if (event) events.push(event);
					}
				},
				errorEmissionPolicy: { queueErrorAlreadyEmitted: false },
				waitForCompletion: true
			});
			if ("queued" in result) throw new AcpRuntimeError("ACP_TURN_FAILED", "Shared turn ended without a completion result.");
			return {
				status: result.stopReason === "cancelled" ? "cancelled" : "completed",
				stopReason: result.stopReason,
				_meta: result._meta
			};
		};
		const result = this.track(run().catch(turnFailure).finally(() => {
			settled = true;
			promptStarted.reject(new AcpRuntimeError("ACP_TURN_FAILED", "Turn settled before its prompt was written."));
			input.signal?.removeEventListener("abort", onAbort);
			events.close();
		}));
		return {
			requestId: input.requestId,
			promptStarted: promptStarted.promise,
			events: events.iterate(),
			result,
			cancel,
			closeStream: async () => {
				events.clear();
				events.close();
			}
		};
	}
	async *runTurn(input) {
		const turn = this.startTurn(input);
		yield* turn.events;
		yield legacyTerminalEventFromTurnResult(await turn.result);
	}
	async getStatus(input) {
		this.assertOpen();
		return runtimeStatusFromRecord(await resolveSessionRecord(sharedRecordId(input.handle)));
	}
	async getCapabilities(input) {
		this.assertOpen();
		if (!input?.handle) return capabilitiesFromRecord(void 0);
		return capabilitiesFromRecord(await readSessionRecord(sharedRecordId(input.handle)));
	}
	/**
	* Preserves this client's control order, including caller-side persistence
	* when talking to older owners. Updated owners checkpoint accepted controls
	* themselves; serialization here cannot coordinate other legacy callers.
	*/
	chainControl(sessionId, run) {
		const result = (this.controlChains.get(sessionId) ?? Promise.resolve()).then(run, run);
		const chained = result.then(() => void 0, () => void 0);
		this.controlChains.set(sessionId, chained);
		chained.then(() => {
			if (this.controlChains.get(sessionId) === chained) this.controlChains.delete(sessionId);
		});
		return result;
	}
	/**
	* Checks this client's authority locally, then hands the control to the
	* running owner. The check gates this client's dispatch only; it cannot gate
	* controls other clients of the same owner send, and it cannot recall a
	* control the owner already accepted.
	*/
	async dispatchControl(input, control, send) {
		this.assertOpen();
		const sessionId = sharedRecordId(input.handle);
		const assertAuthority = () => {
			this.assertOpen();
			assertControlAuthority(input);
		};
		return await this.track(this.chainControl(sessionId, async () => {
			assertAuthority();
			return requireOwnerDispatch(await send(sessionId, assertAuthority), control);
		}));
	}
	async setMode(input) {
		await this.dispatchControl(input, "session/set_mode", async (sessionId, assertAuthority) => setSessionModeOnOwner({
			sessionId,
			modeId: input.mode,
			timeoutMs: this.options.timeoutMs,
			assertDispatch: assertAuthority
		}));
	}
	async setModel(input) {
		await this.dispatchControl(input, "session/set_model", async (sessionId, assertAuthority) => setSessionModelOnOwner({
			sessionId,
			modelId: input.model,
			timeoutMs: this.options.timeoutMs,
			assertDispatch: assertAuthority
		}));
	}
	async setConfigOption(input) {
		return (await this.dispatchControl(input, "session/set_config_option", async (sessionId, assertAuthority) => {
			const configId = resolveSupportedConfigOptionId(await resolveSessionRecord(sessionId), input.key);
			assertAuthority();
			return await setSessionConfigOptionOnOwner({
				sessionId,
				configId,
				value: input.value,
				timeoutMs: this.options.timeoutMs,
				assertDispatch: assertAuthority
			});
		})).response;
	}
	watchSession(input) {
		this.assertOpen();
		const signal = input.signal ? AbortSignal.any([input.signal, this.disconnect.signal]) : this.disconnect.signal;
		const recordId = sharedRecordId(input.handle);
		const record = readSessionRecord(recordId).then((value) => {
			if (!value || value.acpxRecordId !== recordId) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "Shared session record is unavailable.");
			return value;
		});
		record.catch(() => {});
		return watchSession({
			record,
			cursor: input.cursor,
			signal
		});
	}
	async cancel(input) {
		this.assertOpen();
		await this.track(cancelSessionPrompt({ sessionId: sharedRecordId(input.handle) }));
	}
	async close(input) {
		this.assertOpen();
		await this.track(closeSession(sharedRecordId(input.handle)));
	}
	async shutdown() {
		this.disconnect.abort(new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "Shared client detached; submitted work may still be running."));
		await Promise.allSettled(this.pending);
	}
};
function createSharedAcpRuntime(options) {
	return new SharedAcpRuntime(options);
}
// src/runtime.ts
const ACPX_BACKEND_ID = "acpx";
var AcpxRuntime = class {
	options;
	testOptions;
	healthy = false;
	shutdownTask;
	probeTasks = /* @__PURE__ */ new Set();
	manager = null;
	managerPromise = null;
	constructor(options, testOptions) {
		this.options = options;
		this.testOptions = testOptions;
		if (options.agentProcessEnv || options.agentProcessEnvAllow !== void 0 || options.agentProcessEnvBlock !== void 0) this.options = {
			...options,
			agentProcessEnv: options.agentProcessEnv ? { ...options.agentProcessEnv } : void 0,
			agentProcessEnvAllow: snapshotEnvNames("agentProcessEnvAllow", options.agentProcessEnvAllow),
			agentProcessEnvBlock: snapshotEnvNames("agentProcessEnvBlock", options.agentProcessEnvBlock)
		};
	}
	isHealthy() {
		return this.healthy;
	}
	async probeAvailability() {
		const report = await this.runProbe();
		this.healthy = report.ok;
	}
	async doctor() {
		const report = await this.runProbe();
		this.healthy = report.ok;
		return {
			ok: report.ok,
			code: report.ok ? void 0 : "ACP_BACKEND_UNAVAILABLE",
			message: report.message,
			details: normalizeRuntimeDetails(report.details)
		};
	}
	resolveSessionIdentity(input) {
		const sessionName = input.sessionKey.trim();
		if (!sessionName) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP session key is required.");
		const agent = input.agent.trim();
		if (!agent) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP agent id is required.");
		return {
			sessionName,
			agent
		};
	}
	async findSession(input) {
		const { sessionName, agent } = this.resolveSessionIdentity(input);
		const record = await (await this.getManager()).findSession(sessionName);
		return record ? this.createSessionHandle({
			sessionKey: input.sessionKey,
			agent,
			mode: "persistent"
		}, record) : void 0;
	}
	async ensureSession(input) {
		const { sessionName, agent } = this.resolveSessionIdentity(input);
		const record = await (await this.getManager()).ensureSession({
			sessionKey: sessionName,
			agent,
			mode: input.mode,
			cwd: input.cwd ?? this.options.cwd,
			resumeSessionId: input.resumeSessionId,
			sessionOptions: input.sessionOptions
		});
		return this.createSessionHandle({
			...input,
			agent
		}, record);
	}
	createSessionHandle(input, record) {
		const handle = {
			sessionKey: input.sessionKey,
			backend: ACPX_BACKEND_ID,
			runtimeSessionName: "",
			cwd: record.cwd,
			acpxRecordId: record.acpxRecordId,
			backendSessionId: record.acpSessionId,
			agentSessionId: record.agentSessionId
		};
		writeHandleState(handle, {
			name: input.sessionKey.trim(),
			agent: input.agent,
			cwd: record.cwd,
			mode: input.mode,
			acpxRecordId: record.acpxRecordId,
			backendSessionId: record.acpSessionId,
			agentSessionId: record.agentSessionId
		});
		return handle;
	}
	startTurn(input) {
		const { handle, state } = this.resolveManagerHandle(input.handle);
		const turnPromise = this.getManager().then((manager) => manager.startTurn({
			handle,
			text: input.text,
			attachments: input.attachments,
			mode: input.mode,
			sessionMode: state.mode,
			requestId: input.requestId,
			timeoutMs: input.timeoutMs,
			signal: input.signal,
			assertActive: input.assertActive,
			onElicitation: input.onElicitation,
			onPermissionRequest: input.onPermissionRequest
		}));
		turnPromise.catch(() => {});
		return {
			requestId: input.requestId,
			get promptStarted() {
				return turnPromise.then((turn) => turn.promptStarted);
			},
			events: { async *[Symbol.asyncIterator]() {
				yield* (await turnPromise).events;
			} },
			get result() {
				return turnPromise.then((turn) => turn.result);
			},
			cancel(inputArgs) {
				return turnPromise.then((turn) => turn.cancel(inputArgs));
			},
			closeStream(inputArgs) {
				return turnPromise.then((turn) => turn.closeStream(inputArgs));
			}
		};
	}
	async *runTurn(input) {
		const { handle, state } = this.resolveManagerHandle(input.handle);
		yield* (await this.getManager()).runTurn({
			handle,
			text: input.text,
			attachments: input.attachments,
			mode: input.mode,
			sessionMode: state.mode,
			requestId: input.requestId,
			timeoutMs: input.timeoutMs,
			signal: input.signal,
			assertActive: input.assertActive,
			onElicitation: input.onElicitation,
			onPermissionRequest: input.onPermissionRequest
		});
	}
	async getCapabilities(input) {
		if (!input?.handle) return capabilitiesFromRecord(void 0);
		const { handle } = this.resolveManagerHandle(input.handle);
		return capabilitiesFromRecord(await this.options.sessionStore.load(handle.acpxRecordId ?? handle.sessionKey));
	}
	async getStatus(input) {
		const { handle } = this.resolveManagerHandle(input.handle);
		return await (await this.getManager()).getStatus(handle);
	}
	async setMode(input) {
		const { handle, state } = this.resolveManagerHandle(input.handle);
		await (await this.getManager()).setMode(handle, input.mode, state.mode, input);
	}
	async setModel(input) {
		const { handle, state } = this.resolveManagerHandle(input.handle);
		await (await this.getManager()).setModel(handle, input.model, state.mode, input);
	}
	async setConfigOption(input) {
		const { handle, state } = this.resolveManagerHandle(input.handle);
		return await (await this.getManager()).setConfigOption(handle, input.key, input.value, state.mode, input);
	}
	async cancel(input) {
		const { handle } = this.resolveManagerHandle(input.handle);
		await (await this.getManager()).cancel(handle);
	}
	async prepareFreshSession(input) {
		const { handle } = this.resolveManagerHandle(input.handle);
		await (await this.getManager()).prepareFreshSession(handle);
	}
	async close(input) {
		const { handle } = this.resolveManagerHandle(input.handle);
		await (await this.getManager()).close(handle, { discardPersistentState: input.discardPersistentState });
	}
	shutdown() {
		if (!this.shutdownTask) {
			this.healthy = false;
			const managerShutdown = this.managerPromise?.then((manager) => manager.shutdown());
			this.shutdownTask = Promise.allSettled([managerShutdown, ...this.probeTasks]).then(([manager]) => {
				if (manager.status === "rejected") throw manager.reason;
			});
		}
		return this.shutdownTask;
	}
	async getManager() {
		if (this.shutdownTask) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
		if (this.manager) return this.manager;
		if (!this.managerPromise) this.managerPromise = Promise.resolve(this.testOptions?.managerFactory?.(this.options) ?? new AcpRuntimeManager(this.options)).then((manager) => {
			this.manager = manager;
			return manager;
		});
		return await this.managerPromise;
	}
	async runProbe() {
		if (this.shutdownTask) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
		const probe = this.testOptions?.probeRunner?.(this.options) ?? this.getManager().then((manager) => manager.probe());
		this.probeTasks.add(probe);
		try {
			const report = await probe;
			if (this.shutdownTask) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
			return report;
		} finally {
			this.probeTasks.delete(probe);
		}
	}
	resolveManagerHandle(handle) {
		const state = this.resolveHandleState(handle);
		return {
			handle: {
				...handle,
				acpxRecordId: state.acpxRecordId ?? handle.acpxRecordId ?? handle.sessionKey
			},
			state
		};
	}
	resolveHandleState(handle) {
		const decoded = decodeAcpxRuntimeHandleState(handle.runtimeSessionName);
		if (decoded) return {
			...decoded,
			acpxRecordId: decoded.acpxRecordId ?? handle.acpxRecordId,
			backendSessionId: decoded.backendSessionId ?? handle.backendSessionId,
			agentSessionId: decoded.agentSessionId ?? handle.agentSessionId
		};
		const runtimeSessionName = handle.runtimeSessionName.trim();
		if (!runtimeSessionName) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "Invalid embedded ACP runtime handle: runtimeSessionName is missing.");
		return {
			name: runtimeSessionName,
			agent: deriveAgentFromSessionKey(handle.sessionKey, DEFAULT_AGENT_NAME),
			cwd: handle.cwd ?? this.options.cwd,
			mode: "persistent",
			acpxRecordId: handle.acpxRecordId,
			backendSessionId: handle.backendSessionId,
			agentSessionId: handle.agentSessionId
		};
	}
};
function createAcpRuntime(options) {
	return new AcpxRuntime(options);
}
function createRuntimeStore(options) {
	return createFileSessionStore(options);
}
export { ACPX_BACKEND_ID, AcpRuntimeError, AcpxRuntime, DEFAULT_AGENT_NAME, REQUESTED_MODEL_UNSUPPORTED_ERROR_CODE, REQUESTED_MODEL_UNSUPPORTED_REASONS, RequestedModelUnsupportedError, SharedAcpRuntime, createAcpRuntime, createAgentRegistry, createFileSessionStore, createRuntimeStore, createSharedAcpRuntime, decodeAcpxRuntimeHandleState, encodeAcpxRuntimeHandleState, inspectAgentModels, isAcpRuntimeError, isRequestedModelUnsupportedError };

//# sourceMappingURL=runtime.js.map