'use strict';

const CODEX_APP_ONCE_NOTICE_PATTERNS = [
  /^Under-development features enabled:/i,
  /^Heads up: Long threads and multiple compactions/i,
];

function readPositiveIntEnv(name, fallback, options = {}) {
  const raw = Number.parseInt(String(process.env[name] || ''), 10);
  const min = Number.isFinite(options.min) ? options.min : 1;
  const max = Number.isFinite(options.max) ? options.max : Number.MAX_SAFE_INTEGER;
  if (!Number.isFinite(raw) || raw <= 0) return fallback;
  return Math.max(min, Math.min(max, raw));
}

const RUNTIME_FULL_TEXT_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_FULL_TEXT_MAX_CHARS', 256 * 1024, { min: 4096 });
const RUNTIME_AGENT_ITEM_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_AGENT_ITEM_MAX_CHARS', 128 * 1024, { min: 4096 });
const RUNTIME_TOOL_DELTA_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_TOOL_DELTA_MAX_CHARS', 64 * 1024, { min: 1024 });
const RUNTIME_TOOL_RESULT_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_TOOL_RESULT_MAX_CHARS', 32 * 1024, { min: 1024 });
const RUNTIME_TOOL_INPUT_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_TOOL_INPUT_MAX_CHARS', 16 * 1024, { min: 1024 });
const RUNTIME_STREAM_DELTA_MAX_CHARS = readPositiveIntEnv('CC_WEB_CODEX_APP_STREAM_DELTA_MAX_CHARS', 16 * 1024, { min: 1024 });
const RUNTIME_MAX_TOOL_CALLS = readPositiveIntEnv('CC_WEB_CODEX_APP_RUNTIME_MAX_TOOL_CALLS', 120, { min: 1, max: 1000 });
const RUNTIME_TRUNCATED_HEAD = '[cc-web: 前文过长，已保留尾部以保护服务稳定性]\n';
const RUNTIME_TRUNCATED_TAIL = '\n[cc-web: 内容过长，已截断以保护服务稳定性]';
const CODEX_APP_PLAN_ITEM_TYPES = new Set(['plan', 'plan_list', 'planlist', 'todo', 'todo_list', 'todolist', 'task_list']);
const CODEX_APP_PLAN_TOOL_NAMES = new Set(['update_plan', 'plan', 'plan_list', 'todo_list', 'updateplan', 'todolist']);

function createCodexAppRuntime(deps = {}) {
  const {
    wsSend,
    loadSession,
    saveSession,
    truncateObj,
  } = deps;

  function limitPreviewValue(value, options = {}, depth = 0, seen = new WeakSet()) {
    const maxString = options.maxString || RUNTIME_TOOL_RESULT_MAX_CHARS;
    const maxDepth = options.maxDepth || 4;
    const maxArray = options.maxArray || 50;
    const maxKeys = options.maxKeys || 60;

    if (value === null || value === undefined) return value;
    if (typeof value === 'string') return truncateEnd(value, maxString);
    if (typeof value === 'number' || typeof value === 'boolean') return value;
    if (typeof value === 'bigint') return String(value);
    if (typeof value === 'function' || typeof value === 'symbol') return undefined;
    if (Buffer.isBuffer(value)) return `[Buffer ${value.length} bytes]`;
    if (depth >= maxDepth) return '[Object truncated]';
    if (typeof value !== 'object') return String(value);
    if (seen.has(value)) return '[Circular]';
    seen.add(value);

    if (Array.isArray(value)) {
      const output = [];
      const limit = Math.min(value.length, maxArray);
      for (let index = 0; index < limit; index += 1) {
        output.push(limitPreviewValue(value[index], options, depth + 1, seen));
      }
      if (value.length > limit) output.push({ __truncated: `omitted ${value.length - limit} items` });
      seen.delete(value);
      return output;
    }

    const output = {};
    const keys = Object.keys(value);
    const limit = Math.min(keys.length, maxKeys);
    for (let index = 0; index < limit; index += 1) {
      const key = keys[index];
      const next = limitPreviewValue(value[key], options, depth + 1, seen);
      if (next !== undefined) output[key] = next;
    }
    if (keys.length > limit) output.__truncated = `omitted ${keys.length - limit} fields`;
    seen.delete(value);
    return output;
  }

  function safeStringifyPreview(value, maxLen = RUNTIME_TOOL_RESULT_MAX_CHARS, options = {}) {
    if (typeof value === 'string') return truncateEnd(value, maxLen);
    try {
      const limited = limitPreviewValue(value, {
        maxString: Math.min(maxLen, options.maxString || maxLen),
        maxDepth: options.maxDepth || 4,
        maxArray: options.maxArray || 50,
        maxKeys: options.maxKeys || 60,
      });
      return truncateEnd(JSON.stringify(limited, null, 2), maxLen);
    } catch {
      return truncateEnd(String(value), maxLen);
    }
  }

  function truncateEnd(value, maxLen) {
    const text = String(value || '');
    if (!Number.isFinite(maxLen) || maxLen <= 0 || text.length <= maxLen) return text;
    const keep = Math.max(0, maxLen - RUNTIME_TRUNCATED_TAIL.length);
    return `${text.slice(0, keep)}${RUNTIME_TRUNCATED_TAIL}`;
  }

  function keepTail(value, maxLen) {
    const text = String(value || '');
    if (!Number.isFinite(maxLen) || maxLen <= 0 || text.length <= maxLen) return text;
    const keep = Math.max(0, maxLen - RUNTIME_TRUNCATED_HEAD.length);
    return `${RUNTIME_TRUNCATED_HEAD}${text.slice(-keep)}`;
  }

  function appendCappedText(current, addition, maxLen) {
    return keepTail(`${String(current || '')}${String(addition || '')}`, maxLen);
  }

  function capStreamDelta(text) {
    return truncateEnd(text, RUNTIME_STREAM_DELTA_MAX_CHARS);
  }

  function truncate(value, maxLen) {
    if (typeof value === 'string') return truncateEnd(value, maxLen);
    return safeStringifyPreview(value, maxLen, { maxString: maxLen });
  }

  const shownOnceNoticeKeys = new Set();

  function normalizeNoticeMessage(message) {
    return String(message || '').trim().replace(/\s+/g, ' ');
  }

  function shouldShowRuntimeNotice(method, message) {
    const normalized = normalizeNoticeMessage(message);
    const isOnceNotice = CODEX_APP_ONCE_NOTICE_PATTERNS.some((pattern) => pattern.test(normalized));
    if (!isOnceNotice) return true;

    const key = `${method}:${normalized}`;
    if (shownOnceNoticeKeys.has(key)) return false;
    shownOnceNoticeKeys.add(key);
    return true;
  }

  function normalizeIdentifier(value) {
    return String(value || '')
      .trim()
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '_')
      .replace(/^_+|_+$/g, '');
  }

  function isPlanToolName(value) {
    const name = normalizeIdentifier(value);
    return CODEX_APP_PLAN_TOOL_NAMES.has(name) || name.endsWith('_update_plan');
  }

  function isPlanLikeItem(item) {
    if (!item || typeof item !== 'object') return false;
    if (CODEX_APP_PLAN_ITEM_TYPES.has(normalizeIdentifier(item.type))) return true;
    return isPlanToolName(item.tool || item.name || item.functionName || item.function?.name);
  }

  function parseMaybeJsonValue(value) {
    if (typeof value !== 'string') return value;
    const trimmed = value.trim();
    if (!trimmed || !/^[{[]/.test(trimmed)) return value;
    try {
      return JSON.parse(trimmed);
    } catch {
      return value;
    }
  }

  function extractPlanEntries(value, depth = 0) {
    if (value === null || value === undefined || depth > 3) return null;
    const source = parseMaybeJsonValue(value);
    if (Array.isArray(source)) return source;
    if (!source || typeof source !== 'object') return null;
    const keys = ['plan', 'items', 'todos', 'tasks', 'steps'];
    for (const key of keys) {
      if (Array.isArray(source[key])) return source[key];
    }
    const nestedKeys = ['arguments', 'input', 'params', 'payload', 'structuredContent', 'result'];
    for (const key of nestedKeys) {
      const nested = extractPlanEntries(source[key], depth + 1);
      if (nested) return nested;
    }
    if (Array.isArray(source.contentItems)) {
      for (const part of source.contentItems) {
        const text = typeof part?.text === 'string' ? part.text : '';
        const nested = extractPlanEntries(text, depth + 1);
        if (nested) return nested;
      }
    }
    return null;
  }

  function planEntryCompleted(entry) {
    if (!entry || typeof entry !== 'object') return false;
    if (entry.completed === true || entry.done === true) return true;
    const status = normalizeIdentifier(entry.status || entry.state);
    return ['completed', 'complete', 'done', 'success', 'succeeded'].includes(status);
  }

  function planEntryText(entry) {
    if (typeof entry === 'string') return entry;
    if (!entry || typeof entry !== 'object') return '';
    return entry.step || entry.text || entry.title || entry.name || entry.description || entry.task || entry.item || entry.content || '';
  }

  function normalizeTodoListFromPlanItem(item) {
    if (!isPlanLikeItem(item)) return null;
    const candidates = [
      item.arguments,
      item.input,
      item.params,
      item.payload,
      item.structuredContent,
      item.result?.structuredContent,
      item.result,
      item,
    ];
    let entries = null;
    for (const candidate of candidates) {
      entries = extractPlanEntries(candidate);
      if (entries) break;
    }
    if (!Array.isArray(entries)) return null;
    const items = entries
      .map((entry) => {
        const text = truncateEnd(planEntryText(entry), RUNTIME_TOOL_INPUT_MAX_CHARS);
        if (!text) return null;
        return {
          text,
          completed: planEntryCompleted(entry),
        };
      })
      .filter(Boolean);
    return {
      id: item.id || item.itemId || item.planId || 'codex-app-plan',
      type: 'todo_list',
      items,
    };
  }

  function isTodoListPlanDone(item, todoList) {
    const status = normalizeIdentifier(item?.status || item?.state);
    if (['completed', 'complete', 'done', 'success', 'succeeded', 'failed', 'error', 'cancelled', 'canceled'].includes(status)) {
      return true;
    }
    return Array.isArray(todoList?.items) && todoList.items.length > 0 && todoList.items.every((planItem) => planItem.completed);
  }

  function todoListPlanStatus(item, todoList) {
    if (isTodoListPlanDone(item, todoList)) return 'completed';
    return item?.status || 'inProgress';
  }

  function planUpdateItemFromParams(params = {}) {
    const item = params.item && typeof params.item === 'object' ? { ...params.item } : {};
    return {
      ...params,
      ...item,
      id: item.id || params.itemId || params.id || params.planId || 'codex-app-plan',
      type: item.type || params.type || 'planList',
      status: item.status || params.status || 'inProgress',
      plan: item.plan || params.plan,
      items: item.items || params.items,
      todos: item.todos || params.todos,
      tasks: item.tasks || params.tasks,
    };
  }

  function codexAppErrorMessage(value) {
    if (!value) return '';
    if (typeof value === 'string') return value;
    if (typeof value !== 'object') return String(value);

    const parts = [];
    const directMessage = value.message || value.title || value.detail || value.reason;
    if (directMessage) parts.push(String(directMessage));

    const error = value.error && typeof value.error === 'object' ? value.error : null;
    if (error) {
      if (error.message) parts.push(String(error.message));
      if (error.code) parts.push(String(error.code));
      if (error.type) parts.push(String(error.type));
    }

    if (value.code) parts.push(String(value.code));
    if (value.type) parts.push(String(value.type));
    if (parts.length > 0) return [...new Set(parts)].join(' ');
    return safeStringifyPreview(value, 2000, { maxDepth: 3, maxArray: 10, maxKeys: 20 });
  }

  function sendRuntime(entry, sessionId, payload) {
    wsSend(entry.ws, { ...payload, sessionId });
  }

  function formatAgentMessageDividerTime(date = new Date()) {
    const pad = (value) => String(value).padStart(2, '0');
    return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
  }

  function createAgentMessageDivider() {
    const time = formatAgentMessageDividerTime();
    return `<div class="agent-message-divider" data-divider-time="${time}"><span>${time}</span></div>`;
  }

  function hasAgentMessageBoundaryAtEnd(text) {
    return /\n\s*(?:---|\*\*\*|___)\s*$/.test(text)
      || /(?:^|\n)\s*<div\s+class=["']agent-message-divider["'][\s\S]*?<\/div>\s*$/.test(text);
  }

  function hasAgentMessageBoundaryAtStart(text) {
    return /^\s*(?:---|\*\*\*|___)\s*\n/.test(text)
      || /^\s*<div\s+class=["']agent-message-divider["'][\s\S]*?<\/div>\s*/.test(text);
  }

  function itemKind(item) {
    if (normalizeTodoListFromPlanItem(item)) return 'todo_list';
    switch (item?.type) {
      case 'commandExecution':
        return 'command_execution';
      case 'mcpToolCall':
        return 'mcp_tool_call';
      case 'fileChange':
        return 'file_change';
      case 'reasoning':
        return 'reasoning';
      case 'dynamicToolCall':
        return 'dynamic_tool_call';
      case 'collabAgentToolCall':
        return 'collab_agent_tool_call';
      case 'webSearch':
        return 'web_search';
      case 'imageView':
        return 'image_view';
      case 'imageGeneration':
        return 'image_generation';
      default:
        return item?.type || 'codex_app_item';
    }
  }

  function itemName(item) {
    if (normalizeTodoListFromPlanItem(item)) return 'PlanList';
    switch (item?.type) {
      case 'commandExecution':
        return 'CommandExecution';
      case 'mcpToolCall':
        return 'McpToolCall';
      case 'fileChange':
        return 'FileChange';
      case 'reasoning':
        return 'Reasoning';
      case 'dynamicToolCall':
        return item.tool || 'DynamicToolCall';
      case 'collabAgentToolCall':
        return item.tool || 'CollabAgentToolCall';
      case 'webSearch':
        return 'WebSearch';
      case 'imageGeneration':
        return 'ImageGeneration';
      default:
        return item?.type || 'CodexAppItem';
    }
  }

  function itemInput(item) {
    if (!item) return null;
    const todoList = normalizeTodoListFromPlanItem(item);
    if (todoList) return todoList;
    switch (item.type) {
      case 'commandExecution':
        return { command: truncateEnd(item.command || '', RUNTIME_TOOL_INPUT_MAX_CHARS) };
      case 'mcpToolCall':
        return {
          server: truncateEnd(item.server || '', 256),
          tool: truncateEnd(item.tool || '', 256),
          arguments: limitPreviewValue(item.arguments ?? null, {
            maxString: RUNTIME_TOOL_INPUT_MAX_CHARS,
            maxDepth: 5,
            maxArray: 80,
            maxKeys: 80,
          }),
        };
      case 'fileChange':
        return { changes: limitPreviewValue(item.changes || [], { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 5 }) };
      case 'reasoning':
        return {
          content: limitPreviewValue(item.content || [], { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 4 }),
          summary: limitPreviewValue(item.summary || [], { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 4 }),
        };
      case 'dynamicToolCall':
        return {
          tool: truncateEnd(item.tool || '', 256),
          namespace: truncateEnd(item.namespace || '', 256) || null,
          arguments: limitPreviewValue(item.arguments ?? null, { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 5 }),
        };
      case 'collabAgentToolCall':
        return {
          tool: truncateEnd(item.tool || '', 256),
          prompt: truncateEnd(item.prompt || '', RUNTIME_TOOL_INPUT_MAX_CHARS) || null,
          receiverThreadIds: limitPreviewValue(item.receiverThreadIds || [], { maxString: 512, maxDepth: 3 }),
          agentsStates: limitPreviewValue(item.agentsStates || {}, { maxString: RUNTIME_TOOL_INPUT_MAX_CHARS, maxDepth: 5 }),
        };
      case 'imageGeneration':
        return {
          prompt: truncateEnd(item.prompt || item.query || '', RUNTIME_TOOL_INPUT_MAX_CHARS),
          size: item.size || null,
          quality: item.quality || null,
        };
      default:
        return limitPreviewValue(item, {
          maxString: Math.min(500, RUNTIME_TOOL_INPUT_MAX_CHARS),
          maxDepth: 4,
          maxArray: 30,
          maxKeys: 40,
        });
    }
  }

  function reasoningTextFromItem(item) {
    const parts = [...(item?.summary || []), ...(item?.content || [])];
    return parts.map((part) => {
      if (typeof part === 'string') return part;
      if (typeof part?.text === 'string') return part.text;
      return '';
    }).filter(Boolean).join('\n\n');
  }

  function hasReasoningContent(item) {
    return Boolean(reasoningTextFromItem(item).trim());
  }

  function itemMeta(item) {
    if (!item) return null;
    const todoList = normalizeTodoListFromPlanItem(item);
    if (todoList) {
      return {
        kind: 'todo_list',
        title: 'Plan List',
        subtitle: item.explanation || item.title || item.tool || '',
        status: todoListPlanStatus(item, todoList),
      };
    }
    switch (item.type) {
      case 'commandExecution':
        return {
          kind: 'command_execution',
          title: 'Shell Command',
          subtitle: item.command || '',
          exitCode: typeof item.exitCode === 'number' ? item.exitCode : null,
          status: item.status || null,
        };
      case 'mcpToolCall':
        return {
          kind: 'mcp_tool_call',
          title: 'MCP Tool',
          subtitle: [item.server, item.tool].filter(Boolean).join('.'),
          status: item.status || null,
        };
      case 'fileChange':
        return {
          kind: 'file_change',
          title: 'File Change',
          subtitle: (item.changes || []).map((change) => change.path).filter(Boolean).join(', '),
          status: item.status || null,
        };
      case 'reasoning':
        return {
          kind: 'reasoning',
          title: 'Reasoning',
          subtitle: reasoningTextFromItem(item).replace(/\s+/g, ' ').slice(0, 120),
          status: item.status || null,
        };
      default:
        return {
          kind: itemKind(item),
          title: itemName(item),
          subtitle: item.tool || item.query || '',
          status: item.status || null,
        };
    }
  }

  function stringifyMcpResult(result) {
    if (!result) return '';
    if (Array.isArray(result.content)) {
      const text = result.content.map((part) => {
        if (typeof part?.text === 'string') return truncateEnd(part.text, RUNTIME_TOOL_RESULT_MAX_CHARS);
        return safeStringifyPreview(part, RUNTIME_TOOL_RESULT_MAX_CHARS);
      }).filter(Boolean).join('\n');
      if (text) return truncateEnd(text, RUNTIME_TOOL_RESULT_MAX_CHARS);
    }
    return safeStringifyPreview(result, RUNTIME_TOOL_RESULT_MAX_CHARS);
  }

  function itemResult(item) {
    if (!item) return '';
    const todoList = normalizeTodoListFromPlanItem(item);
    if (todoList) return JSON.stringify(todoList, null, 2);
    switch (item.type) {
      case 'commandExecution':
        return truncateEnd(item.aggregatedOutput || '', RUNTIME_TOOL_RESULT_MAX_CHARS);
      case 'mcpToolCall':
        return item.error?.message || stringifyMcpResult(item.result);
      case 'fileChange':
        return safeStringifyPreview(item.changes || [], RUNTIME_TOOL_RESULT_MAX_CHARS);
      case 'reasoning':
        return truncateEnd(reasoningTextFromItem(item), RUNTIME_TOOL_RESULT_MAX_CHARS);
      case 'dynamicToolCall':
        return safeStringifyPreview({
          success: item.success ?? null,
          contentItems: item.contentItems || null,
        }, RUNTIME_TOOL_RESULT_MAX_CHARS);
      case 'collabAgentToolCall':
        return safeStringifyPreview({
          status: item.status || null,
          receiverThreadIds: item.receiverThreadIds || [],
          agentsStates: item.agentsStates || {},
        }, RUNTIME_TOOL_RESULT_MAX_CHARS);
      case 'imageGeneration':
        return safeStringifyPreview({
          status: item.status || null,
          images: Array.isArray(item.images) ? item.images.map((image) => ({
            path: image.path || image.filePath || null,
            mime: image.mime || image.mimeType || null,
            size: image.size || null,
          })) : null,
          outputPath: item.outputPath || item.path || null,
        }, RUNTIME_TOOL_RESULT_MAX_CHARS);
      default:
        if (typeof item.text === 'string') return truncateEnd(item.text, RUNTIME_TOOL_RESULT_MAX_CHARS);
        return safeStringifyPreview(item, Math.min(1200, RUNTIME_TOOL_RESULT_MAX_CHARS));
    }
  }

  function ensureToolCall(entry, item, sessionId) {
    if (!item?.id) return null;
    const kind = itemKind(item);
    let toolCall = entry.toolCalls.find((tool) => tool.id === item.id);
    if (toolCall) {
      toolCall.name = itemName(item);
      toolCall.kind = kind;
      toolCall.meta = itemMeta(item) || toolCall.meta || null;
      if (toolCall.input == null || kind === 'todo_list') toolCall.input = itemInput(item);
      return toolCall;
    }

    if (entry.toolCalls.length >= RUNTIME_MAX_TOOL_CALLS) {
      let overflowTool = entry.toolCalls.find((tool) => tool.id === 'ccweb-toolcalls-overflow');
      if (!overflowTool) {
        overflowTool = {
          name: 'cc-web',
          id: 'ccweb-toolcalls-overflow',
          kind: 'system',
          meta: { kind: 'system', title: 'Tool Calls', subtitle: 'too many tool calls', status: 'inProgress' },
          input: null,
          done: false,
          result: '工具调用数量过多，后续工具调用已折叠显示。',
        };
        entry.toolCalls.push(overflowTool);
        sendRuntime(entry, sessionId, {
          type: 'tool_start',
          name: overflowTool.name,
          toolUseId: overflowTool.id,
          input: overflowTool.input,
          kind: overflowTool.kind,
          meta: overflowTool.meta,
        });
      }
      return overflowTool;
    }

    toolCall = {
      name: itemName(item),
      id: item.id,
      kind,
      meta: itemMeta(item),
      input: itemInput(item),
      done: false,
    };
    entry.toolCalls.push(toolCall);
    sendRuntime(entry, sessionId, {
      type: 'tool_start',
      name: toolCall.name,
      toolUseId: toolCall.id,
      input: toolCall.input,
      kind: toolCall.kind,
      meta: toolCall.meta,
    });
    return toolCall;
  }

  function appendAgentText(entry, itemId, text) {
    const nextText = String(text || '');
    if (!nextText) return '';
    if (!entry.agentMessageItems) entry.agentMessageItems = new Map();
    const currentItemText = entry.agentMessageItems.get(itemId) || '';
    const separator = agentMessageSeparator(entry, itemId, nextText);
    const appended = separator + nextText;
    entry.agentMessageItems.set(itemId, appendCappedText(currentItemText, nextText, RUNTIME_AGENT_ITEM_MAX_CHARS));
    entry.fullText = appendCappedText(entry.fullText || '', appended, RUNTIME_FULL_TEXT_MAX_CHARS);
    return capStreamDelta(appended);
  }

  function appendAgentCompletedText(entry, item) {
    const text = String(item?.text || '');
    if (!text) return '';
    if (!entry.agentMessageItems) entry.agentMessageItems = new Map();
    const currentItemText = entry.agentMessageItems.get(item.id) || '';
    if (currentItemText && text.startsWith(currentItemText)) {
      const remainder = text.slice(currentItemText.length);
      entry.agentMessageItems.set(item.id, keepTail(text, RUNTIME_AGENT_ITEM_MAX_CHARS));
      entry.fullText = appendCappedText(entry.fullText || '', remainder, RUNTIME_FULL_TEXT_MAX_CHARS);
      return capStreamDelta(remainder);
    }
    if (currentItemText === text) return '';
    const separator = agentMessageSeparator(entry, item.id, text);
    const appended = separator + text;
    entry.agentMessageItems.set(item.id, keepTail(text, RUNTIME_AGENT_ITEM_MAX_CHARS));
    entry.fullText = appendCappedText(entry.fullText || '', appended, RUNTIME_FULL_TEXT_MAX_CHARS);
    return capStreamDelta(appended);
  }

  function agentMessageSeparator(entry, itemId, nextText) {
    if (entry.agentMessageItems?.get(itemId)) return '';
    const currentText = entry.fullText || '';
    if (!/\S/.test(currentText)) return '';
    const hasVisualBoundary = hasAgentMessageBoundaryAtEnd(currentText) || hasAgentMessageBoundaryAtStart(String(nextText || ''));
    return hasVisualBoundary ? '' : `\n\n${createAgentMessageDivider()}\n\n`;
  }

  function updateToolResult(entry, sessionId, itemId, result, done = false, patch = {}) {
    if (!itemId) return;
    let toolCall = entry.toolCalls.find((tool) => tool.id === itemId);
    if (!toolCall) {
      const targetItemId = entry.toolCalls.length >= RUNTIME_MAX_TOOL_CALLS ? 'ccweb-toolcalls-overflow' : itemId;
      toolCall = entry.toolCalls.find((tool) => tool.id === targetItemId);
      itemId = targetItemId;
    }
    if (!toolCall) {
      toolCall = {
        name: patch.name || 'CodexAppItem',
        id: itemId,
        kind: patch.kind || 'codex_app_item',
        meta: patch.meta || null,
        input: patch.input || null,
        done: false,
      };
      entry.toolCalls.push(toolCall);
      sendRuntime(entry, sessionId, {
        type: 'tool_start',
        name: toolCall.name,
        toolUseId: toolCall.id,
        input: toolCall.input,
        kind: toolCall.kind,
        meta: toolCall.meta,
      });
    }
    if (patch.name) toolCall.name = patch.name;
    if (patch.kind) toolCall.kind = patch.kind;
    if (patch.meta) toolCall.meta = patch.meta;
    if (patch.input !== undefined) {
      toolCall.input = limitPreviewValue(patch.input, {
        maxString: RUNTIME_TOOL_INPUT_MAX_CHARS,
        maxDepth: 5,
        maxArray: 80,
        maxKeys: 80,
      });
    }
    const safeResult = truncateEnd(result, RUNTIME_TOOL_RESULT_MAX_CHARS);
    toolCall.done = done;
    toolCall.result = safeResult;
    sendRuntime(entry, sessionId, {
      type: done ? 'tool_end' : 'tool_update',
      toolUseId: itemId,
      name: toolCall.name,
      input: toolCall.input,
      result: safeResult,
      kind: toolCall.kind,
      meta: toolCall.meta,
    });
  }

  function updateUsage(sessionId, entry, usage) {
    const total = usage?.total || usage || null;
    if (!total) return;
    const session = loadSession(sessionId);
    if (!session) return;
    session.totalUsage = {
      inputTokens: total.inputTokens || 0,
      cachedInputTokens: total.cachedInputTokens || 0,
      outputTokens: total.outputTokens || 0,
    };
    entry.lastUsage = session.totalUsage;
    saveSession(session);
    sendRuntime(entry, sessionId, { type: 'usage', totalUsage: session.totalUsage });
  }

  function processCodexAppNotification(entry, notification, sessionId) {
    if (!entry || !notification?.method) return { done: false };
    const method = notification.method;
    const params = notification.params || {};

    if (params.threadId && !entry.threadId) entry.threadId = params.threadId;
    if (params.turnId && !entry.turnId) entry.turnId = params.turnId;

    switch (method) {
      case 'turn/started':
        if (params.turn?.id) entry.turnId = params.turn.id;
        return { done: false };

      case 'item/started': {
        const item = params.item;
        if (!item || !item.id || item.type === 'agentMessage' || item.type === 'userMessage') return { done: false };
        if (item.type === 'reasoning' && !hasReasoningContent(item)) return { done: false };
        ensureToolCall(entry, item, sessionId);
        return { done: false };
      }

      case 'item/agentMessage/delta': {
        const chunk = appendAgentText(entry, params.itemId || 'agent-message', params.delta || '');
        if (chunk) sendRuntime(entry, sessionId, { type: 'text_delta', text: chunk });
        return { done: false };
      }

      case 'item/commandExecution/outputDelta': {
        const itemId = params.itemId;
        const current = entry.toolOutputDeltas?.get(itemId) || '';
        const next = appendCappedText(current, params.delta || '', RUNTIME_TOOL_DELTA_MAX_CHARS);
        if (!entry.toolOutputDeltas) entry.toolOutputDeltas = new Map();
        entry.toolOutputDeltas.set(itemId, next);
        updateToolResult(entry, sessionId, itemId, next, false, {
          name: 'CommandExecution',
          kind: 'command_execution',
        });
        return { done: false };
      }

      case 'item/fileChange/patchUpdated': {
        updateToolResult(entry, sessionId, params.itemId, JSON.stringify(params.changes || [], null, 2), false, {
          name: 'FileChange',
          kind: 'file_change',
          input: { changes: params.changes || [] },
          meta: {
            kind: 'file_change',
            title: 'File Change',
            subtitle: (params.changes || []).map((change) => change.path).filter(Boolean).join(', '),
            status: 'inProgress',
          },
        });
        return { done: false };
      }

      case 'item/mcpToolCall/progress': {
        updateToolResult(entry, sessionId, params.itemId, params.message || '', false, {
          name: 'McpToolCall',
          kind: 'mcp_tool_call',
        });
        return { done: false };
      }

      case 'plan/updated':
      case 'turn/plan/updated':
      case 'item/plan/updated':
      case 'item/todoList/updated': {
        const item = planUpdateItemFromParams(params);
        const todoList = normalizeTodoListFromPlanItem(item);
        if (!todoList) return { done: false };
        updateToolResult(entry, sessionId, todoList.id, JSON.stringify(todoList, null, 2), isTodoListPlanDone(item, todoList), {
          name: 'PlanList',
          kind: 'todo_list',
          input: todoList,
          meta: itemMeta(item),
        });
        return { done: false };
      }

      case 'item/reasoning/summaryTextDelta':
      case 'item/reasoning/textDelta': {
        const itemId = params.itemId;
        const delta = String(params.delta || '');
        if (!itemId || !delta) return { done: false };
        const current = entry.toolOutputDeltas?.get(itemId) || '';
        const next = appendCappedText(current, delta, RUNTIME_TOOL_DELTA_MAX_CHARS);
        if (!entry.toolOutputDeltas) entry.toolOutputDeltas = new Map();
        entry.toolOutputDeltas.set(itemId, next);
        updateToolResult(entry, sessionId, itemId, next, false, {
          name: 'Reasoning',
          kind: 'reasoning',
        });
        return { done: false };
      }

      case 'item/completed': {
        const item = params.item;
        if (!item || !item.id) return { done: false };
        if (item.type === 'agentMessage') {
          const chunk = appendAgentCompletedText(entry, item);
          if (chunk) sendRuntime(entry, sessionId, { type: 'text_delta', text: chunk });
          return { done: false };
        }
        if (item.type === 'userMessage') return { done: false };
        if (item.type === 'reasoning') {
          const result = truncateEnd(itemResult(item) || entry.toolOutputDeltas?.get(item.id) || '', RUNTIME_TOOL_RESULT_MAX_CHARS);
          if (!result.trim()) return { done: false };
          const toolCall = ensureToolCall(entry, item, sessionId);
          if (!toolCall) return { done: false };
          toolCall.done = true;
          toolCall.result = result;
          toolCall.meta = itemMeta(item) || toolCall.meta;
          sendRuntime(entry, sessionId, {
            type: 'tool_end',
            toolUseId: toolCall.id,
            result,
            kind: toolCall.kind,
            meta: toolCall.meta,
          });
          return { done: false };
        }
        const toolCall = ensureToolCall(entry, item, sessionId);
        if (!toolCall) return { done: false };
        const result = truncateEnd(itemResult(item), RUNTIME_TOOL_RESULT_MAX_CHARS);
        toolCall.done = true;
        toolCall.result = result;
        toolCall.meta = itemMeta(item) || toolCall.meta;
        sendRuntime(entry, sessionId, {
          type: 'tool_end',
          toolUseId: toolCall.id,
          result,
          kind: toolCall.kind,
          meta: toolCall.meta,
        });
        return { done: false };
      }

      case 'thread/tokenUsage/updated':
        updateUsage(sessionId, entry, params.tokenUsage);
        return { done: false };

      case 'turn/completed': {
        if (params.turn?.id) entry.turnId = params.turn.id;
        entry.turnStatus = params.turn?.status || 'completed';
        if (params.turn?.status === 'failed') {
          entry.lastError = codexAppErrorMessage(params.turn?.error) || 'Codex App 任务失败';
        }
        return { done: true };
      }

      case 'error':
      case 'warning':
      case 'guardianWarning':
      case 'configWarning':
      case 'deprecationNotice': {
        const message = method === 'error'
          ? codexAppErrorMessage(params)
          : (params.message || params.title || '');
        if (message) {
          if (method === 'error') entry.lastError = message;
          if (method === 'error' || shouldShowRuntimeNotice(method, message)) {
            sendRuntime(entry, sessionId, { type: 'system_message', message });
          }
        }
        return { done: method === 'error' };
      }

      default:
        return { done: false };
    }
  }

  return {
    processCodexAppNotification,
    updateUsage,
  };
}

module.exports = { createCodexAppRuntime };
