/**
 * OpenAI Codex SDK Adapter
 *
 * 通过 @openai/codex-sdk 的 Thread/runStreamed() API
 * 将 Codex 事件流映射到 runtime-bridge 统一 ProviderEvent。
 */
import { EventEmitter } from 'node:events';
import { v4 as uuidv4 } from 'uuid';
import { IdlePollLoop } from './idle-poll-loop.js';
import { getToolActionInfo } from './types.js';
function hasErrorCode(error) {
    return error instanceof Error && 'code' in error && typeof error.code === 'string';
}
function isMissingPackageError(error, packageName) {
    return hasErrorCode(error) && error.code === 'ERR_MODULE_NOT_FOUND' && error.message.includes(packageName);
}
const CODEX_IDLE_THRESHOLD_MS = 1500;
function isRecord(value) {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function toRecord(value) {
    return isRecord(value) ? value : { value };
}
function serializeValue(value) {
    if (typeof value === 'string') {
        return value;
    }
    try {
        return JSON.stringify(value);
    }
    catch {
        return String(value);
    }
}
function toEnvRecord(env, overrides) {
    const result = {};
    for (const [key, value] of Object.entries(env)) {
        if (typeof value === 'string') {
            result[key] = value;
        }
    }
    return { ...result, ...(overrides ?? {}) };
}
function splitCommandLine(command) {
    const tokens = [];
    let current = '';
    let quote = null;
    let escaping = false;
    for (const char of command.trim()) {
        if (escaping) {
            current += char;
            escaping = false;
            continue;
        }
        if (char === '\\' && quote !== "'") {
            escaping = true;
            continue;
        }
        if ((char === '"' || char === "'") && quote === null) {
            quote = char;
            continue;
        }
        if (char === quote) {
            quote = null;
            continue;
        }
        if (/\s/.test(char) && quote === null) {
            if (current.length > 0) {
                tokens.push(current);
                current = '';
            }
            continue;
        }
        current += char;
    }
    if (escaping) {
        current += '\\';
    }
    if (current.length > 0) {
        tokens.push(current);
    }
    return tokens;
}
function commandName(command) {
    const normalized = (command || '').replace(/\\/g, '/').toLowerCase();
    const lastSegment = normalized.split('/').pop() || normalized;
    return lastSegment.replace(/\.(cmd|exe)$/i, '');
}
function isPathLikeCommand(command) {
    return command.includes('/') || command.includes('\\') || /^[a-zA-Z]:/.test(command);
}
function parseCodexCommand(command) {
    const tokens = splitCommandLine(command || 'codex');
    if (tokens.length === 0) {
        return { args: [] };
    }
    const [first, second, ...rest] = tokens;
    const firstName = commandName(first);
    if (isPathLikeCommand(first)) {
        return { executablePath: first, args: tokens.slice(1) };
    }
    if ((firstName === 'npx' || firstName === 'pnpm' || firstName === 'yarn' || firstName === 'bun') && commandName(second) === 'codex') {
        return { args: rest };
    }
    if (firstName === 'codex') {
        return { args: tokens.slice(1) };
    }
    return { executablePath: first, args: tokens.slice(1) };
}
function parseBoolean(value) {
    if (/^(true|1|yes)$/i.test(value))
        return true;
    if (/^(false|0|no)$/i.test(value))
        return false;
    return undefined;
}
function stripQuotes(value) {
    if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
        return value.slice(1, -1);
    }
    return value;
}
function setNestedConfig(target, dottedKey, rawValue) {
    const parts = dottedKey.split('.').filter(Boolean);
    if (parts.length === 0)
        return;
    let cursor = target;
    for (const part of parts.slice(0, -1)) {
        const existing = cursor[part];
        if (!isRecord(existing)) {
            cursor[part] = {};
        }
        cursor = cursor[part];
    }
    const value = stripQuotes(rawValue);
    const boolValue = parseBoolean(value);
    cursor[parts[parts.length - 1]] = boolValue ?? value;
}
function readOptionValue(args, index, option) {
    const token = args[index];
    const inlinePrefix = `${option}=`;
    if (token.startsWith(inlinePrefix)) {
        return { value: token.slice(inlinePrefix.length), nextIndex: index };
    }
    return { value: args[index + 1], nextIndex: index + 1 };
}
function parseCodexCommandOverrides(args) {
    const overrides = { additionalDirectories: [], config: {} };
    for (let i = 0; i < args.length; i++) {
        const token = args[i];
        switch (token) {
            case '--model':
            case '-m': {
                const parsed = readOptionValue(args, i, token);
                overrides.model = parsed.value;
                i = parsed.nextIndex;
                break;
            }
            case '--sandbox': {
                const parsed = readOptionValue(args, i, token);
                if (parsed.value === 'read-only' || parsed.value === 'workspace-write' || parsed.value === 'danger-full-access') {
                    overrides.sandboxMode = parsed.value;
                }
                i = parsed.nextIndex;
                break;
            }
            case '--skip-git-repo-check':
                overrides.skipGitRepoCheck = true;
                break;
            case '--add-dir': {
                const parsed = readOptionValue(args, i, token);
                if (parsed.value)
                    overrides.additionalDirectories.push(parsed.value);
                i = parsed.nextIndex;
                break;
            }
            case '--config':
            case '-c': {
                const parsed = readOptionValue(args, i, token);
                if (parsed.value) {
                    const equalsIndex = parsed.value.indexOf('=');
                    if (equalsIndex > 0) {
                        const key = parsed.value.slice(0, equalsIndex);
                        const value = parsed.value.slice(equalsIndex + 1);
                        if (key === 'model_reasoning_effort') {
                            const effort = stripQuotes(value);
                            if (effort === 'minimal' || effort === 'low' || effort === 'medium' || effort === 'high' || effort === 'xhigh') {
                                overrides.modelReasoningEffort = effort;
                            }
                        }
                        else if (key === 'web_search') {
                            const mode = stripQuotes(value);
                            if (mode === 'disabled' || mode === 'cached' || mode === 'live') {
                                overrides.webSearchMode = mode;
                            }
                        }
                        else if (key === 'sandbox_workspace_write.network_access') {
                            overrides.networkAccessEnabled = parseBoolean(stripQuotes(value));
                        }
                        else if (key === 'approval_policy') {
                            const approval = stripQuotes(value);
                            if (approval === 'never' || approval === 'on-request' || approval === 'on-failure' || approval === 'untrusted') {
                                overrides.approvalPolicy = approval;
                            }
                        }
                        else {
                            setNestedConfig(overrides.config, key, value);
                        }
                    }
                }
                i = parsed.nextIndex;
                break;
            }
            default: {
                if (token.startsWith('--model=')) {
                    overrides.model = token.slice('--model='.length);
                }
                else if (token.startsWith('--sandbox=')) {
                    const sandbox = token.slice('--sandbox='.length);
                    if (sandbox === 'read-only' || sandbox === 'workspace-write' || sandbox === 'danger-full-access') {
                        overrides.sandboxMode = sandbox;
                    }
                }
            }
        }
    }
    return overrides;
}
function mapUsage(usage) {
    return {
        inputTokens: usage?.input_tokens ?? 0,
        outputTokens: usage?.output_tokens ?? 0,
    };
}
export class CodexSdkAdapter extends EventEmitter {
    constructor() {
        super(...arguments);
        this.providerId = 'codex';
        this.displayName = 'Codex';
        this.sessions = new Map();
    }
    async startSession(sessionId, config) {
        console.log(`[CodexSdkAdapter] ★★★ 启动 Codex SDK 会话 ★★★ sessionId="${sessionId}", workingDirectory="${config.workingDirectory}"`);
        const adapterSession = {
            sessionId,
            status: 'starting',
            messages: [],
            createdAt: new Date().toISOString(),
            totalUsage: { inputTokens: 0, outputTokens: 0 },
        };
        try {
            const sdk = await this.loadSdk();
            const codex = new sdk.Codex(this.buildCodexOptions(config));
            this.warnUnsupportedMcpConfig(config);
            const thread = codex.startThread(this.buildThreadOptions(config));
            const entry = {
                adapterSession,
                config,
                thread,
                terminating: false,
                currentAssistantText: '',
                emittedToolStarts: new Set(),
                emittedTextLengths: new Map(),
                emittedReasoningLengths: new Map(),
                lastActivityAt: Date.now(),
            };
            this.sessions.set(sessionId, entry);
            if (config.initialPrompt) {
                queueMicrotask(() => {
                    void this.sendMessage(sessionId, config.initialPrompt).catch((promptError) => {
                        const promptErrorMessage = promptError instanceof Error ? promptError.message : String(promptError);
                        const currentSession = this.sessions.get(sessionId);
                        if (currentSession) {
                            currentSession.adapterSession.status = 'error';
                        }
                        this.emit('status-change', sessionId, 'error');
                        this.emitEvent({
                            type: 'error',
                            sessionId,
                            timestamp: new Date().toISOString(),
                            data: { text: `Codex initial prompt failed: ${promptErrorMessage}` },
                        });
                    });
                });
            }
            else {
                adapterSession.status = 'waiting_input';
                this.emit('status-change', sessionId, 'waiting_input');
            }
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            adapterSession.status = 'error';
            this.emit('status-change', sessionId, 'error');
            this.emitEvent({
                type: 'error',
                sessionId,
                timestamp: new Date().toISOString(),
                data: { text: `Codex start failed: ${message}` },
            });
            throw error;
        }
    }
    async sendMessage(sessionId, message) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            throw new Error(`Session ${sessionId} not found`);
        }
        if (session.runningTurn) {
            throw new Error(`Session ${sessionId} already has a running Codex turn`);
        }
        const userMsg = {
            id: uuidv4(),
            sessionId,
            role: 'user',
            content: message,
            timestamp: new Date().toISOString(),
        };
        session.adapterSession.messages.push(userMsg);
        this.emit('conversation-message', sessionId, userMsg);
        session.adapterSession.status = 'thinking';
        this.emit('status-change', sessionId, 'thinking');
        this.stopIdleDetection(sessionId);
        session.lastActivityAt = Date.now();
        const abortController = new AbortController();
        session.currentAbortController = abortController;
        session.runningTurn = this.runTurn(sessionId, session, message, abortController);
        try {
            await session.runningTurn;
        }
        finally {
            session.runningTurn = undefined;
            session.currentAbortController = undefined;
        }
    }
    async sendConfirmation(_sessionId, _accept) {
        // Codex SDK exposes approval behavior through thread options; there is no
        // separate permission response API in the current SDK surface.
    }
    async sendQuestionAnswer(_sessionId, _answers) {
        // Codex SDK does not expose an AskUserQuestion channel in the current event model.
    }
    setIdleCommands(sessionId, commands) {
        const session = this.sessions.get(sessionId);
        if (session) {
            session.idleCommands = commands;
            if (session.adapterSession.status === 'waiting_input' &&
                (commands.idleInputCommand || commands.nonInputCommand)) {
                this.startIdleDetection(sessionId);
            }
        }
    }
    async abortCurrentTurn(sessionId) {
        const session = this.sessions.get(sessionId);
        session?.currentAbortController?.abort();
    }
    async terminateSession(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session)
            return;
        this.stopIdleDetection(sessionId);
        session.terminating = true;
        session.currentAbortController?.abort();
        session.adapterSession.status = 'terminated';
        this.emit('status-change', sessionId, 'terminated');
        this.emitEvent({
            type: 'session_complete',
            sessionId,
            timestamp: new Date().toISOString(),
            data: { exitCode: 0 },
        });
        this.sessions.delete(sessionId);
    }
    async resumeSession(sessionId, providerSessionId, config) {
        const sdk = await this.loadSdk();
        const codex = new sdk.Codex(this.buildCodexOptions(config));
        this.warnUnsupportedMcpConfig(config);
        const thread = codex.resumeThread(providerSessionId, this.buildThreadOptions(config));
        const adapterSession = {
            sessionId,
            providerSessionId,
            status: 'waiting_input',
            messages: [],
            createdAt: new Date().toISOString(),
            totalUsage: { inputTokens: 0, outputTokens: 0 },
        };
        this.sessions.set(sessionId, {
            adapterSession,
            config,
            thread,
            terminating: false,
            currentAssistantText: '',
            emittedToolStarts: new Set(),
            emittedTextLengths: new Map(),
            emittedReasoningLengths: new Map(),
            lastActivityAt: Date.now(),
        });
        this.emit('status-change', sessionId, 'waiting_input');
    }
    getConversation(sessionId) {
        return this.sessions.get(sessionId)?.adapterSession.messages ?? [];
    }
    hasSession(sessionId) {
        return this.sessions.has(sessionId);
    }
    getProviderSessionId(sessionId) {
        const threadId = this.sessions.get(sessionId)?.thread.id;
        return threadId ?? undefined;
    }
    getSessionStatus(sessionId) {
        return this.sessions.get(sessionId)?.adapterSession.status;
    }
    getSessionPid(_sessionId) {
        return undefined;
    }
    cleanup() {
        for (const [sessionId] of this.sessions) {
            void this.terminateSession(sessionId);
        }
        this.sessions.clear();
    }
    /**
     * 按需加载 Codex SDK：只有启动 Codex provider 时才解析重依赖，
     * 避免 bridge 基础安装默认拉取 @openai/codex 的多平台二进制包。
     */
    async loadSdk() {
        if (!this.sdkModule) {
            try {
                this.sdkModule = await import('@openai/codex-sdk');
            }
            catch (error) {
                if (isMissingPackageError(error, '@openai/codex-sdk')) {
                    throw new Error([
                        'Codex SDK provider is not installed.',
                        '',
                        'aws-runtime-bridge keeps heavy provider SDKs optional to reduce global install size.',
                        'To use SDK mode with command "codex", install:',
                        '',
                        '  npm install -g @openai/codex-sdk',
                        '',
                        'If aws-runtime-bridge is installed locally, install the SDK in the same project:',
                        '',
                        '  npm install @openai/codex-sdk',
                        '',
                        `Original error: ${error instanceof Error ? error.message : String(error)}`,
                    ].join('\n'));
                }
                throw error;
            }
        }
        return this.sdkModule;
    }
    buildCodexOptions(config) {
        const env = toEnvRecord(process.env, config.envOverrides);
        const options = { env };
        const apiKey = env.CODEX_API_KEY || env.OPENAI_API_KEY;
        if (apiKey) {
            options.apiKey = apiKey;
        }
        const baseUrl = env.CODEX_BASE_URL || env.OPENAI_BASE_URL;
        if (baseUrl) {
            options.baseUrl = baseUrl;
        }
        const parsedCommand = parseCodexCommand(config.command);
        if (config.executablePath) {
            options.codexPathOverride = config.executablePath;
        }
        else if (parsedCommand.executablePath) {
            options.codexPathOverride = parsedCommand.executablePath;
        }
        const commandOverrides = parseCodexCommandOverrides(parsedCommand.args);
        if (Object.keys(commandOverrides.config).length > 0) {
            options.config = commandOverrides.config;
        }
        return options;
    }
    warnUnsupportedMcpConfig(config) {
        const hasExtraMcpServers = Boolean(config.extraMcpServers && Object.keys(config.extraMcpServers).length > 0);
        if (config.mcpConfigPath || hasExtraMcpServers) {
            console.warn('[CodexSdkAdapter] Codex SDK ThreadOptions do not expose dynamic MCP server injection; ' +
                'mcpConfigPath/extraMcpServers are ignored for Codex SDK sessions.');
        }
    }
    buildThreadOptions(config) {
        const parsedCommand = parseCodexCommand(config.command);
        const commandOverrides = parseCodexCommandOverrides(parsedCommand.args);
        const options = {
            workingDirectory: config.workingDirectory,
            skipGitRepoCheck: commandOverrides.skipGitRepoCheck ?? false,
            approvalPolicy: commandOverrides.approvalPolicy ?? (config.autoAccept ? 'never' : 'on-request'),
        };
        if (config.model || commandOverrides.model) {
            options.model = config.model ?? commandOverrides.model;
        }
        if (config.additionalDirectories || commandOverrides.additionalDirectories.length > 0) {
            options.additionalDirectories = [
                ...(config.additionalDirectories ?? []),
                ...commandOverrides.additionalDirectories,
            ];
        }
        if (commandOverrides.sandboxMode) {
            options.sandboxMode = commandOverrides.sandboxMode;
        }
        if (commandOverrides.modelReasoningEffort) {
            options.modelReasoningEffort = commandOverrides.modelReasoningEffort;
        }
        if (commandOverrides.networkAccessEnabled !== undefined) {
            options.networkAccessEnabled = commandOverrides.networkAccessEnabled;
        }
        if (commandOverrides.webSearchMode) {
            options.webSearchMode = commandOverrides.webSearchMode;
        }
        if (commandOverrides.webSearchEnabled !== undefined) {
            options.webSearchEnabled = commandOverrides.webSearchEnabled;
        }
        return options;
    }
    async runTurn(sessionId, session, message, abortController) {
        try {
            const streamed = await session.thread.runStreamed(message, { signal: abortController.signal });
            for await (const event of streamed.events) {
                this.handleThreadEvent(sessionId, session, event);
            }
        }
        catch (error) {
            if (abortController.signal.aborted) {
                if (!this.sessions.has(sessionId) || session.terminating || session.adapterSession.status === 'terminated') {
                    return;
                }
                session.adapterSession.status = 'waiting_input';
                this.emit('status-change', sessionId, 'waiting_input');
                return;
            }
            const text = error instanceof Error ? error.message : String(error);
            session.adapterSession.status = 'error';
            this.emit('status-change', sessionId, 'error');
            this.emitEvent({
                type: 'error',
                sessionId,
                timestamp: new Date().toISOString(),
                data: { text },
            });
        }
    }
    handleThreadEvent(sessionId, session, event) {
        const timestamp = new Date().toISOString();
        switch (event.type) {
            case 'thread.started':
                session.adapterSession.providerSessionId = event.thread_id;
                break;
            case 'turn.started':
                session.adapterSession.status = 'thinking';
                this.emit('status-change', sessionId, 'thinking');
                break;
            case 'item.started':
            case 'item.updated':
            case 'item.completed':
                this.handleThreadItem(sessionId, session, event.item, event.type, timestamp);
                break;
            case 'turn.completed':
                this.completeTurn(sessionId, session, event.usage, timestamp);
                break;
            case 'turn.failed':
                session.adapterSession.status = 'error';
                this.emit('status-change', sessionId, 'error');
                this.emitEvent({ type: 'error', sessionId, timestamp, data: { text: event.error.message } });
                break;
            case 'error':
                session.adapterSession.status = 'error';
                this.emit('status-change', sessionId, 'error');
                this.emitEvent({ type: 'error', sessionId, timestamp, data: { text: event.message } });
                break;
        }
    }
    handleThreadItem(sessionId, session, item, eventType, timestamp) {
        switch (item.type) {
            case 'agent_message':
                this.emitTextDelta(sessionId, session, item.id, item.text, timestamp);
                break;
            case 'reasoning':
                this.emitReasoningDelta(sessionId, session, item.id, item.text, timestamp);
                break;
            case 'command_execution':
                this.handleToolLikeItem(sessionId, session, {
                    id: item.id,
                    toolName: 'bash',
                    toolInput: { command: item.command },
                    result: item.aggregated_output,
                    isError: item.status === 'failed',
                    isRunning: item.status === 'in_progress',
                    eventType,
                    timestamp,
                });
                break;
            case 'mcp_tool_call': {
                const metadata = { mcpServer: item.server };
                this.handleToolLikeItem(sessionId, session, {
                    id: item.id,
                    toolName: item.tool,
                    toolInput: toRecord(item.arguments),
                    result: item.error?.message ?? serializeValue(item.result),
                    isError: item.status === 'failed',
                    isRunning: item.status === 'in_progress',
                    eventType,
                    timestamp,
                    metadata,
                });
                break;
            }
            case 'file_change':
                this.handleToolLikeItem(sessionId, session, {
                    id: item.id,
                    toolName: 'apply_patch',
                    toolInput: { changes: item.changes },
                    result: item.status,
                    isError: item.status === 'failed',
                    isRunning: eventType !== 'item.completed',
                    eventType,
                    timestamp,
                });
                break;
            case 'web_search':
                this.handleToolLikeItem(sessionId, session, {
                    id: item.id,
                    toolName: 'web_search',
                    toolInput: { query: item.query },
                    result: item.query,
                    isError: false,
                    isRunning: eventType !== 'item.completed',
                    eventType,
                    timestamp,
                });
                break;
            case 'todo_list':
                this.emitEvent({
                    type: 'thinking',
                    sessionId,
                    timestamp,
                    data: { text: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\n') },
                });
                break;
            case 'error':
                this.emitEvent({ type: 'error', sessionId, timestamp, data: { text: item.message } });
                break;
        }
    }
    emitTextDelta(sessionId, session, itemId, text, timestamp) {
        const emittedLength = session.emittedTextLengths.get(itemId) ?? 0;
        const delta = text.slice(emittedLength);
        session.emittedTextLengths.set(itemId, text.length);
        if (!delta)
            return;
        session.currentAssistantText += delta;
        this.emitEvent({ type: 'text_delta', sessionId, timestamp, data: { text: delta } });
    }
    emitReasoningDelta(sessionId, session, itemId, text, timestamp) {
        const emittedLength = session.emittedReasoningLengths.get(itemId) ?? 0;
        const delta = text.slice(emittedLength);
        session.emittedReasoningLengths.set(itemId, text.length);
        if (!delta)
            return;
        this.emitEvent({ type: 'thinking', sessionId, timestamp, data: { text: delta } });
    }
    handleToolLikeItem(sessionId, session, item) {
        if (!session.emittedToolStarts.has(item.id)) {
            session.emittedToolStarts.add(item.id);
            const actionInfo = { ...getToolActionInfo(item.toolName, item.toolInput, item.metadata), actionId: item.id };
            this.emitEvent({
                type: 'tool_use_start',
                sessionId,
                timestamp: item.timestamp,
                data: { toolName: item.toolName, toolInput: item.toolInput, toolUseId: item.id, ...actionInfo },
            });
            this.emit('status-change', sessionId, 'tool_using', actionInfo);
            session.adapterSession.messages.push({
                id: uuidv4(),
                sessionId,
                role: 'tool_use',
                content: `${item.toolName}: ${JSON.stringify(item.toolInput).slice(0, 100)}`,
                timestamp: item.timestamp,
                toolName: item.toolName,
                toolInput: item.toolInput,
                toolUseId: item.id,
            });
        }
        if (item.isRunning || item.eventType !== 'item.completed') {
            return;
        }
        this.emitEvent({
            type: 'tool_use_end',
            sessionId,
            timestamp: item.timestamp,
            data: {
                toolName: item.toolName,
                toolResult: item.result,
                isError: item.isError,
                toolUseId: item.id,
            },
        });
        session.emittedToolStarts.delete(item.id);
    }
    completeTurn(sessionId, session, usage, timestamp) {
        const mappedUsage = mapUsage(usage);
        session.adapterSession.totalUsage = mappedUsage;
        if (session.currentAssistantText) {
            session.adapterSession.messages.push({
                id: uuidv4(),
                sessionId,
                role: 'assistant',
                content: session.currentAssistantText,
                timestamp,
            });
            session.currentAssistantText = '';
        }
        this.emitEvent({ type: 'turn_complete', sessionId, timestamp, data: { usage: mappedUsage } });
        session.adapterSession.status = 'waiting_input';
        this.emit('status-change', sessionId, 'waiting_input');
        session.lastActivityAt = Date.now();
        this.startIdleDetection(sessionId);
    }
    emitEvent(event) {
        this.emit('event', event);
    }
    startIdleDetection(sessionId) {
        this.stopIdleDetection(sessionId);
        const session = this.sessions.get(sessionId);
        const idleInputCommand = session?.idleCommands?.idleInputCommand;
        if (!session || !idleInputCommand)
            return;
        if (this.isMessagePollingIdleCommand(idleInputCommand)) {
            if (!session.idlePollLoop) {
                session.idlePollLoop = new IdlePollLoop({
                    getStatus: () => this.sessions.get(sessionId)?.adapterSession.status,
                    getCommand: () => this.sessions.get(sessionId)?.idleCommands?.idleInputCommand,
                    isPollingCommand: (command) => this.isMessagePollingIdleCommand(command),
                    dispatchPollingPrompt: (command) => this.sendIdlePrompt(sessionId, command, { keepWaitingStatus: true }),
                    dispatchRegularIdleCommand: (command) => this.sendIdlePrompt(sessionId, command),
                    regularIdleDelayMs: CODEX_IDLE_THRESHOLD_MS,
                });
            }
            session.idlePollLoop.start();
            return;
        }
        if (!session.idlePollLoop) {
            session.idlePollLoop = new IdlePollLoop({
                getStatus: () => this.sessions.get(sessionId)?.adapterSession.status,
                getCommand: () => this.sessions.get(sessionId)?.idleCommands?.idleInputCommand,
                isPollingCommand: (command) => this.isMessagePollingIdleCommand(command),
                dispatchPollingPrompt: (command) => this.sendIdlePrompt(sessionId, command, { keepWaitingStatus: true }),
                dispatchRegularIdleCommand: (command) => this.sendIdlePrompt(sessionId, command),
                regularIdleDelayMs: CODEX_IDLE_THRESHOLD_MS,
            });
        }
        session.lastActivityAt = Date.now();
        const timer = setInterval(() => {
            this.checkIdleState(sessionId);
        }, 500);
        session.idleTimer = timer;
    }
    stopIdleDetection(sessionId) {
        const session = this.sessions.get(sessionId);
        session?.idlePollLoop?.stop();
        if (session?.idleTimer) {
            clearInterval(session.idleTimer);
            session.idleTimer = undefined;
        }
    }
    checkIdleState(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session || session.adapterSession.status !== 'waiting_input')
            return;
        if (!session.idleCommands?.idleInputCommand)
            return;
        const lastActivityAt = session.lastActivityAt ?? 0;
        const idleTime = Date.now() - lastActivityAt;
        if (idleTime < CODEX_IDLE_THRESHOLD_MS)
            return;
        session.lastActivityAt = Date.now();
        void this.sendIdlePrompt(sessionId, session.idleCommands.idleInputCommand);
        this.stopIdleDetection(sessionId);
    }
    async sendIdlePrompt(sessionId, command, options) {
        const session = this.sessions.get(sessionId);
        if (!session)
            return;
        if (session.runningTurn)
            return;
        const prompt = this.toIdlePrompt(command);
        if (!prompt)
            return;
        if (!options?.keepWaitingStatus) {
            session.adapterSession.status = 'thinking';
            this.emit('status-change', sessionId, 'thinking');
        }
        const abortController = new AbortController();
        session.currentAbortController = abortController;
        session.runningTurn = this.runTurn(sessionId, session, prompt, abortController);
        try {
            await session.runningTurn;
        }
        finally {
            session.runningTurn = undefined;
            session.currentAbortController = undefined;
        }
    }
    toIdlePrompt(command) {
        const trimmed = command.trim();
        if (this.isMessagePollingIdleCommand(trimmed)) {
            return '系统提醒：当前工作已完成或运行时已进入空闲状态。请立即调用 poll_message 工具阻塞等待新消息和流程线任务提醒；收到流程任务提醒后调用 my_task 查看详情，按需调用 start_task 开始处理；如果 start_task 失败且提示任务已被其他实例领取，请不要停下，继续调用 poll_message 等待新消息；完成任务后调用 submit_task_result 提交结果，然后继续 poll_message。';
        }
        return trimmed;
    }
    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('取消息');
    }
}
