import { BoundedWriter } from "./bounded-writer.js";
import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
import { InteractiveEngineMonitor } from "../interactive-engine/engine-monitor.js";
import { INTERACTIVE_ENGINE_MAX_FRAME_BYTES, serializeInteractiveEngineFrame } from "../interactive-engine/protocol.js";
import { sleep } from "../../utils/sleep.js";
import { createInteractiveJsonlOptions, spawnRpcClientProcess, terminateRpcClientProcess } from "./rpc-client-process.js";
import { RpcClientApi } from "./rpc-client-api.js";
import { RpcEventBuffer } from "./rpc-event-buffer.js";
import { collectRpcEvents, waitForRpcIdle } from "./rpc-client-waits.js";
function restartArgs(args, sessionFile) {
    const result = [];
    for (let index = 0; index < (args?.length ?? 0); index += 1) {
        const value = args[index];
        if (value === "--no-session")
            continue;
        if (value === "--session") {
            index += 1;
            continue;
        }
        result.push(value);
    }
    result.push(sessionFile ? "--session" : "--no-session");
    if (sessionFile)
        result.push(sessionFile);
    return result;
}
/**
 * Commands whose response is legitimately gated on human/agent interaction or a
 * turn boundary and therefore must not be subject to the generic request
 * timeout: interactive prompts and custom-UI pickers (routed through `prompt`),
 * queued turn-boundary sends (`steer`/`follow_up`), long-running shell and
 * compaction work, and session-tree navigation/mutation that can open pickers.
 *
 * Failure detection is unaffected: engine exit, transport violations, aborts,
 * generation replacement, and explicit stop all reject pending requests via
 * rejectPendingRequests/failTransport independently of any timer.
 */
const LONG_LIVED_COMMANDS = new Set([
    "prompt",
    "steer",
    "follow_up",
    "bash",
    "user_bash",
    "compact",
    "fork",
    "clone",
    "switch_session",
    "new_session",
    "import_session",
    "navigate_tree",
    "invoke_shortcut",
]);
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
export class RpcClient extends RpcClientApi {
    constructor(options = {}) {
        super();
        this.process = null;
        this.stopReadingStdout = null;
        this.eventListeners = [];
        this.extensionUIListeners = [];
        this.pendingExtensionUIRequests = [];
        this.engineMessageListeners = [];
        this.pendingEngineMessages = [];
        this.pendingEngineMessageBytes = 0;
        this.pendingRequests = new Map();
        this.requestId = 0;
        this.stderr = "";
        this.exitError = null;
        this.generation = 0;
        this.activeActivityIds = new Set();
        this.options = options;
    }
    async start() {
        if (this.process) {
            throw new Error("Client already started");
        }
        const cliPath = this.options.cliPath ?? "dist/cli.js";
        const args = ["--mode", "rpc"];
        if (this.options.provider) {
            args.push("--provider", this.options.provider);
        }
        if (this.options.model) {
            args.push("--model", this.options.model);
        }
        if (this.options.contextWindow !== undefined) {
            args.push("--context-window", String(this.options.contextWindow));
        }
        if (this.options.args) {
            args.push(...this.options.args);
        }
        const generation = ++this.generation;
        this.exitError = null;
        this.stderr = "";
        this.activeActivityIds.clear();
        this.enginePid = undefined;
        this.engineMonitor = this.options.interactiveEngine
            ? new InteractiveEngineMonitor(this.options.interactiveEngine.onDiagnostic, (message) => this.observeInteractiveEngineMessage(message))
            : undefined;
        this.eventBuffer = this.engineMonitor ? new RpcEventBuffer((event) => this.emitEvent(event)) : undefined;
        const childProcess = spawnRpcClientProcess({
            cliPath,
            cliArgs: args,
            cwd: this.options.cwd,
            env: this.options.env,
            runtimeExecutable: this.options.runtimeExecutable,
            runtimeArgs: this.options.runtimeArgs,
            interactiveEngine: this.engineMonitor !== undefined,
        });
        this.process = childProcess;
        this.stdinWriter = new BoundedWriter(childProcess.stdin, {
            maxFrameBytes: INTERACTIVE_ENGINE_MAX_FRAME_BYTES,
            maxQueuedBytes: 2 * INTERACTIVE_ENGINE_MAX_FRAME_BYTES,
        });
        childProcess.once("exit", (code, signal) => {
            if (generation !== this.generation)
                return;
            const error = this.createProcessExitError(code, signal);
            this.exitError = error;
            this.stdinWriter?.close(error);
            this.engineMonitor?.fail(error);
            this.activeActivityIds.clear();
            this.options.interactiveEngine?.onActivityChange?.(false);
            this.rejectPendingRequests(error);
        });
        childProcess.once("error", (error) => {
            if (generation !== this.generation)
                return;
            this.exitError = error;
            this.stdinWriter?.close(error);
            this.engineMonitor?.fail(error);
            this.rejectPendingRequests(error);
        });
        childProcess.stdin?.on("error", (error) => {
            if (generation !== this.generation)
                return;
            this.exitError = error;
            this.stdinWriter?.close(error);
            this.engineMonitor?.fail(error);
            this.rejectPendingRequests(error);
        });
        childProcess.stderr?.on("data", (data) => {
            if (generation !== this.generation)
                return;
            const next = this.stderr + data.toString();
            this.stderr = Buffer.byteLength(next, "utf8") <= 256 * 1024
                ? next
                : `${Buffer.from(next).subarray(-256 * 1024).toString("utf8")}\n[stderr truncated]`;
            process.stderr.write(data);
        });
        const readerOptions = createInteractiveJsonlOptions(this.engineMonitor !== undefined, this.options.interactiveEngine?.onDiagnostic);
        this.stopReadingStdout = attachJsonlLineReader(childProcess.stdout, (line) => {
            if (generation === this.generation)
                this.handleLine(line);
        }, {
            ...readerOptions,
            onOversizedLine: () => {
                readerOptions.onOversizedLine?.();
                if (generation === this.generation)
                    this.failTransport(new Error("Interactive engine emitted an oversized RPC frame"));
            },
        });
        if (this.engineMonitor)
            await this.engineMonitor.waitUntilReady();
        else
            await sleep(100);
        if (generation !== this.generation || this.process?.exitCode !== null) {
            throw new Error(`Agent process exited immediately. Stderr: ${this.stderr}`);
        }
    }
    async stop() {
        const child = this.process;
        if (!child)
            return;
        this.invalidateEngineShortcuts();
        const terminateTree = this.engineMonitor !== undefined;
        this.stopReadingStdout?.();
        this.engineMonitor?.stop();
        this.stdinWriter?.close(new Error("Agent process stopped"));
        this.engineMonitor = undefined;
        this.stopReadingStdout = null;
        this.stdinWriter = undefined;
        this.process = null;
        this.generation += 1;
        await terminateRpcClientProcess(child, terminateTree);
        this.rejectPendingRequests(new Error("Agent process stopped"));
        this.eventBuffer?.dispose();
        this.eventBuffer = undefined;
        this.activeActivityIds.clear();
        this.options.interactiveEngine?.onActivityChange?.(false);
    }
    /** Subscribe to agent events. */
    onEvent(listener) {
        this.eventListeners.push(listener);
        return () => {
            const index = this.eventListeners.indexOf(listener);
            if (index !== -1) {
                this.eventListeners.splice(index, 1);
            }
        };
    }
    onExtensionUIRequest(listener) {
        this.extensionUIListeners.push(listener);
        for (const request of this.pendingExtensionUIRequests.splice(0))
            listener(request);
        return () => {
            const index = this.extensionUIListeners.indexOf(listener);
            if (index !== -1)
                this.extensionUIListeners.splice(index, 1);
        };
    }
    async respondExtensionUI(response) {
        await this.requireWriter().write(serializeJsonLine(response));
    }
    onInteractiveEngineMessage(listener) {
        this.engineMessageListeners.push(listener);
        for (const message of this.pendingEngineMessages.splice(0))
            listener(message);
        this.pendingEngineMessageBytes = 0;
        return () => {
            const index = this.engineMessageListeners.indexOf(listener);
            if (index !== -1)
                this.engineMessageListeners.splice(index, 1);
        };
    }
    onInteractiveEngineKeybindingState(listener) {
        const messageListener = (message) => {
            if (message.type === "engine_keybindings_reloaded")
                listener(message.state);
        };
        this.engineMessageListeners.push(messageListener);
        if (this.latestEngineKeybindingState)
            listener(this.latestEngineKeybindingState);
        return () => {
            const index = this.engineMessageListeners.indexOf(messageListener);
            if (index !== -1)
                this.engineMessageListeners.splice(index, 1);
        };
    }
    sendInteractiveEngineCommand(command) {
        const writer = this.stdinWriter;
        if (!writer)
            return;
        const frame = serializeInteractiveEngineFrame(command);
        if (command.type === "engine_custom_render" || command.type === "engine_tool_render" || command.type === "engine_message_render") {
            writer.offerLatest(`render:${command.componentId}`, frame);
            return;
        }
        this.bestEffort(writer.write(frame), `engine command ${command.type}`);
    }
    waitForInteractiveEngineBound() { return this.engineMonitor?.waitUntilBound() ?? Promise.resolve(); }
    getEnginePid() { return this.enginePid; }
    getGeneration() { return this.generation; }
    async restart(sessionFile) {
        await this.stop();
        this.options = { ...this.options, args: restartArgs(this.options.args, sessionFile) };
        await this.start();
        await this.waitForInteractiveEngineBound();
    }
    async requestInternal(command) {
        return this.data(await this.request(command));
    }
    getStderr() {
        return this.stderr;
    }
    waitForIdle(timeout = 60000) {
        return waitForRpcIdle(this, timeout);
    }
    collectEvents(timeout = 60000) {
        return collectRpcEvents(this, timeout);
    }
    async promptAndWait(message, images, timeout = 60000) {
        const eventsPromise = this.collectEvents(timeout);
        await this.prompt(message, images);
        return eventsPromise;
    }
    handleLine(line) {
        if (this.engineMonitor?.handleLine(line))
            return;
        try {
            const data = JSON.parse(line);
            if (data.type === "extension_ui_request") {
                const request = data;
                if (this.extensionUIListeners.length === 0)
                    this.pendingExtensionUIRequests.push(request);
                for (const listener of this.extensionUIListeners)
                    listener(request);
                return;
            }
            if (data.type === "response" && data.id && this.pendingRequests.has(data.id)) {
                const pending = this.pendingRequests.get(data.id);
                this.pendingRequests.delete(data.id);
                pending.resolve(data);
                return;
            }
            const event = data;
            if (this.eventBuffer)
                this.eventBuffer.enqueue(event);
            else
                this.emitEvent(event);
        }
        catch {
            if (this.engineMonitor)
                this.failTransport(new Error("Interactive engine emitted malformed JSONL"));
        }
    }
    emitEvent(event) {
        for (const listener of this.eventListeners)
            listener(event);
    }
    emitInteractiveEngineMessage(message) {
        if (this.engineMessageListeners.length === 0 && message.type.startsWith("engine_custom_")) {
            const bytes = Buffer.byteLength(JSON.stringify(message), "utf8");
            while (this.pendingEngineMessages.length > 0 && this.pendingEngineMessageBytes + bytes > INTERACTIVE_ENGINE_MAX_FRAME_BYTES) {
                const removed = this.pendingEngineMessages.shift();
                this.pendingEngineMessageBytes -= Buffer.byteLength(JSON.stringify(removed), "utf8");
            }
            if (bytes <= INTERACTIVE_ENGINE_MAX_FRAME_BYTES) {
                this.pendingEngineMessages.push(message);
                this.pendingEngineMessageBytes += bytes;
            }
        }
        for (const listener of this.engineMessageListeners)
            listener(message);
    }
    createProcessExitError(code, signal) {
        return new Error(`Agent process exited (code=${code} signal=${signal}). Stderr: ${this.stderr}`);
    }
    invalidateEngineShortcuts() {
        if (!this.latestEngineKeybindingState || this.latestEngineKeybindingState.shortcuts.length === 0)
            return;
        const state = { ...this.latestEngineKeybindingState, shortcuts: [] };
        this.latestEngineKeybindingState = state;
        this.emitInteractiveEngineMessage({ type: "engine_keybindings_reloaded", state });
    }
    observeInteractiveEngineMessage(message) {
        if (message.type === "engine_ready")
            this.enginePid = message.pid;
        if (message.type === "engine_keybindings_reloaded")
            this.latestEngineKeybindingState = message.state;
        if (message.type === "engine_activity_started")
            this.activeActivityIds.add(message.activity.id);
        else if (message.type === "engine_activity_finished")
            this.activeActivityIds.delete(message.activityId);
        this.options.interactiveEngine?.onActivityChange?.(this.activeActivityIds.size > 0);
        this.emitInteractiveEngineMessage(message);
    }
    rejectPendingRequests(error) {
        for (const { reject } of this.pendingRequests.values())
            reject(error);
        this.pendingRequests.clear();
    }
    failTransport(error) {
        this.exitError = error;
        this.stdinWriter?.close(error);
        this.engineMonitor?.fail(error);
        this.rejectPendingRequests(error);
        this.options.interactiveEngine?.onDiagnostic({
            activity: undefined, elapsedMs: 0, level: "unresponsive", message: error.message,
        });
    }
    requireWriter() {
        if (!this.stdinWriter)
            throw new Error("Agent process stdin is not writable");
        return this.stdinWriter;
    }
    bestEffort(operation, label) {
        void operation.catch((error) => {
            if (!this.process || this.exitError)
                return;
            this.options.interactiveEngine?.onDiagnostic({
                activity: undefined, elapsedMs: 0, level: "unresponsive",
                message: `Interactive engine ${label} failed: ${error.message}`,
            });
        });
    }
    async request(command) {
        const childProcess = this.process;
        if (!childProcess)
            throw new Error("Client not started");
        if (this.exitError)
            throw this.exitError;
        if (childProcess.exitCode !== null) {
            const error = this.createProcessExitError(childProcess.exitCode, childProcess.signalCode);
            this.exitError = error;
            throw error;
        }
        const id = `req_${++this.requestId}`;
        const fullCommand = { ...command, id };
        let timeout;
        let rejectResponse;
        const response = new Promise((resolve, reject) => {
            rejectResponse = reject;
            this.pendingRequests.set(id, {
                resolve: (value) => { if (timeout)
                    clearTimeout(timeout); resolve(value); },
                reject: (error) => { if (timeout)
                    clearTimeout(timeout); reject(error); },
            });
        });
        try {
            await this.requireWriter().write(serializeJsonLine(fullCommand));
        }
        catch (error) {
            this.pendingRequests.delete(id);
            rejectResponse(error instanceof Error ? error : new Error(String(error)));
            return response;
        }
        if (this.pendingRequests.has(id) && !LONG_LIVED_COMMANDS.has(command.type)) {
            timeout = setTimeout(() => {
                this.pendingRequests.delete(id);
                rejectResponse(new Error(`Timeout waiting for response to ${command.type}. Stderr: ${this.stderr}`));
            }, this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
        }
        return response;
    }
    assertSuccess(response) {
        if (!response.success)
            throw new Error(response.error);
    }
    data(response) {
        this.assertSuccess(response);
        const successResponse = response;
        return successResponse.data;
    }
}
//# sourceMappingURL=rpc-client.js.map