/**
 * ACode SDK Adapter
 *
 * 主流程：直接加载随 aws-runtime-bridge 内置发布的 ACode headless runtime，
 * 不依赖外部 CLI 安装；将 ACode 的结构化事件映射为 runtime-bridge 统一事件流。
 */
import { EventEmitter } from "node:events";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { v4 as uuidv4 } from "uuid";
import { getAcodeConfigFile, getAcodeSkillsDir, getRuntimeHomeDir, } from "../config.js";
import { getReleasedAcodeEntryPath } from "../services/acode-package.js";
import { IdlePollLoop } from "./idle-poll-loop.js";
import { getCanonicalMcpDetail, getToolActionInfo, } from "./types.js";
const ACODE_AUTO_POLL_PROMPT = [
    "Continue the AgentsWork message loop now.",
    "You MUST call the exact MCP tool named aws-mcp__poll_message as your next action and wait for the next assigned message or task.",
    "Do not answer in plain text before calling aws-mcp__poll_message.",
    "After handling any received work, reply through the appropriate aws-mcp messaging tool, then continue listening again.",
].join(" ");
const ACODE_AUTO_POLL_DELAY_MS = 250;
// Backoff used when a polling-driven turn fails (e.g., 60s MCP tool timeout,
// model 5xx, provider unreachable). Larger than ACODE_AUTO_POLL_DELAY_MS to
// avoid busy-looping when the same failure recurs on every retry.
const ACODE_AUTO_POLL_FAILURE_DELAY_MS = 2000;
const ACODE_MESSAGE_POLLING_IDLE_PROMPT = "系统提醒：当前工作已完成或运行时已进入空闲状态。请立即调用 poll_message 工具阻塞等待新消息和流程线任务提醒；收到流程任务提醒后调用 my_task 查看已由后台静默抢占的任务详情并直接处理；完成任务后调用 submit_task_result 提交结果，然后继续 poll_message。";
function isMessagePollingIdleCommand(command) {
    const normalized = command.trim();
    if (!normalized)
        return false;
    const lower = normalized.toLowerCase();
    return (lower.includes("poll_message") ||
        lower.includes("get_message") ||
        normalized.includes("取消息"));
}
function getAcodePackageEntryPath() {
    return path.resolve(getReleasedAcodeEntryPath());
}
export function toAcodeEsmImportSpecifier(modulePath) {
    return pathToFileURL(path.resolve(modulePath)).href;
}
function toUsage(value) {
    if (!value || typeof value !== "object") {
        return undefined;
    }
    const usage = value;
    const inputTokens = toFiniteTokenCount(usage.inputTokens, usage.input_tokens, usage.promptTokens, usage.prompt_tokens);
    const outputTokens = toFiniteTokenCount(usage.outputTokens, usage.output_tokens, usage.completionTokens, usage.completion_tokens);
    return {
        inputTokens,
        outputTokens,
    };
}
function toFiniteTokenCount(...values) {
    for (const value of values) {
        const count = Number(value);
        if (Number.isFinite(count) && count > 0) {
            return count;
        }
    }
    return 0;
}
/**
 * Check whether an ACode turn_complete event represents a failed turn.
 * Main flow: only explicit failed=true prevents error status from being reset to waiting_input.
 */
export function isFailedAcodeTurnComplete(data) {
    return data.failed === true;
}
function toAcodeMcpActionDetail(actionDetail, server, tool) {
    if (!actionDetail) {
        return tool ? `${server}/${tool}` : server;
    }
    const prefix = `${server}__`;
    if (actionDetail.startsWith(prefix)) {
        return getCanonicalMcpDetail(`${server}__${actionDetail.slice(prefix.length)}`);
    }
    return getCanonicalMcpDetail(actionDetail.replace("/", "__"));
}
export class AcodeSdkAdapter extends EventEmitter {
    constructor() {
        super(...arguments);
        this.providerId = "acode";
        this.displayName = "ACode";
        this.sessions = new Map();
    }
    async startSession(sessionId, config) {
        const runtimeHome = getRuntimeHomeDir();
        const session = {
            sessionId,
            status: "starting",
            messages: [],
            createdAt: new Date().toISOString(),
            totalUsage: { inputTokens: 0, outputTokens: 0 },
        };
        this.sessions.set(sessionId, {
            adapterSession: session,
            config,
            runtime: await this.createRuntime(sessionId, config, runtimeHome),
            autoPollActive: false,
        });
        this.emit("status-change", sessionId, "starting");
        const entry = this.sessions.get(sessionId);
        if (!entry) {
            throw new Error("ACode session was not initialized");
        }
        try {
            await entry.runtime.start();
            session.status = "waiting_input";
            this.emit("status-change", sessionId, "waiting_input");
            if (config.initialPrompt?.trim()) {
                this.startInitialPromptTurn(sessionId, config.initialPrompt);
            }
            else {
                // R1 fix: when no initial prompt is provided, explicitly arm the
                // auto-poll loop. Without this, the session sits at 'waiting_input'
                // forever if setIdleCommands hasn't been called yet (or is configured
                // with a non-polling command). setIdleCommands will cancel this timer
                // and replace it with the configured idle loop when invoked.
                this.armIdleLoopOrAutoPoll(sessionId);
            }
        }
        catch (error) {
            session.status = "error";
            this.emitRuntimeErrorEvent(sessionId, error);
            this.emit("status-change", sessionId, "error");
            this.sessions.delete(sessionId);
            throw error;
        }
    }
    async sendMessage(sessionId, message) {
        const entry = this.requireSession(sessionId);
        this.cancelScheduledAutoPoll(entry);
        entry.failedTurnRecoveryScheduled = false;
        entry.idlePollLoop?.stop();
        const userMessage = {
            id: uuidv4(),
            sessionId,
            role: "user",
            content: message,
            timestamp: new Date().toISOString(),
        };
        entry.adapterSession.messages.push(userMessage);
        this.emit("conversation-message", sessionId, userMessage);
        entry.adapterSession.status = "thinking";
        this.emit("status-change", sessionId, "thinking");
        try {
            await entry.runtime.submit(message);
            this.recoverResolvedTurnWithoutTerminalEvent(sessionId);
        }
        catch (error) {
            this.emitRuntimeError(sessionId, error, { recover: true });
        }
    }
    async sendConfirmation(_sessionId, _accept) { }
    async abortCurrentTurn(sessionId) {
        await this.requireSession(sessionId).runtime.abort();
    }
    async terminateSession(sessionId) {
        const entry = this.sessions.get(sessionId);
        if (!entry) {
            return;
        }
        this.cancelScheduledAutoPoll(entry);
        entry.failedTurnRecoveryScheduled = false;
        entry.idlePollLoop?.stop();
        await entry.runtime.stop();
        entry.adapterSession.status = "terminated";
        this.sessions.delete(sessionId);
        this.emit("status-change", sessionId, "terminated");
    }
    getConversation(sessionId) {
        return this.sessions.get(sessionId)?.adapterSession.messages ?? [];
    }
    hasSession(sessionId) {
        return this.sessions.has(sessionId);
    }
    getProviderSessionId(sessionId) {
        return this.sessions.has(sessionId) ? sessionId : undefined;
    }
    getSessionStatus(sessionId) {
        return this.sessions.get(sessionId)?.adapterSession.status;
    }
    cleanup() {
        for (const sessionId of this.sessions.keys()) {
            void this.terminateSession(sessionId);
        }
    }
    setIdleCommands(sessionId, commands) {
        const entry = this.sessions.get(sessionId);
        if (!entry)
            return;
        entry.idleCommands = commands;
        this.cancelScheduledAutoPoll(entry);
        if (commands.idleInputCommand &&
            isMessagePollingIdleCommand(commands.idleInputCommand)) {
            const loop = this.getIdlePollLoop(entry);
            loop.start();
            return;
        }
        if (entry.adapterSession.status === "waiting_input" &&
            (commands.idleInputCommand || commands.nonInputCommand)) {
            this.getIdlePollLoop(entry).start();
        }
    }
    /** Injects a runtime constructor for adapter tests without loading the packaged ACode module. */
    setRuntimeConstructorForTest(runtimeConstructor) {
        this.runtimeConstructor = runtimeConstructor;
    }
    async createRuntime(sessionId, config, runtimeHome) {
        const ACodeRuntime = await this.loadRuntimeConstructor();
        const runtime = new ACodeRuntime({
            sessionId,
            workingDirectory: config.workingDirectory,
            configPath: config.mcpConfigPath || getAcodeConfigFile(runtimeHome),
            skillsDir: getAcodeSkillsDir(runtimeHome),
            model: config.model,
            systemPrompt: typeof config.systemPrompt === "string"
                ? config.systemPrompt
                : undefined,
            env: config.envOverrides,
        });
        runtime.on("event", (event) => this.forwardAcodeEvent(event));
        return runtime;
    }
    async loadRuntimeConstructor() {
        if (this.runtimeConstructor) {
            return this.runtimeConstructor;
        }
        const modulePath = getAcodePackageEntryPath();
        const module = (await import(toAcodeEsmImportSpecifier(modulePath)));
        if (!module.ACodeRuntime) {
            throw new Error(`ACode runtime export not found: ${modulePath}`);
        }
        this.runtimeConstructor = module.ACodeRuntime;
        return this.runtimeConstructor;
    }
    requireSession(sessionId) {
        const entry = this.sessions.get(sessionId);
        if (!entry) {
            throw new Error("ACode session not found");
        }
        return entry;
    }
    /**
     * Run the startup prompt after the HTTP launch has already completed.
     * Main flow: mark the session busy -> submit the prompt in the background ->
     * rely on normal ACode events to publish turn completion or errors.
     */
    startInitialPromptTurn(sessionId, initialPrompt) {
        const entry = this.sessions.get(sessionId);
        if (!entry) {
            return;
        }
        entry.adapterSession.status = "thinking";
        this.emit("status-change", sessionId, "thinking");
        void entry.runtime
            .submit(initialPrompt)
            .then(() => {
            this.recoverResolvedTurnWithoutTerminalEvent(sessionId);
        })
            .catch((error) => {
            const currentEntry = this.sessions.get(sessionId);
            if (!currentEntry) {
                return;
            }
            // R4 fix: a failed initial prompt turn used to leave the session
            // permanently in 'error' because autoPollActive was false at this
            // point (only set by submitAutoPollPrompt), so the R2 recovery in
            // forwardAcodeEvent skipped it, and no idle loop was armed.
            // Now we always recover to 'waiting_input' and arm the idle loop
            // so the agent can retry via poll_message.
            this.emitRuntimeError(sessionId, error, { recover: true });
        });
    }
    forwardAcodeEvent(event) {
        const entry = this.sessions.get(event.sessionId);
        if (!entry) {
            return;
        }
        if (event.type === "assistant_delta") {
            const text = typeof event.data.text === "string" ? event.data.text : "";
            entry.adapterSession.messages.push({
                id: uuidv4(),
                sessionId: event.sessionId,
                role: "assistant",
                content: text,
                timestamp: event.timestamp,
            });
            this.emitProviderEvent(event, "text_delta", { text });
            return;
        }
        if (event.type === "thinking_delta") {
            const text = typeof event.data.text === "string" ? event.data.text : "";
            this.emitProviderEvent(event, "thinking", { text });
            return;
        }
        if (event.type === "ai_request") {
            this.emitProviderEvent(event, "ai_request", {
                model: typeof event.data.model === "string" ? event.data.model : undefined,
                endpoint: typeof event.data.endpoint === "string"
                    ? event.data.endpoint
                    : undefined,
                stream: typeof event.data.stream === "boolean"
                    ? event.data.stream
                    : undefined,
                messageCount: typeof event.data.messageCount === "number"
                    ? event.data.messageCount
                    : undefined,
                toolCount: typeof event.data.toolCount === "number"
                    ? event.data.toolCount
                    : undefined,
                lastUserMessagePreview: typeof event.data.lastUserMessagePreview === "string"
                    ? event.data.lastUserMessagePreview
                    : undefined,
                requestBody: typeof event.data.requestBody === "string"
                    ? event.data.requestBody
                    : undefined,
            });
            return;
        }
        if (event.type === "turn_complete") {
            const usage = toUsage(event.data.usage);
            if (usage) {
                entry.adapterSession.totalUsage.inputTokens += usage.inputTokens;
                entry.adapterSession.totalUsage.outputTokens += usage.outputTokens;
            }
            if (isFailedAcodeTurnComplete(event.data)) {
                // R2 fix: a failed polling-driven turn (e.g., 60s MCP tool timeout,
                // model 5xx, provider unreachable) used to leave the session parked
                // in 'error' or 'thinking' forever, so the agent stopped calling
                // poll_message permanently. Recover to 'waiting_input' and re-arm
                // the auto-poll with a longer backoff so a transient failure
                // doesn't break the message loop.
                //
                // R4 fix: previously this only recovered when autoPollActive was
                // true (i.e. during a submitAutoPollPrompt-driven turn). When the
                // initial prompt turn failed, autoPollActive was false and the
                // session got permanently stuck in 'error'. Now we always recover
                // for non-terminal sessions so the idle loop can retry.
                this.recoverTurnFailure(event.sessionId, entry);
                return;
            }
            this.emitProviderEvent(event, "turn_complete", {
                usage: entry.adapterSession.totalUsage,
                responseBody: typeof event.data.responseBody === "string"
                    ? event.data.responseBody
                    : undefined,
                finishReason: typeof event.data.finishReason === "string"
                    ? event.data.finishReason
                    : undefined,
            });
            entry.adapterSession.status = "waiting_input";
            this.emit("status-change", event.sessionId, "waiting_input", {
                usage: entry.adapterSession.totalUsage,
            });
            this.armIdleLoopOrAutoPoll(event.sessionId);
            return;
        }
        if (event.type === "session_complete") {
            this.cancelScheduledAutoPoll(entry);
            entry.failedTurnRecoveryScheduled = false;
            entry.idlePollLoop?.stop();
            entry.adapterSession.status = "completed";
            this.emitProviderEvent(event, "session_complete", {
                exitCode: Number(event.data.exitCode) || 0,
            });
            this.emit("status-change", event.sessionId, "completed");
            return;
        }
        if (event.type === "error") {
            entry.adapterSession.status = "error";
            this.emitProviderEvent(event, "error", {
                text: String(event.data.message || "ACode runtime error"),
            });
            this.emit("status-change", event.sessionId, "error");
            if (!entry.autoPollActive) {
                this.recoverTurnFailure(event.sessionId, entry);
            }
            return;
        }
        if (event.type === "mcp_status") {
            // R3 fix: the ACode runtime emits a startup mcp_status event with no
            // `action` field but containing a `tools` array describing what the
            // LLM can actually call. Validate that a poll_message tool is exposed,
            // otherwise the message loop cannot continue.
            //
            // The previous version of this check matched the literal string
            // 'aws-mcp' against the user-configured MCP server keys in the
            // `servers` field, which produced false positives when the operator
            // named the entry differently (e.g. 'aws-mcp-server' or 'agent-mcp')
            // while still exposing aws-mcp's tools to the LLM. Worse, on a
            // mismatch it emitted a generic `type: 'error'` ProviderEvent, which
            // the rest of the bridge promotes to a fatal session-level 'error'
            // status that the dashboard cannot recover from — the LLM keeps
            // working but the timeline header gets stuck on "发生错误" forever.
            //
            // The fix below validates against the actual `tools` list (the same
            // tools the LLM sees) and on a missing poll_message it only logs a
            // warning. The runtime will naturally emit a real 'error' event the
            // first time the LLM tries to call a missing tool, which the existing
            // error handling already routes correctly without permanently locking
            // the session status.
            if (!event.data.action && Array.isArray(event.data.tools)) {
                const tools = event.data.tools;
                const hasPollMessage = tools.some((tool) => typeof tool?.name === "string" &&
                    tool.name.includes("poll_message"));
                if (!hasPollMessage) {
                    const message = "ACode runtime is not exposing a poll_message tool. The LLM cannot call poll_message. " +
                        "Please configure an MCP server that provides poll_message (e.g. aws-mcp) in the dashboard panel for this runtime.";
                    console.warn(`[${this.displayName}] ${message}`);
                }
                return;
            }
            const server = typeof event.data.server === "string" ? event.data.server : "mcp";
            const tool = typeof event.data.tool === "string" ? event.data.tool : "";
            const toolName = server && tool ? `${server}__${tool}` : tool || server;
            const actionInfo = getToolActionInfo(toolName);
            const actionDetail = toAcodeMcpActionDetail(actionInfo.actionDetail, server, tool);
            const toolUseId = typeof event.data.toolUseId === "string"
                ? event.data.toolUseId
                : undefined;
            const eventData = {
                toolName: actionDetail,
                actionType: actionInfo.actionType,
                actionLabel: actionInfo.actionLabel,
                actionDetail,
                ...(toolUseId ? { toolUseId, actionId: toolUseId } : {}),
            };
            if (event.data.action === "tool_result") {
                this.emitProviderEvent(event, "tool_use_end", {
                    ...eventData,
                    toolResult: typeof event.data.result === "string"
                        ? event.data.result
                        : JSON.stringify(event.data.result ?? ""),
                });
                entry.adapterSession.status = "thinking";
                this.emit("status-change", event.sessionId, "thinking");
                return;
            }
            const status = actionInfo.actionType === "idle" ? "waiting_input" : "tool_using";
            entry.adapterSession.status = status;
            this.emitProviderEvent(event, "tool_use_start", eventData);
            this.emit("status-change", event.sessionId, status, eventData);
            return;
        }
        this.emitProviderEvent(event, "thinking", {
            text: JSON.stringify(event.data),
        });
    }
    emitProviderEvent(event, type, data) {
        this.emit("event", {
            type,
            sessionId: event.sessionId,
            timestamp: event.timestamp,
            data,
        });
    }
    /**
     * After a completed turn, prefer the configured IdlePollLoop (set via setIdleCommands).
     * Fall back to the legacy auto-poll mechanism when no idle commands are configured.
     */
    armIdleLoopOrAutoPoll(sessionId) {
        const entry = this.sessions.get(sessionId);
        if (!entry || this.isTerminalStatus(entry.adapterSession.status)) {
            return;
        }
        if (entry.idleCommands?.idleInputCommand) {
            entry.idlePollLoop?.start();
            return;
        }
        this.scheduleAutoPoll(sessionId);
    }
    getIdlePollLoop(entry) {
        if (entry.idlePollLoop)
            return entry.idlePollLoop;
        const sessionId = entry.adapterSession.sessionId;
        entry.idlePollLoop = new IdlePollLoop({
            getStatus: () => this.sessions.get(sessionId)?.adapterSession.status,
            getCommand: () => this.sessions.get(sessionId)?.idleCommands?.idleInputCommand,
            isPollingCommand: (command) => isMessagePollingIdleCommand(command),
            dispatchPollingPrompt: () => this.submitAutoPollPrompt(sessionId, ACODE_MESSAGE_POLLING_IDLE_PROMPT),
            dispatchRegularIdleCommand: (command) => this.submitAutoPollPrompt(sessionId, command),
        });
        return entry.idlePollLoop;
    }
    submitAutoPollPrompt(sessionId, prompt) {
        const entry = this.sessions.get(sessionId);
        if (!entry || this.isTerminalStatus(entry.adapterSession.status))
            return;
        entry.autoPollActive = true;
        entry.adapterSession.status = "thinking";
        this.emit("status-change", sessionId, "thinking");
        void entry.runtime
            .submit(prompt)
            .catch((error) => {
            this.emitRuntimeError(sessionId, error);
        })
            .finally(() => {
            const latestEntry = this.sessions.get(sessionId);
            if (!latestEntry)
                return;
            latestEntry.autoPollActive = false;
            // If the R2 recovery path already scheduled a delayed re-arm for a
            // failed polling-driven turn, skip armIdleLoopOrAutoPoll so we do
            // not double-arm (immediate fire via IdlePollLoop AND a 2s timer).
            // The recovery timer is the source of truth here: the flag is just
            // a defensive marker; the timer itself is what matters.
            if (latestEntry.failedTurnRecoveryScheduled ||
                latestEntry.autoPollTimer) {
                latestEntry.failedTurnRecoveryScheduled = false;
                return;
            }
            if (latestEntry.adapterSession.status === "error") {
                this.recoverTurnFailure(sessionId, latestEntry);
                return;
            }
            if (latestEntry.adapterSession.status === "thinking" ||
                latestEntry.adapterSession.status === "tool_using") {
                this.recoverResolvedTurnWithoutTerminalEvent(sessionId);
                return;
            }
            if (latestEntry.adapterSession.status === "waiting_input") {
                this.armIdleLoopOrAutoPoll(sessionId);
            }
        });
    }
    /**
     * Continue ACode's headless agent loop after a completed turn.
     * Main flow: debounce duplicate scheduling -> mark busy -> submit an internal
     * poll instruction -> let normal ACode events publish MCP/tool/turn states.
     *
     * @param delayMs Optional override for the default 250ms backoff. The R2
     *   recovery path uses ACODE_AUTO_POLL_FAILURE_DELAY_MS (2s) to avoid
     *   busy-looping when the same failure recurs on every retry.
     */
    scheduleAutoPoll(sessionId, delayMs = ACODE_AUTO_POLL_DELAY_MS) {
        const entry = this.sessions.get(sessionId);
        if (!entry ||
            entry.autoPollActive ||
            entry.autoPollTimer ||
            this.isTerminalStatus(entry.adapterSession.status)) {
            return;
        }
        entry.autoPollTimer = setTimeout(() => {
            const currentEntry = this.sessions.get(sessionId);
            if (!currentEntry ||
                currentEntry.autoPollActive ||
                this.isTerminalStatus(currentEntry.adapterSession.status)) {
                return;
            }
            currentEntry.autoPollTimer = undefined;
            this.submitAutoPollPrompt(sessionId, ACODE_AUTO_POLL_PROMPT);
        }, delayMs);
    }
    cancelScheduledAutoPoll(entry) {
        if (!entry.autoPollTimer) {
            return;
        }
        clearTimeout(entry.autoPollTimer);
        entry.autoPollTimer = undefined;
    }
    isTerminalStatus(status) {
        return status === "completed" || status === "terminated";
    }
    emitRuntimeError(sessionId, error, options) {
        const currentEntry = this.sessions.get(sessionId);
        if (!currentEntry) {
            return;
        }
        currentEntry.adapterSession.status = "error";
        this.emitRuntimeErrorEvent(sessionId, error);
        this.emit("status-change", sessionId, "error");
        if (options?.recover) {
            this.recoverTurnFailure(sessionId, currentEntry);
        }
    }
    emitRuntimeErrorEvent(sessionId, error) {
        const timestamp = new Date().toISOString();
        this.emit("event", {
            type: "error",
            sessionId,
            timestamp,
            data: { text: error instanceof Error ? error.message : String(error) },
        });
    }
    recoverTurnFailure(sessionId, entry) {
        if (this.isTerminalStatus(entry.adapterSession.status)) {
            return;
        }
        if (entry.autoPollTimer || entry.failedTurnRecoveryScheduled) {
            if (entry.adapterSession.status !== "waiting_input") {
                entry.adapterSession.status = "waiting_input";
                this.emit("status-change", sessionId, "waiting_input");
            }
            return;
        }
        this.cancelScheduledAutoPoll(entry);
        entry.adapterSession.status = "waiting_input";
        this.emit("status-change", sessionId, "waiting_input");
        entry.failedTurnRecoveryScheduled = true;
        entry.autoPollTimer = setTimeout(() => {
            const currentEntry = this.sessions.get(sessionId);
            if (!currentEntry ||
                currentEntry.autoPollActive ||
                this.isTerminalStatus(currentEntry.adapterSession.status)) {
                if (currentEntry) {
                    currentEntry.failedTurnRecoveryScheduled = false;
                }
                return;
            }
            currentEntry.autoPollTimer = undefined;
            currentEntry.failedTurnRecoveryScheduled = false;
            if (currentEntry.idleCommands?.idleInputCommand) {
                this.getIdlePollLoop(currentEntry).start();
            }
            else {
                this.submitAutoPollPrompt(sessionId, ACODE_AUTO_POLL_PROMPT);
            }
        }, ACODE_AUTO_POLL_FAILURE_DELAY_MS);
    }
    /**
     * Recover turns where runtime.submit() resolved but no terminal turn event arrived.
     *
     * Main flow: ACode should normally emit turn_complete/session_complete/error before
     * submit resolves. Some edge paths can emit only intermediate events (for example
     * command_status or tool_result), then resolve. At that point there is no active
     * turn anymore, so keeping the adapter at thinking/tool_using makes the dashboard
     * look permanently stuck. Recover to waiting_input and arm the listening loop.
     */
    recoverResolvedTurnWithoutTerminalEvent(sessionId) {
        const entry = this.sessions.get(sessionId);
        if (!entry || this.isTerminalStatus(entry.adapterSession.status)) {
            return;
        }
        if (entry.adapterSession.status !== "thinking" &&
            entry.adapterSession.status !== "tool_using") {
            return;
        }
        entry.adapterSession.status = "waiting_input";
        this.emit("status-change", sessionId, "waiting_input");
        this.armIdleLoopOrAutoPoll(sessionId);
    }
}
