/**
 * IPC Handlers — 所有 IPC 通道处理逻辑
 *
 * 集成三层日志：
 * - TOOL 层：AuditService 记录工具调用到 audit_logs 表
 * - TRACE 层：SessionRecorder 写入 session_*.jsonl
 * - AGENT 层：electron-log 记录状态转换
 *
 * @see docs/MetonaAI-Desktop 架构与交互设计.html — IPC 架构 + 日志分层
 */

import { ipcMain, BrowserWindow, shell, app, dialog } from 'electron';
import { join } from 'path';
import { nanoid } from 'nanoid';
import type { SessionService } from '../services/session.service';
import type { ConfigService } from '../services/config.service';
import type { WorkspaceService } from '../services/workspace.service';
import type { ContextBuilder } from '../harness/prompts/context-builder';
import type { AgentLoopEngine } from '../harness/agent-loop';
import type { ToolRegistry } from '../harness/tools/registry';
import type { AuditService, AuditEventType } from '../services/audit.service';
import type { SessionRecorder } from '../services/session-recorder.service';
import type { MemoryManager, MemoryType } from '../harness/memory/manager';
import type { MCPManager } from '../services/mcp-manager.service';
import type { PromptInjectionDefender } from '../harness/security/prompt-injection-defense';
import type { OutputValidator } from '../harness/verification/output-validator';
import type { ConfirmationHook } from '../harness/hooks/confirmation-hook';
import type { MemoryConsolidator } from '../harness/memory/consolidator';
import type { TaskOrchestrator } from '../harness/orchestration/orchestrator';
import type { MetonaMessage, MetonaStreamEvent } from '../harness/types';
import { MetonaErrorCode, MetonaStreamEventType } from '../harness/types';
import type { MetonaError } from '../harness/types';
import { estimateMessagesTokens } from '../harness/utils/token-estimator';
import log from 'electron-log';

/**
 * 注册所有 IPC 处理器
 */
export function registerAllIPCHandlers(
  mainWindow: BrowserWindow,
  sessionService: SessionService,
  configService: ConfigService,
  workspaceService: WorkspaceService,
  contextBuilder: ContextBuilder,
  agentLoop: AgentLoopEngine,
  toolRegistry: ToolRegistry,
  auditService: AuditService,
  sessionRecorder: SessionRecorder,
  memoryManager: MemoryManager,
  mcpManager: MCPManager,
  reloadAdapter: () => boolean,
  promptInjectionDefender: PromptInjectionDefender,
  outputValidator: OutputValidator,
  confirmationHook: ConfirmationHook,
  memoryConsolidator: MemoryConsolidator,
  orchestrator: TaskOrchestrator,
): void {

  // ===== Agent 交互 =====

  ipcMain.handle('agent:sendMessage', async (_event, userMessage: MetonaMessage, sessionId: string) => {
    // M-33 修复: 参数校验，防止 undefined/非字符串导致下游异常
    // P1-5 修复: 校验失败时也发 ERROR+DONE 流事件，防止 isStreaming 永久卡死
    if (!sessionId || typeof sessionId !== 'string') {
      log.warn('[AGENT] sendMessage rejected: invalid sessionId');
      if (!mainWindow.isDestroyed()) {
        const errorEvent: MetonaStreamEvent = {
          type: MetonaStreamEventType.ERROR,
          requestId: '', sessionId: sessionId ?? '', iteration: 0, seq: 0, timestamp: Date.now(),
          error: { code: MetonaErrorCode.UNKNOWN, message: '无效的会话 ID', retryable: false },
        };
        mainWindow.webContents.send('agent:streamEvent', errorEvent);
        mainWindow.webContents.send('agent:streamEvent', { ...errorEvent, type: MetonaStreamEventType.DONE });
      }
      return { success: false, error: 'Invalid sessionId' };
    }
    if (!userMessage || typeof userMessage !== 'object' || typeof userMessage.content !== 'string') {
      log.warn('[AGENT] sendMessage rejected: invalid userMessage');
      if (!mainWindow.isDestroyed()) {
        const errorEvent: MetonaStreamEvent = {
          type: MetonaStreamEventType.ERROR,
          requestId: '', sessionId, iteration: 0, seq: 0, timestamp: Date.now(),
          error: { code: MetonaErrorCode.UNKNOWN, message: '无效的消息格式', retryable: false },
        };
        mainWindow.webContents.send('agent:streamEvent', errorEvent);
        mainWindow.webContents.send('agent:streamEvent', { ...errorEvent, type: MetonaStreamEventType.DONE });
      }
      return { success: false, error: 'Invalid message format' };
    }
    log.info('[AGENT] sendMessage:', sessionId, (userMessage.content ?? '').slice(0, 80));

    // 发送消息前确保 Adapter 使用最新配置（失败则中止，防止用旧 Provider 的 adapter 发送）
    if (!reloadAdapter()) {
      const errorMsg = 'Adapter 加载失败，请检查 LLM 配置（Provider、API Key、Base URL、Model 是否完整）';
      log.error('[AGENT]', errorMsg);
      if (!mainWindow.isDestroyed()) {
        const errorEvent: MetonaStreamEvent = {
          type: MetonaStreamEventType.ERROR,
          requestId: '',
          sessionId,
          iteration: 0,
          seq: 0,
          timestamp: Date.now(),
          error: { code: MetonaErrorCode.UNKNOWN, message: errorMsg, retryable: false },
        };
        mainWindow.webContents.send('agent:streamEvent', errorEvent);
        mainWindow.webContents.send('agent:streamEvent', {
          ...errorEvent,
          type: MetonaStreamEventType.DONE,
        });
      }
      sessionRecorder.stopRecording({ totalIterations: 0, totalTokens: 0, durationMs: 0, terminationReason: 'error' });
      return { success: false, error: errorMsg };
    }

    // TRACE 层：开始录制
    sessionRecorder.startRecording(sessionId);

    // TOOL 层：记录会话开始
    auditService.logSessionStart(sessionId);

    // 保存用户消息到数据库
    // IPC boundary: frontend sends attachments not defined in the MetonaMessage type
    sessionService.saveMessage({
      sessionId,
      role: 'user',
      content: userMessage.content,
      attachments: (userMessage as MetonaMessage & { attachments?: unknown[] }).attachments,
    });

    // 加载历史消息
    const historyRows = sessionService.getMessages(sessionId);
    const history: MetonaMessage[] = historyRows
      .filter((m) => m.role !== 'system')
      .slice(0, -1)
      .map((m) => ({
        role: m.role as MetonaMessage['role'],
        content: m.content,
        reasoningContent: m.reasoningContent,
        toolCalls: m.toolCalls as MetonaMessage['toolCalls'],
        toolResult: m.toolResult as MetonaMessage['toolResult'],
        timestamp: m.timestamp,
      }));

    // 从工作空间文件构建 System Prompt（注入当前工作空间路径，便于 LLM 在需要绝对路径时使用）
    const workspaceFiles = workspaceService.getFiles();
    const systemPrompt = contextBuilder.buildSystemPrompt(workspaceFiles, workspaceService.getPath());

    // v0.3.18 修复: SOUL.md 为空或不存在时降级到默认身份，向前端发 toast 提示用户
    // 避免用户自定义人格丢失但不知情的"安静失败"问题
    if (contextBuilder.isUsingFallbackRole() && !mainWindow.isDestroyed()) {
      mainWindow.webContents.send('toast:show', {
        type: 'info',
        message: '未找到 SOUL.md 或内容为空，已使用默认 Metona 身份。可在工作空间根目录创建 SOUL.md 自定义 Agent 人格',
      });
    }

    // 检索与用户消息相关的记忆，注入到 System Prompt 动态区
    try {
      const memories = memoryManager.search(userMessage.content, { topK: 5, minImportance: 0.3 });
      if (memories.length > 0) {
        const memorySection = memories.map((m, i) =>
          `[${i + 1}] (${m.type}, 重要度: ${m.importance.toFixed(1)}) ${m.content.slice(0, 200)}`,
        ).join('\n');
        const memoryBlock = `## Relevant Memories (Retrieved)\n${memorySection}`;
        systemPrompt.dynamicReminders = systemPrompt.dynamicReminders
          ? `${systemPrompt.dynamicReminders}\n\n---\n\n${memoryBlock}`
          : memoryBlock;
        log.debug(`[AGENT] Injected ${memories.length} memories into system prompt`);
      }
    } catch (err) {
      log.warn('[AGENT] Memory retrieval failed, proceeding without memories:', err);
    }

    // 附件提示注入：用户直接上传的文件/图片，避免 LLM 误以为需要在工作空间查找
    // 解决场景：用户上传图片后，AI 仍调 view_image 去磁盘读取——根因是提示措辞
    // "do NOT search in workspace" 与 view_image "Read an image file" 语义冲突，
    // AI 认为"读取"不等于"搜索"。强化为明确禁止调用 view_image 读取已上传的图片。
    const attachments = (userMessage as MetonaMessage & { attachments?: Array<{ name: string; type: string }> }).attachments;
    if (Array.isArray(attachments) && attachments.length > 0) {
      const attachmentList = attachments.map((att, i) => {
        const typeLabel = att.type === 'image' ? 'image' : att.type === 'text' ? 'text file' : 'file';
        const note = att.type === 'image'
          ? 'already provided to you via vision capability — you can SEE it directly, do NOT call view_image or any tool to read it again'
          : att.type === 'text'
            ? 'content already inlined in the user message, do NOT search in workspace or read it again'
            : 'uploaded directly by user, do NOT search in workspace';
        return `${i + 1}. [${typeLabel}] ${att.name} — ${note}`;
      }).join('\n');

      const attachmentBlock = `## User Attachments (Direct Upload)\nThe following files were uploaded directly by the user to this conversation. They are inline attachments, NOT workspace files:\n${attachmentList}\n\n**IMPORTANT**: Images listed above are already visible to you in this conversation. Do NOT call \`view_image\`, \`read_file\`, or any file tool to read them — doing so wastes a tool call and may fail (they are not workspace files).`;

      systemPrompt.dynamicReminders = systemPrompt.dynamicReminders
        ? `${systemPrompt.dynamicReminders}\n\n---\n\n${attachmentBlock}`
        : attachmentBlock;

      log.debug(`[AGENT] Injected ${attachments.length} attachment hints into system prompt`);
    }

    // 监听 Agent Loop 事件
    // F8: 主进程 text_delta 节流合并
    // 问题：主进程 1:1 转发所有流式事件，text_delta 频率 30-50 次/秒，
    //       每次 IPC 调用都有跨进程开销，叠加渲染进程处理成本。
    // 方案：在主进程聚合 text_delta，32ms 间隔合并转发（IPC 频率降至 ~30 次/秒）。
    //       非 text_delta 事件立即转发（先 flush 缓冲区保证顺序）。
    //       done/error 时立即 flush，避免最后一段 delta 丢失。
    //       与 F5 渲染进程 rAF 批处理叠加，总延迟约 48ms（人眼不敏感）。
    let textDeltaBuffer = '';
    let lastTextEventMeta: Pick<MetonaStreamEvent, 'requestId' | 'sessionId' | 'iteration' | 'seq' | 'timestamp' | 'runId'> | null = null;
    let flushTimer: ReturnType<typeof setTimeout> | null = null;

    const flushTextDeltaBuffer = () => {
      flushTimer = null;
      if (!textDeltaBuffer || !lastTextEventMeta || mainWindow.isDestroyed()) {
        textDeltaBuffer = '';
        lastTextEventMeta = null;
        return;
      }
      // 构建合并的 text_delta 事件，保留最后一个 delta 的元数据
      const mergedEvent: MetonaStreamEvent = {
        ...lastTextEventMeta,
        type: MetonaStreamEventType.TEXT_DELTA,
        delta: textDeltaBuffer,
        seq: lastTextEventMeta.seq, // 使用最后一个 delta 的 seq
        timestamp: Date.now(),
      };
      mainWindow.webContents.send('agent:streamEvent', mergedEvent);
      textDeltaBuffer = '';
      lastTextEventMeta = null;
    };

    const onStreamEvent = (event: MetonaStreamEvent) => {
      if (mainWindow.isDestroyed()) return;

      // F8: text_delta 聚合，其他事件立即转发（先 flush 保证顺序）
      if (event.type === MetonaStreamEventType.TEXT_DELTA && event.delta) {
        if (textDeltaBuffer === '') {
          lastTextEventMeta = {
            requestId: event.requestId,
            sessionId: event.sessionId,
            iteration: event.iteration,
            seq: event.seq,
            timestamp: event.timestamp,
            runId: event.runId,
          };
        } else {
          // 更新 seq 为最新 delta 的 seq（保证前端 seq 连续性判断）
          lastTextEventMeta = {
            ...lastTextEventMeta!,
            seq: event.seq,
            timestamp: event.timestamp,
            runId: event.runId,
          };
        }
        textDeltaBuffer += event.delta;
        if (flushTimer === null) {
          flushTimer = setTimeout(flushTextDeltaBuffer, 32);
        }
        return;
      }

      // 非 text_delta 事件：先 flush 缓冲区，再立即转发（保证事件顺序）
      if (flushTimer !== null) {
        clearTimeout(flushTimer);
        flushTextDeltaBuffer();
      }
      mainWindow.webContents.send('agent:streamEvent', event);
    };

    const onStateChange = (data: { previous?: string; current?: string; sessionId?: string; iteration?: number; state?: string; runId?: string }) => {
      // AGENT 层：记录状态转换
      if (data.previous) log.info(`[AGENT] State: ${data.previous} → ${data.current}`);
      // 转发到渲染进程（携带迭代号）
      if (!mainWindow.isDestroyed()) {
        mainWindow.webContents.send('agent:stateChange', data);
      }
    };

    // M-6: 转发上下文压缩事件为 toast 通知 + streamEvent 通知
    // v0.3.18 修复: 同时通过 streamEvent 转发压缩数据，前端 store 据以更新 token 显示
    //   解决"压缩触发但前端 token 显示不降"的缺陷
    const onCompressed = (data: { iteration?: number; originalTokens?: number; compressedTokens?: number }) => {
      const savedTokens = Math.max(0, (data.originalTokens ?? 0) - (data.compressedTokens ?? 0));
      if (!mainWindow.isDestroyed()) {
        // toast 通知用户压缩已发生
        mainWindow.webContents.send('toast:show', {
          type: 'info',
          message: `上下文压缩: ${data.originalTokens ?? '?'} → ${data.compressedTokens ?? '?'} tokens（节省 ${savedTokens}）`,
        });
        // 通过 streamEvent 转发，前端 useAgentStream 监听 'compressed' 类型后更新 store
        mainWindow.webContents.send('agent:streamEvent', {
          type: 'compressed',
          sessionId,
          iteration: data.iteration ?? 0,
          originalTokens: data.originalTokens,
          compressedTokens: data.compressedTokens,
          savedTokens,
          timestamp: Date.now(),
        });
      }
    };

    // v0.3.0: 转发死循环检测事件为 toast 警告
    const onDeadLoop = (data: { iteration?: number; runId?: string; sessionId?: string }) => {
      log.warn(`[AGENT] Dead loop detected at iteration ${data.iteration ?? '?'}`);
      if (!mainWindow.isDestroyed()) {
        mainWindow.webContents.send('toast:show', {
          type: 'warning',
          message: `检测到死循环（第 ${data.iteration ?? '?'} 轮）：连续3轮重复相同工具调用，已自动终止`,
        });
      }
    };

    agentLoop.on('streamEvent', onStreamEvent);
    agentLoop.on('stateChange', onStateChange);
    agentLoop.on('compressed', onCompressed);
    agentLoop.on('deadLoop', onDeadLoop);

    try {
      // 提示注入检测（安全模块）
      const injectionResult = promptInjectionDefender.detect(userMessage.content);
      if (injectionResult.riskScore >= 7) {
        log.warn('[PromptInjectionDefender] Blocked message:', injectionResult.findings);
        if (!mainWindow.isDestroyed()) {
          const metonaError: MetonaError = {
            code: MetonaErrorCode.UNKNOWN,
            message: `Message blocked by prompt injection defense: ${injectionResult.recommendation}`,
            retryable: false,
          };
          const errorEvent: MetonaStreamEvent = {
            type: MetonaStreamEventType.ERROR,
            requestId: '',
            sessionId,
            iteration: 0,
            seq: 0,
            timestamp: Date.now(),
            error: metonaError,
          };
          mainWindow.webContents.send('agent:streamEvent', errorEvent);
        }
        // TRACE 层：停止录制
        sessionRecorder.stopRecording({
          totalIterations: 0,
          totalTokens: 0,
          durationMs: 0,
          terminationReason: 'error',
        });
        return { success: false, error: 'Message blocked by prompt injection defense' };
      }
      if (injectionResult.riskScore >= 4) {
        log.warn('[PromptInjectionDefender] Suspicious patterns detected:', injectionResult.findings);
      }

      // TRACE 层：记录上下文构建
      sessionRecorder.recordContextBuilt({
        tokenCount: estimateMessagesTokens(history),
        usageRatio: 0,
      });

      // 启动 Agent Loop
      const output = await agentLoop.runStream(userMessage, sessionId, history, systemPrompt);

      // 输出验证（不阻塞响应，仅记录警告）
      // v0.3.0 修复: 传入 toolResults 和 context，启用事实一致性检查和幻觉检测
      try {
        const toolResults = output.iterations
          .flatMap((step) => step.toolResults ?? [])
          .map((r) => (typeof r.result === 'string' ? r.result : JSON.stringify(r.result)));
        const context = [...history, { role: 'user', content: userMessage.content }]
          .map((m) => `${m.role}: ${m.content}`).join('\n');
        const validation = await outputValidator.validate(output.finalAnswer, {
          toolResults: toolResults.length > 0 ? toolResults : undefined,
          context,
        });
        if (!validation.valid || validation.issues.length > 0) {
          log.warn('[OutputValidator] Validation issues:', validation.issues);
        }
        log.debug(`[OutputValidator] Score: ${validation.score}, Valid: ${validation.valid}`);
      } catch (err) {
        log.error('[OutputValidator] Validation failed:', err);
      }

      // 保存每轮迭代的 assistant 消息到数据库（含思考内容和工具调用）
      for (const step of output.iterations) {
        if (!step.thought) continue;

        // 将 MetonaToolCall + MetonaToolResult 转换为前端 ToolCallInfo 格式
        const toolCallsWithResults = step.toolCalls?.map((tc) => {
          const result = step.toolResults?.find((r) => r.toolCallId === tc.id);
          return {
            id: tc.id,
            name: tc.name,
            args: tc.args,
            status: result?.success ? 'success' as const : 'error' as const,
            result: result?.result,
            durationMs: result?.durationMs,
            error: result?.error,
          };
        });

        // 只有当有内容、思考内容或工具调用时才保存
        if (step.thought.content || step.thought.reasoningContent || toolCallsWithResults?.length) {
          // C-6 修复: assistant 消息仅有 tool_calls 时 content 必须为 null（而非空字符串）
          // @see project_memory.md — Assistant messages with tool_calls must set content to null
          // 空字符串会导致 LLM API 返回 400 错误，必须使用 null
          const assistantContent = (toolCallsWithResults?.length && !step.thought.content)
            ? null
            : step.thought.content;
          sessionService.saveMessage({
            sessionId,
            role: 'assistant',
            content: assistantContent,
            reasoningContent: step.thought.reasoningContent || undefined,
            toolCalls: toolCallsWithResults,
            iteration: step.iteration,
          });
        }

        // v0.3.0 修复: 保存 tool 结果消息到数据库
        // DeepSeek/OpenAI API 要求 assistant 消息有 tool_calls 时，后续必须有对应的 tool 结果消息
        // 缺失会导致第二次发消息时 API 返回 400 Bad Request
        if (step.toolResults) {
          for (const result of step.toolResults) {
            const resultContent = typeof result.result === 'string'
              ? result.result
              : JSON.stringify(result.result);
            sessionService.saveMessage({
              sessionId,
              role: 'tool',
              content: result.error ?? resultContent,
              toolResult: result,
              iteration: step.iteration,
            });
          }
        }
      }

      // 更新 Token 统计
      if (output.totalTokenUsage.totalTokens > 0) {
        sessionService.updateTokenUsage(sessionId, output.totalTokenUsage.totalTokens);
      }

      // 更新 MEMORY.md 时间戳
      workspaceService.updateMemoryTimestamp();

      // 会话结束：AI 判断本次对话有哪些重要内容需要持久化到 MEMORY.md
      // 异步执行，不阻塞主流程返回；失败仅记录日志
      memoryConsolidator
        .consolidate(userMessage.content, output.finalAnswer, output.iterations)
        .then((result) => {
          if (result.appended > 0) {
            log.info(`[AGENT] Memory consolidated: ${result.appended} entries appended to MEMORY.md`);
            // 通知渲染进程记忆已更新
            if (!mainWindow.isDestroyed()) {
              mainWindow.webContents.send('toast:show', {
                type: 'info',
                message: `AI 已将 ${result.appended} 条重要记忆写入 MEMORY.md`,
              });
            }
          }
        })
        .catch((err) => {
          log.warn('[AGENT] Memory consolidation failed:', err);
        });

      // TOOL 层：记录会话结束
      auditService.logSessionEnd({
        sessionId,
        totalIterations: output.iterations.length,
        totalTokens: output.totalTokenUsage.totalTokens,
        durationMs: output.durationMs,
        terminationReason: output.terminationReason,
      });

      // TRACE 层：停止录制
      sessionRecorder.stopRecording({
        totalIterations: output.iterations.length,
        totalTokens: output.totalTokenUsage.totalTokens,
        durationMs: output.durationMs,
        terminationReason: output.terminationReason,
      });

      log.info(`[AGENT] Completed: ${output.terminationReason}, ${output.iterations.length} iterations, ${output.durationMs}ms`);

      return { success: true };
    } catch (error) {
      log.error('[AGENT] Error:', error);

      // TOOL 层：记录错误
      auditService.log({
        sessionId,
        eventType: 'error',
        actor: 'agent',
        target: 'agent_loop',
        details: { error: (error as Error).message },
        outcome: 'error',
      });

      // TRACE 层：停止录制
      sessionRecorder.stopRecording({
        totalIterations: 0,
        totalTokens: 0,
        durationMs: 0,
        terminationReason: 'error',
      });

      // 发送错误事件到 UI
      if (!mainWindow.isDestroyed()) {
        const metonaError: MetonaError = {
          code: MetonaErrorCode.UNKNOWN,
          message: (error as Error).message,
          retryable: false,
        };
        const errorEvent: MetonaStreamEvent = {
          type: MetonaStreamEventType.ERROR,
          requestId: '',
          sessionId,
          iteration: 0,
          seq: 0,
          timestamp: Date.now(),
          error: metonaError,
        };
        mainWindow.webContents.send('agent:streamEvent', errorEvent);
      }

      return { success: false, error: (error as Error).message };
    } finally {
      // F8: 注销前 flush 残留的 text_delta 缓冲区，避免丢失最后一段内容
      if (flushTimer !== null) {
        clearTimeout(flushTimer);
        flushTextDeltaBuffer();
      }
      agentLoop.off('streamEvent', onStreamEvent);
      agentLoop.off('stateChange', onStateChange);
      agentLoop.off('compressed', onCompressed);
      agentLoop.off('deadLoop', onDeadLoop);
    }
  });

  ipcMain.handle('agent:abortSession', async (_event, sessionId) => {
    log.info('[AGENT] Abort:', sessionId);
    agentLoop.abort();
    // MT-1 修复: 等待当前 run 完全结束再返回，防止用户立即重发时新消息卡在
    // runStream 的 currentRunPromise 等待中（最长卡 120s 工具超时）
    await agentLoop.waitForAbort();
    // v0.3.0 修复: 清理所有等待中的工具确认，避免定时器泄漏和超时 toast 在新会话中弹出
    confirmationHook.clearPending();

    // TOOL 层：记录中断
    auditService.log({
      sessionId,
      eventType: 'session_end',
      actor: 'user',
      target: 'session',
      details: { reason: 'user_abort' },
      outcome: 'denied',
    });

    return { success: true };
  });

  // ===== 会话管理 =====

  ipcMain.handle('sessions:list', async () => {
    return sessionService.list();
  });

  ipcMain.handle('sessions:create', async (_event, title?: string) => {
    return sessionService.create(title);
  });

  // M-34 修复: 统一 sessionId 校验辅助函数
  const isValidSessionId = (id: unknown): id is string =>
    typeof id === 'string' && id.length > 0 && id.length <= 200;

  ipcMain.handle('sessions:rename', async (_event, sessionId: unknown, title: unknown) => {
    // M-34 修复: 校验 sessionId 和 title
    if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' };
    if (typeof title !== 'string' || !title.trim()) return { success: false, error: 'Invalid title' };
    return { success: sessionService.rename(sessionId, title) };
  });

  ipcMain.handle('sessions:delete', async (_event, sessionId: unknown) => {
    // M-34 修复: 校验 sessionId
    if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' };
    return { success: sessionService.delete(sessionId) };
  });

  ipcMain.handle('sessions:getMessages', async (_event, sessionId: unknown) => {
    // M-34 修复: 校验 sessionId
    if (!isValidSessionId(sessionId)) return [];
    return sessionService.getMessages(sessionId);
  });

  ipcMain.handle('sessions:pin', async (_event, sessionId: unknown, pinned: unknown) => {
    // M-34 修复: 校验 sessionId 和 pinned
    if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' };
    if (typeof pinned !== 'boolean') return { success: false, error: 'Invalid pinned flag' };
    return { success: sessionService.pin(sessionId, pinned) };
  });

  ipcMain.handle('sessions:archive', async (_event, sessionId: unknown, archived: unknown) => {
    // M-34 修复: 校验 sessionId 和 archived
    if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' };
    if (typeof archived !== 'boolean') return { success: false, error: 'Invalid archived flag' };
    return { success: sessionService.archive(sessionId, archived) };
  });

  ipcMain.handle('sessions:deleteMessage', async (_event, messageId: unknown) => {
    // M-34 修复: 校验 messageId
    if (typeof messageId !== 'string' || !messageId) return { success: false, error: 'Invalid messageId' };
    return { success: sessionService.deleteMessage(messageId) };
  });

  ipcMain.handle('sessions:clearMessages', async (_event, sessionId: unknown) => {
    // M-34 修复: 校验 sessionId
    if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' };
    return { success: sessionService.clearMessages(sessionId) };
  });

  ipcMain.handle('sessions:saveTrace', async (_event, sessionId: unknown, data: unknown) => {
    // M-34 修复: 校验 sessionId 和 data
    if (!isValidSessionId(sessionId)) return { success: false, error: 'Invalid sessionId' };
    // 严格校验 data 结构：traceSteps 必须为数组，tokenUsage 必须存在
    if (!data || typeof data !== 'object') return { success: false, error: 'Invalid trace data' };
    const traceData = data as { traceSteps?: unknown; tokenUsage?: unknown };
    if (!Array.isArray(traceData.traceSteps)) {
      return { success: false, error: 'Invalid trace data: traceSteps must be an array' };
    }
    if (traceData.tokenUsage === undefined) {
      return { success: false, error: 'Invalid trace data: tokenUsage is required' };
    }
    try {
      sessionService.saveTraceData(sessionId, {
        traceSteps: traceData.traceSteps,
        tokenUsage: traceData.tokenUsage,
      });
      return { success: true };
    } catch (error) {
      return { success: false, error: (error as Error).message };
    }
  });

  ipcMain.handle('sessions:getTrace', async (_event, sessionId: unknown) => {
    // M-34 修复: 校验 sessionId
    if (!isValidSessionId(sessionId)) return null;
    return sessionService.getTraceData(sessionId);
  });

  // ===== MCP 管理 =====

  ipcMain.handle('mcp:listServers', async () => {
    return mcpManager.getServerStates();
  });

  ipcMain.handle('mcp:addServer', async (_event, config: { name: string; transport: string; command?: string; args?: string[]; url?: string }) => {
    // M-38 修复: 完整参数校验，防止字段缺失或类型不符导致异常行为
    if (!config || typeof config !== 'object') {
      return { success: false, error: 'Invalid config' };
    }
    if (typeof config.name !== 'string' || !config.name.trim()) {
      return { success: false, error: 'Server name is required' };
    }
    // M-5 修复: transport 运行时校验（替代 as 'stdio' | 'sse' 断言）
    if (config.transport !== 'stdio' && config.transport !== 'sse') {
      return { success: false, error: `Invalid transport: ${config.transport}. Must be 'stdio' or 'sse'` };
    }
    // stdio 类型必须有 command
    if (config.transport === 'stdio' && (typeof config.command !== 'string' || !config.command.trim())) {
      return { success: false, error: 'command is required for stdio transport' };
    }
    // sse 类型必须有合法 url
    if (config.transport === 'sse') {
      if (typeof config.url !== 'string' || !config.url.trim()) {
        return { success: false, error: 'url is required for sse transport' };
      }
      try { new URL(config.url); } catch {
        return { success: false, error: 'Invalid url format' };
      }
    }
    try {
      await mcpManager.addServer({
        name: config.name,
        transport: config.transport,  // 已校验，无需断言
        command: config.command,
        args: config.args,
        url: config.url,
        enabled: true,
      });
      log.info(`MCP server added: ${config.name}`);
      return { success: true };
    } catch (error) {
      return { success: false, error: (error instanceof Error ? error.message : String(error)) };
    }
  });

  ipcMain.handle('mcp:removeServer', async (_event, name: string) => {
    // M-38 修复: name 校验
    if (typeof name !== 'string' || !name.trim()) {
      return { success: false, error: 'Invalid server name' };
    }
    try {
      await mcpManager.removeServer(name);
      return { success: true };
    } catch (error) {
      return { success: false, error: (error instanceof Error ? error.message : String(error)) };
    }
  });

  ipcMain.handle('mcp:toggleServer', async (_event, name: string, enabled: boolean) => {
    // M-38 修复: name 和 enabled 校验
    if (typeof name !== 'string' || !name.trim()) {
      return { success: false, error: 'Invalid server name' };
    }
    if (typeof enabled !== 'boolean') {
      return { success: false, error: 'Invalid enabled flag' };
    }
    try {
      await mcpManager.toggleServer(name, enabled);
      return { success: true };
    } catch (error) {
      return { success: false, error: (error instanceof Error ? error.message : String(error)) };
    }
  });

  // ===== 记忆 =====

  ipcMain.handle('db:searchMemories', async (_event, query: unknown, options: unknown) => {
    // M-37 修复: query 和 options 校验
    if (typeof query !== 'string' || !query.trim()) {
      return [];
    }
    // 构造合法的搜索选项（仅保留已知字段，强制类型安全）
    const VALID_MEMORY_TYPES: readonly MemoryType[] = ['episodic', 'semantic', 'working'];
    const searchOptions: { topK?: number; sessionId?: string; type?: MemoryType; minImportance?: number } = {};
    if (options && typeof options === 'object') {
      const opts = options as Record<string, unknown>;
      // topK 限制范围 1-100（防止过大查询）
      if (opts.topK !== undefined) {
        const topK = Number(opts.topK);
        if (Number.isFinite(topK) && topK >= 1 && topK <= 100) {
          searchOptions.topK = topK;
        } else {
          searchOptions.topK = 10;  // 默认值
        }
      }
      if (typeof opts.sessionId === 'string') searchOptions.sessionId = opts.sessionId;
      // type 必须是合法的 MemoryType 枚举值
      if (typeof opts.type === 'string' && (VALID_MEMORY_TYPES as readonly string[]).includes(opts.type)) {
        searchOptions.type = opts.type as MemoryType;
      }
      if (typeof opts.minImportance === 'number' && Number.isFinite(opts.minImportance)) {
        searchOptions.minImportance = opts.minImportance;
      }
    }
    return memoryManager.search(query, searchOptions);
  });

  // ===== 配置 =====

  ipcMain.handle('config:get', async (_event, key) => {
    return configService.get(key);
  });

  ipcMain.handle('config:set', async (_event, key: unknown, value: unknown) => {
    // M-42 修复: 参数校验 + 敏感数据脱敏
    if (typeof key !== 'string' || !key.trim()) {
      return { success: false, error: 'Invalid config key' };
    }
    // value 必须是可序列化的基本类型（string|number|boolean|null）
    if (value !== null && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
      return { success: false, error: 'Invalid config value: must be string, number, boolean, or null' };
    }

    // C-1 修复: Provider 切换时清空 API key，防止使用不兼容的 key 导致 401 错误
    // @see project_memory.md — When switching LLM providers, the API key must be cleared
    if (key === 'llm.provider') {
      const oldProvider = configService.get<string>('llm.provider') ?? '';
      const newProvider = (value as string) ?? '';
      if (oldProvider && newProvider && oldProvider !== newProvider) {
        configService.set('llm.apiKey', '');
        log.info(`[CONFIG] Provider changed (${oldProvider} → ${newProvider}), API key cleared to prevent incompatible key usage`);
      }
    }

    configService.set(key, value);

    // M-42 修复: 敏感配置项脱敏后再写入审计日志，防止 API Key 明文泄露
    // 敏感 key 包括 llm.apiKey、llm.fallbackApiKey、agnes.apiKey 等
    // 审计补充修复: 短值（length <= 4）时 slice(-4) 会返回整个 value 导致脱敏失效，
    // 改为：长值保留后 4 位核对，短值完全掩码为 '***'
    const SENSITIVE_KEY_PATTERNS = ['apikey', 'api_key', 'apitoken', 'token', 'secret', 'password'];
    const isSensitiveKey = SENSITIVE_KEY_PATTERNS.some(p => key.toLowerCase().includes(p));
    const auditValue = isSensitiveKey && typeof value === 'string' && value.length > 0
      ? (value.length > 4 ? '***' + value.slice(-4) : '***')
      : value;

    // TOOL 层：记录配置变更
    auditService.log({
      sessionId: '',
      eventType: 'config_change',
      actor: 'user',
      target: key,
      details: { value: auditValue },
      outcome: 'success',
    });

    // LLM 相关配置变更时热重载 Adapter
    // v0.3.1: 加入 deepseek.contextWindow / agnes.contextWindow / mimo.contextWindow，使上下文窗口配置变化也触发热重载
    if (['llm.provider', 'llm.model', 'llm.apiKey', 'llm.baseURL', 'ollama.numCtx',
         'deepseek.contextWindow', 'agnes.contextWindow', 'mimo.contextWindow'].includes(key)) {
      // P0-3 修复: reloadAdapter 失败时返回 success:false，让前端知道配置不完整
      const reloadSuccess = reloadAdapter();
      if (!reloadSuccess) {
        log.warn(`[CONFIG] Adapter reload failed after ${key} change`);
        return { success: false, error: 'LLM 配置不完整，请检查 Provider、API Key、Base URL 和 Model 是否都已填写' };
      } else {
        log.info(`[CONFIG] LLM config changed (${key}), adapter reloaded`);
      }
    }

    // Agent 配置变更时热更新 Engine
    if (key === 'agent.maxIterations') {
      agentLoop.updateConfig({ maxIterations: value as number });
      log.info(`[CONFIG] Agent maxIterations updated to ${value}`);
    } else if (key === 'agent.totalTimeoutMs') {
      agentLoop.updateConfig({ totalTimeoutMs: value as number });
      log.info(`[CONFIG] Agent totalTimeoutMs updated to ${value}`);
    } else if (key === 'agent.enableThinking') {
      agentLoop.updateConfig({ thinkingEnabled: value as boolean });
      // L-18 修复: 同步更新 orchestrator.defaultConfig，确保新建 SubAgent 使用最新配置
      orchestrator.updateDefaultConfig({ thinkingEnabled: value as boolean });
      log.info(`[CONFIG] Agent thinkingEnabled updated to ${value}`);
    } else if (key === 'agent.thinkingEffort') {
      agentLoop.updateConfig({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' });
      orchestrator.updateDefaultConfig({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' });
      log.info(`[CONFIG] Agent thinkingEffort updated to ${value}`);
    } else if (key === 'ollama.numCtx') {
      // numCtx 同时更新 Engine 的 contextLength
      agentLoop.updateConfig({ contextLength: (value as number) || undefined });
      orchestrator.updateDefaultConfig({ contextLength: (value as number) || undefined });
      log.info(`[CONFIG] Agent contextLength updated to ${value}`);
    } else if (key === 'deepseek.contextWindow' || key === 'agnes.contextWindow' || key === 'mimo.contextWindow') {
      // v0.3.1: DeepSeek/Agnes/MiMo contextWindow 变更，同步到 Engine 和 Orchestrator
      // reloadAdapter 已重建 adapter，此处确保 Engine contextWindow 同步（兜底）
      const ctxWindow = (value as number) || undefined;
      agentLoop.updateConfig({ contextWindow: ctxWindow });
      orchestrator.updateDefaultConfig({ contextWindow: ctxWindow });
      log.info(`[CONFIG] ${key} updated to ${value}`);
    } else if (key === 'agent.confirmationTimeoutMs') {
      // 工具确认超时变更，即时应用到 ConfirmationHook
      confirmationHook.setConfirmationTimeout(value as number);
      log.info(`[CONFIG] Confirmation timeout updated to ${value}ms`);
    } else if (key === 'agent.toolExecutionTimeoutMs') {
      // 工具执行兜底超时变更，即时应用到 Engine
      agentLoop.updateConfig({ toolExecutionTimeoutMs: value as number });
      log.info(`[CONFIG] Tool execution timeout updated to ${value}ms`);
    }

    // 日志级别变更时即时应用
    if (key === 'logging.level') {
      const level = value as 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly';
      log.transports.file.level = level;
      log.transports.console.level = level;
      log.info(`[CONFIG] Log level updated to ${level}`);
    }

    // 广播配置变更事件，前端监听后更新 store（如 maxIterations 显示）
    if (!mainWindow.isDestroyed()) {
      mainWindow.webContents.send('config:changed', { key, value });
    }

    // 工作空间路径变更时写入独立文件（下次启动生效）
    if (key === 'workspace.path') {
      try {
        const { writeWorkspacePathToFile } = await import('../main');
        if (value) writeWorkspacePathToFile(value as string);
        log.info(`[CONFIG] Workspace path saved (restart required): ${value}`);
      } catch (err) {
        log.error('[CONFIG] Failed to save workspace path:', err);
        // v0.3.10: 工作空间路径写入失败必须告知用户，否则下次启动仍使用旧路径
        // 用户看到"配置已保存"却实际未生效，会误以为配置系统故障
        return { success: false, error: `工作空间路径保存失败：${(err as Error).message}` };
      }
    }

    return { success: true };
  });

  // ===== v0.3.9: 批量保存配置（解决串行保存中间态触发 reloadAdapter 失败问题）=====
  // 设计原因：前端设置页一次保存 8 个字段，串行 config:set 会在中间态（如 provider
  // 已改但 apiKey 还没保存）触发 reloadAdapter，导致返回"LLM 配置不完整"错误。
  // 批量保存：先写入所有字段，最后统一触发一次 reloadAdapter 和 Engine/Orchestrator 同步。
  ipcMain.handle('config:setBatch', async (_event, entries: unknown) => {
    // 参数校验：必须是 {key, value}[] 非空数组
    if (!Array.isArray(entries) || entries.length === 0) {
      return { success: false, error: 'Invalid entries: must be non-empty array of {key, value}' };
    }
    // 逐条校验每个元素的结构和类型
    for (const entry of entries) {
      if (!entry || typeof entry !== 'object') {
        return { success: false, error: 'Invalid entry: must be {key, value} object' };
      }
      const e = entry as { key?: unknown; value?: unknown };
      if (typeof e.key !== 'string' || !e.key.trim()) {
        return { success: false, error: 'Invalid config key in batch' };
      }
      const v = e.value;
      if (v !== null && typeof v !== 'string' && typeof v !== 'number' && typeof v !== 'boolean') {
        return { success: false, error: `Invalid config value for key "${e.key}": must be string, number, boolean, or null` };
      }
    }

    // 敏感字段脱敏工具（与 config:set 保持一致）
    const SENSITIVE_KEY_PATTERNS = ['apikey', 'api_key', 'apitoken', 'token', 'secret', 'password'];
    const maskSensitive = (key: string, value: unknown): unknown => {
      const isSensitive = SENSITIVE_KEY_PATTERNS.some(p => key.toLowerCase().includes(p));
      if (isSensitive && typeof value === 'string' && value.length > 0) {
        return value.length > 4 ? '***' + value.slice(-4) : '***';
      }
      return value;
    };

    // LLM 相关字段集合（用于判断是否需要 reloadAdapter）
    const LLM_KEYS = ['llm.provider', 'llm.model', 'llm.apiKey', 'llm.baseURL', 'ollama.numCtx',
                      'deepseek.contextWindow', 'agnes.contextWindow', 'mimo.contextWindow'];

    // 第一步：逐条写入 configService + 审计日志
    // 不在此处触发 reloadAdapter，避免中间态失败
    let needsReloadAdapter = false;
    const engineUpdates: { key: string; value: unknown }[] = [];
    let workspacePathValue: string | null = null;
    let logLevelValue: 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly' | null = null;

    // v0.3.10 防御性修复: Provider 切换时清空 API key 的逻辑必须在循环前执行，
    // 不能依赖 entries 中 llm.provider 出现在 llm.apiKey 之前。
    // 旧实现：若前端误把 llm.apiKey 放在 llm.provider 之前，会先 set apiKey，
    //        随后处理 llm.provider 时清空 apiKey，导致用户填写的 apiKey 丢失。
    // 新实现：先扫描 entries 找出 llm.provider 的值，与当前值比较，若变化则清空 apiKey，
    //        然后循环按 entries 顺序 set（包括 llm.apiKey），最终值正确。
    const providerEntry = (entries as Array<{ key: string; value: unknown }>)
      .find((e) => e.key === 'llm.provider');
    if (providerEntry) {
      const oldProvider = configService.get<string>('llm.provider') ?? '';
      const newProvider = (providerEntry.value as string) ?? '';
      if (oldProvider && newProvider && oldProvider !== newProvider) {
        configService.set('llm.apiKey', '');
        log.info(`[CONFIG] Provider changed (${oldProvider} → ${newProvider}), API key cleared (batch mode)`);
      }
    }

    for (const entry of entries) {
      const { key, value } = entry as { key: string; value: unknown };

      configService.set(key, value);
      needsReloadAdapter = needsReloadAdapter || LLM_KEYS.includes(key);

      // 收集 Engine/Orchestrator 相关更新（稍后统一应用）
      if (key === 'agent.maxIterations' || key === 'agent.totalTimeoutMs' ||
          key === 'agent.enableThinking' || key === 'agent.thinkingEffort' ||
          key === 'ollama.numCtx' || key === 'deepseek.contextWindow' ||
          key === 'agnes.contextWindow' || key === 'mimo.contextWindow' ||
          key === 'agent.toolExecutionTimeoutMs' || key === 'agent.confirmationTimeoutMs') {
        engineUpdates.push({ key, value });
      }

      if (key === 'workspace.path' && typeof value === 'string') {
        workspacePathValue = value;
      }
      if (key === 'logging.level' && typeof value === 'string') {
        logLevelValue = value as 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly';
      }

      // 审计日志（脱敏）
      auditService.log({
        sessionId: '',
        eventType: 'config_change',
        actor: 'user',
        target: key,
        details: { value: maskSensitive(key, value) },
        outcome: 'success',
      });
    }

    // 第二步：统一触发一次 reloadAdapter（仅在 LLM 配置变更时）
    if (needsReloadAdapter) {
      const reloadSuccess = reloadAdapter();
      if (!reloadSuccess) {
        log.warn('[CONFIG] Adapter reload failed after batch config save');
        return { success: false, error: 'LLM 配置不完整，请检查 Provider、API Key、Base URL 和 Model 是否都已填写' };
      } else {
        log.info('[CONFIG] LLM config batch changed, adapter reloaded');
      }
    }

    // 第三步：统一应用 Engine/Orchestrator 更新（取每个 key 的最后值）
    // 注意：reloadAdapter 内部已重建 adapter 并同步 contextWindow，此处仅处理非 LLM 字段
    // 的 Engine 配置（maxIterations/totalTimeoutMs/enableThinking 等）
    for (const { key, value } of engineUpdates) {
      if (key === 'agent.maxIterations') {
        agentLoop.updateConfig({ maxIterations: value as number });
      } else if (key === 'agent.totalTimeoutMs') {
        agentLoop.updateConfig({ totalTimeoutMs: value as number });
      } else if (key === 'agent.enableThinking') {
        agentLoop.updateConfig({ thinkingEnabled: value as boolean });
        orchestrator.updateDefaultConfig({ thinkingEnabled: value as boolean });
      } else if (key === 'agent.thinkingEffort') {
        agentLoop.updateConfig({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' });
        orchestrator.updateDefaultConfig({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' });
      } else if (key === 'ollama.numCtx') {
        agentLoop.updateConfig({ contextLength: (value as number) || undefined });
        orchestrator.updateDefaultConfig({ contextLength: (value as number) || undefined });
      } else if (key === 'deepseek.contextWindow' || key === 'agnes.contextWindow' || key === 'mimo.contextWindow') {
        const ctxWindow = (value as number) || undefined;
        agentLoop.updateConfig({ contextWindow: ctxWindow });
        orchestrator.updateDefaultConfig({ contextWindow: ctxWindow });
      } else if (key === 'agent.confirmationTimeoutMs') {
        confirmationHook.setConfirmationTimeout(value as number);
      } else if (key === 'agent.toolExecutionTimeoutMs') {
        agentLoop.updateConfig({ toolExecutionTimeoutMs: value as number });
      }
    }
    if (engineUpdates.length > 0) {
      log.info(`[CONFIG] Engine/Orchestrator updated with ${engineUpdates.length} config changes (batch mode)`);
    }

    // 第四步：日志级别即时应用
    if (logLevelValue) {
      log.transports.file.level = logLevelValue;
      log.transports.console.level = logLevelValue;
      log.info(`[CONFIG] Log level updated to ${logLevelValue}`);
    }

    // 第四步半：广播所有配置变更事件，前端监听后更新 store
    if (!mainWindow.isDestroyed()) {
      for (const { key, value } of entries) {
        mainWindow.webContents.send('config:changed', { key, value });
      }
    }

    // 第五步：工作空间路径写入独立文件（下次启动生效）
    if (workspacePathValue) {
      try {
        const { writeWorkspacePathToFile } = await import('../main');
        writeWorkspacePathToFile(workspacePathValue);
        log.info(`[CONFIG] Workspace path saved (restart required): ${workspacePathValue}`);
      } catch (err) {
        log.error('[CONFIG] Failed to save workspace path:', err);
        // v0.3.10: 工作空间路径写入失败必须告知用户，否则下次启动仍使用旧路径
        return { success: false, error: `工作空间路径保存失败：${(err as Error).message}` };
      }
    }

    return { success: true };
  });

  // ===== 应用工具 =====

  ipcMain.handle('app:getVersion', async () => {
    return app.getVersion();
  });

  ipcMain.handle('app:getAppDataPath', async () => {
    return app.getPath('userData');
  });

  ipcMain.handle('app:openExternal', async (_event, url: unknown) => {
    // M-12 修复: URL 协议白名单校验，防止打开 file:///smb:// 等危险协议
    // @see main.ts setWindowOpenHandler — 两处安全策略保持一致
    if (typeof url !== 'string' || !url) {
      return { success: false, error: 'Invalid URL' };
    }
    try {
      const parsed = new URL(url);
      const ALLOWED_PROTOCOLS = ['http:', 'https:', 'mailto:'];
      if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) {
        log.warn(`[IPC] openExternal blocked: protocol "${parsed.protocol}" not in whitelist`);
        return { success: false, error: `Protocol not allowed: ${parsed.protocol}` };
      }
      await shell.openExternal(url);
      return { success: true };
    } catch (error) {
      log.warn('[IPC] openExternal failed:', (error as Error).message);
      return { success: false, error: (error as Error).message };
    }
  });

  ipcMain.handle('app:showItemInFolder', async (_event, path: unknown) => {
    // M-36 修复: path 类型校验，防止非字符串参数
    if (typeof path !== 'string' || !path) {
      return { success: false, error: 'Invalid path' };
    }
    try {
      shell.showItemInFolder(path);
      return { success: true };
    } catch (error) {
      return { success: false, error: (error as Error).message };
    }
  });

  ipcMain.handle('app:selectFolder', async (_event, defaultPath?: string) => {
    const result = await dialog.showOpenDialog(mainWindow, {
      properties: ['openDirectory', 'createDirectory'],
      defaultPath: defaultPath ?? app.getPath('home'),
      title: '选择工作空间目录',
    });
    if (result.canceled || result.filePaths.length === 0) return { canceled: true, path: '' };
    return { canceled: false, path: result.filePaths[0] };
  });

  // 重启应用（工作空间切换后调用）
  ipcMain.handle('app:restart', async () => {
    log.info('[APP] Restart requested');
    // 延迟 200ms 让 IPC 响应先返回
    setTimeout(() => {
      app.relaunch();
      app.exit(0);
    }, 200);
    return { success: true };
  });

  // ===== 工作空间校验与继承 =====

  // 校验工作空间路径：检测路径有效性 + 必需文件状态 + 数据库是否存在
  ipcMain.handle('workspace:check', async (_event, targetPath: string) => {
    if (!targetPath || typeof targetPath !== 'string') {
      return { valid: false, reason: '路径不能为空' };
    }

    const { existsSync, statSync } = await import('fs');
    const { resolve } = await import('path');

    const resolvedPath = resolve(targetPath);

    // 校验 1: 路径是否存在
    if (!existsSync(resolvedPath)) {
      return {
        valid: true,
        path: resolvedPath,
        exists: false,
        missingFiles: ['SOUL.md', 'MEMORY.md'],
        isNewWorkspace: true,
        dbExists: false,
        reason: '目录不存在，将在切换后自动创建',
      };
    }

    // 校验 2: 是否为目录
    try {
      const stat = statSync(resolvedPath);
      if (!stat.isDirectory()) {
        return { valid: false, reason: '路径不是目录' };
      }
    } catch {
      return { valid: false, reason: '无法访问路径' };
    }

    // 校验 3: 检测 2 个必需文件状态（SOUL.md + MEMORY.md）
    const requiredFiles = ['SOUL.md', 'MEMORY.md'];
    const missingFiles: string[] = [];
    for (const f of requiredFiles) {
      if (!existsSync(join(resolvedPath, f))) missingFiles.push(f);
    }

    // 校验 4: 检测 .metona/agent.db 是否存在（用于判断是否显示"继承数据库"选项）
    const dbExists = existsSync(join(resolvedPath, '.metona', 'agent.db'));

    return {
      valid: true,
      path: resolvedPath,
      exists: true,
      missingFiles,
      isNewWorkspace: missingFiles.length === requiredFiles.length,
      dbExists,
    };
  });

  // 从旧工作空间继承文件到新工作空间
  ipcMain.handle('workspace:inheritFiles', async (_event, params: {
    targetPath: string;
    sourcePath: string;
    files: string[];
  }) => {
    const { targetPath, sourcePath, files } = params;
    if (!targetPath || !sourcePath || !Array.isArray(files)) {
      return { success: false, error: '参数无效' };
    }

    const { existsSync, mkdirSync, copyFileSync } = await import('fs');
    const { resolve } = await import('path');

    const resolvedTarget = resolve(targetPath);
    const resolvedSource = resolve(sourcePath);

    // 确保目标目录存在
    if (!existsSync(resolvedTarget)) {
      mkdirSync(resolvedTarget, { recursive: true });
    }

    // 继承白名单：键为前端传入的标识，值为实际相对路径分段
    // - SOUL.md: 工作空间根目录的 Agent 身份定义
    // - .metona/agent.db: 工作空间内的 SQLite 数据库（会话/消息/记忆/Trace）
    // 白名单映射防止路径遍历攻击（不直接使用用户传入的路径拼接到 fs 调用）
    const INHERIT_WHITELIST: Record<string, string[]> = {
      'SOUL.md': ['SOUL.md'],
      '.metona/agent.db': ['.metona', 'agent.db'],
    };

    const inherited: string[] = [];
    const failed: Array<{ file: string; error: string }> = [];

    for (const fileName of files) {
      const pathSegments = INHERIT_WHITELIST[fileName];
      // 不在白名单中：拒绝（防止路径遍历）
      if (!pathSegments) {
        failed.push({ file: fileName, error: '不在继承白名单中' });
        continue;
      }
      const srcFile = join(resolvedSource, ...pathSegments);
      const dstFile = join(resolvedTarget, ...pathSegments);
      try {
        // 确保目标文件的父目录存在（如 .metona/）
        const dstDir = join(resolvedTarget, ...pathSegments.slice(0, -1));
        if (!existsSync(dstDir)) {
          mkdirSync(dstDir, { recursive: true });
        }
        if (!existsSync(srcFile)) {
          failed.push({ file: fileName, error: '源文件不存在' });
          continue;
        }

        // === 数据库文件特殊处理 ===
        // agent.db 启用了 WAL 模式（database.service.ts:53），直接 copyFileSync 存在两个问题：
        // 1. WAL 中未 checkpoint 的事务会丢失（最近会话/消息可能没了）
        // 2. 主进程可能仍持有 db 句柄，Windows 上文件锁定会导致复制失败或不一致
        // 解决方案：用 better-sqlite3 的 backup API（自带 checkpoint + 一致性快照）
        if (fileName === '.metona/agent.db') {
          try {
            const Database = (await import('better-sqlite3')).default;
            // 以只读方式打开源库（如果主进程已持有句柄，better-sqlite3 会共享锁）
            const srcDb = new Database(srcFile, { readonly: true, fileMustExist: true });
            try {
              // backup API 原子性导出完整快照，自动合并 WAL 内容
              srcDb.backup(dstFile);
              inherited.push(fileName);
              log.info(`[WORKSPACE] Inherited ${fileName} (via SQLite backup): ${resolvedSource} → ${resolvedTarget}`);
            } finally {
              srcDb.close();
            }
          } catch (err) {
            // backup 失败时回退到 copyFileSync（至少保证基本可用）
            log.warn(`[WORKSPACE] SQLite backup failed, fallback to copyFileSync: ${(err as Error).message}`);
            try {
              copyFileSync(srcFile, dstFile);
              inherited.push(fileName);
              log.info(`[WORKSPACE] Inherited ${fileName} (fallback copyFileSync): ${resolvedSource} → ${resolvedTarget}`);
            } catch (err2) {
              failed.push({ file: fileName, error: `backup 和 fallback 均失败: ${(err2 as Error).message}` });
            }
          }
          continue;
        }

        // === 普通文件直接复制 ===
        copyFileSync(srcFile, dstFile);
        inherited.push(fileName);
        log.info(`[WORKSPACE] Inherited ${fileName}: ${resolvedSource} → ${resolvedTarget}`);
      } catch (err) {
        failed.push({ file: fileName, error: (err as Error).message });
      }
    }

    return { success: true, inherited, failed };
  });

  // #51 修复: 校验源数据库完整性，防止继承损坏的数据库（如 WAL 损坏）导致新工作空间数据丢失
  // 在前端勾选 inheritDatabase 时调用，执行 PRAGMA integrity_check
  ipcMain.handle('workspace:checkDatabaseIntegrity', async (_event, sourcePath: string) => {
    if (!sourcePath) return { success: false, error: '参数无效' };
    const { existsSync } = await import('fs');
    const { resolve } = await import('path');
    const srcFile = join(resolve(sourcePath), '.metona', 'agent.db');
    if (!existsSync(srcFile)) {
      // 源数据库不存在不算损坏（可能是新工作空间尚未创建数据库），视为校验通过
      return { success: true, ok: true, detail: 'source database not exist' };
    }
    try {
      const Database = (await import('better-sqlite3')).default;
      // 以只读方式打开源库，避免锁竞争
      const db = new Database(srcFile, { readonly: true, fileMustExist: true });
      try {
        const result = db.prepare('PRAGMA integrity_check').get() as { integrity_check: string };
        const ok = result.integrity_check === 'ok';
        if (!ok) {
          log.warn(`[WORKSPACE] Source database integrity check failed: ${result.integrity_check} (source=${sourcePath})`);
        }
        return { success: true, ok, detail: result.integrity_check };
      } finally {
        db.close();
      }
    } catch (err) {
      log.error(`[WORKSPACE] Failed to check database integrity: ${(err as Error).message}`);
      return { success: false, error: (err as Error).message };
    }
  });

  // 获取当前工作空间详情：路径 + 2 个核心文件状态 + 自动目录状态
  ipcMain.handle('workspace:getInfo', async () => {
    const { existsSync, statSync, readFileSync, readdirSync } = await import('fs');
    const workspacePath = workspaceService.getPath();
    const files = workspaceService.reload(); // 同步外部可能的手动修改

    const REQUIRED = ['SOUL.md', 'MEMORY.md'] as const;
    const AUTO_DIRS = ['logs', '.metona'] as const;

    const fileInfos = REQUIRED.map((name) => {
      const filePath = join(workspacePath, name);
      let exists = false;
      let size = 0;
      let mtime = 0;
      let preview = '';
      try {
        if (existsSync(filePath)) {
          const stat = statSync(filePath);
          exists = true;
          size = stat.size;
          mtime = stat.mtimeMs;
          // 截取前 500 字符作为预览
          const content = readFileSync(filePath, 'utf-8');
          preview = content.length > 500 ? content.slice(0, 500) + '\n...(已截断)' : content;
        }
      } catch {
        // ignore
      }
      // 文件内容映射：SOUL.md → files.soul, MEMORY.md → files.memory
      const contentKey = name.toLowerCase().replace('.md', '') as 'soul' | 'memory';
      return {
        name,
        path: filePath,
        exists,
        size,
        mtime,
        preview: exists ? preview : (files[contentKey] ?? ''),
      };
    });

    const dirInfos = AUTO_DIRS.map((name) => {
      const dirPath = join(workspacePath, name);
      let exists = false;
      let fileCount = 0;
      try {
        if (existsSync(dirPath)) {
          exists = true;
          const stat = statSync(dirPath);
          if (stat.isDirectory()) {
            fileCount = readdirSync(dirPath).length;
          }
        }
      } catch {
        // ignore
      }
      return { name, path: dirPath, exists, fileCount };
    });

    return {
      path: workspacePath,
      files: fileInfos,
      dirs: dirInfos,
    };
  });

  // ===== 工具管理 =====

  ipcMain.handle('tools:list', async () => {
    return toolRegistry.listAllTools().map((t) => ({
      name: t.name,
      description: t.description,
      category: t.category,
      riskLevel: t.riskLevel,
      requiresPermission: t.requiresPermission,
      enabled: t.enabled,
    }));
  });

  ipcMain.handle('tools:toggle', async (_event, toolName: string, enabled: boolean) => {
    // M-35 修复: 校验 toolName 合法性，防止配置 key 污染
    if (typeof toolName !== 'string' || !toolName || typeof enabled !== 'boolean') {
      return { success: false, error: 'Invalid parameters' };
    }
    // 校验 toolName 在已注册工具列表中（防止写入任意配置 key）
    const allTools = toolRegistry.listAllTools();
    if (!allTools.some((t) => t.name === toolName)) {
      log.warn(`[IPC] tools:toggle rejected: unknown tool "${toolName}"`);
      return { success: false, error: `Unknown tool: ${toolName}` };
    }
    // 工具开关通过配置持久化
    configService.set(`tools.${toolName}.enabled`, enabled);
    // 同步到 ToolRegistry（立即生效）
    toolRegistry.setToolEnabled(toolName, enabled);
    // 同步到 AgentLoop 的工具列表
    agentLoop.setTools(toolRegistry.listTools());
    log.info(`Tool ${toolName} ${enabled ? 'enabled' : 'disabled'}`);
    return { success: true };
  });

  // ===== 数据管理 =====

  /**
   * 导出限流常量：单会话导出的最大消息条数。
   *
   * 背景：导出会把所有会话×所有消息×完整 toolResult 一次性 JSON.stringify 成单个 Blob
   * 传到渲染进程（SettingsModal handleExport → new Blob([JSON.stringify(...)])）。
   * 含 view_image 的会话单条 tool_result 就有 ~6.7MB base64 dataUrl，全量导出会直接
   * 撑爆渲染进程堆导致 OOM 崩溃。
   *
   * 双重防护：
   * 1. 剥离每条 tool 消息中的 dataUrl 等超大 base64（sanitizeExportMessage）
   * 2. 全量导出时按会话限制消息条数（MAX_EXPORT_MESSAGES_PER_SESSION），避免超长会话
   *    的纯文本累积也拖垮序列化
   */
  const MAX_EXPORT_MESSAGES_PER_SESSION = 2000;

  /**
   * 剥离导出消息中的超大 base64 字段（如 view_image 的 dataUrl）。
   * 仅用于导出快照——原数据库数据不受影响。
   */
  const sanitizeExportMessage = (msg: Record<string, unknown>): Record<string, unknown> => {
    const toolResult = msg.toolResult as Record<string, unknown> | undefined;
    if (toolResult && typeof toolResult === 'object' && 'result' in toolResult) {
      const result = toolResult.result;
      // result 含 dataUrl（view_image 等图片结果）：剥离 dataUrl，保留 path/size/mimeType 等小字段
      if (result && typeof result === 'object' && !Array.isArray(result) && 'dataUrl' in result) {
        const { dataUrl: _omit, ...rest } = result as Record<string, unknown>;
        void _omit;
        return {
          ...msg,
          toolResult: { ...toolResult, result: { ...rest, _displayNote: '[image base64 omitted in export]' } },
        };
      }
    }
    // attachments[].preview（用户上传图片的 base64 缩略图）也一并剥离，避免导出文件膨胀
    const attachments = msg.attachments;
    if (Array.isArray(attachments)) {
      const sanitized = attachments.map((att) => {
        if (att && typeof att === 'object' && 'preview' in att) {
          const { preview: _omit, ...rest } = att as Record<string, unknown>;
          void _omit;
          return { ...rest, _displayNote: '[preview omitted in export]' };
        }
        return att;
      });
      return { ...msg, attachments: sanitized };
    }
    return msg;
  };

  ipcMain.handle('data:export', async (_event, sessionId?: string) => {
    try {
      if (sessionId) {
        // 导出单个会话：剥离超大 base64 后返回
        const messages = sessionService.getMessages(sessionId);
        const sanitized = messages.map((m) => sanitizeExportMessage(m as unknown as Record<string, unknown>));
        return { success: true, data: sanitized };
      }
      // 导出所有会话：剥离 dataUrl + 每会话限条数，防渲染进程 Blob 序列化 OOM
      const sessions = sessionService.list();
      const allData: Record<string, unknown> = { sessions: [], config: configService.getAll() };
      for (const session of sessions) {
        const rawMessages = sessionService.getMessages(session.id, { limit: MAX_EXPORT_MESSAGES_PER_SESSION });
        const sanitizedMessages = rawMessages.map((m) => sanitizeExportMessage(m as unknown as Record<string, unknown>));
        (allData.sessions as Array<Record<string, unknown>>).push({
          ...session,
          messages: sanitizedMessages,
          truncated: rawMessages.length >= MAX_EXPORT_MESSAGES_PER_SESSION,
        });
      }
      return { success: true, data: allData };
    } catch (error) {
      return { success: false, error: (error as Error).message };
    }
  });

  ipcMain.handle('data:clearSessions', async () => {
    // M-40 修复: 危险操作审计日志，便于追踪异常调用
    log.warn('[DATA] DANGER: clearSessions invoked — all sessions and messages will be deleted');
    const db = sessionService.getDB();
    try {
      db.exec('BEGIN');
      const msgCount = db.prepare('SELECT COUNT(*) as c FROM messages').get() as { c: number };
      const sessCount = db.prepare('SELECT COUNT(*) as c FROM sessions').get() as { c: number };
      db.exec('DELETE FROM messages');
      db.exec('DELETE FROM sessions');
      db.exec('COMMIT');
      log.info(`[DATA] All sessions cleared: ${sessCount.c} sessions, ${msgCount.c} messages deleted`);
      return { success: true, deletedSessions: sessCount.c, deletedMessages: msgCount.c };
    } catch (error) {
      try { db.exec('ROLLBACK'); } catch { /* 忽略回滚错误 */ }
      log.error('[DATA] clearSessions failed:', (error as Error).message);
      return { success: false, error: (error as Error).message };
    }
  });

  ipcMain.handle('data:clearMemories', async () => {
    // M-40 修复: 危险操作审计日志，便于追踪异常调用
    log.warn('[DATA] DANGER: clearMemories invoked — all memories will be deleted');
    const db = sessionService.getDB();
    try {
      // M-41 修复: 三个 DELETE 操作用事务包裹，防止部分失败导致三类记忆数据不一致
      const epiCount = db.prepare('SELECT COUNT(*) as c FROM episodic_memories').get() as { c: number };
      const semCount = db.prepare('SELECT COUNT(*) as c FROM semantic_memories').get() as { c: number };
      const workCount = db.prepare('SELECT COUNT(*) as c FROM working_memories').get() as { c: number };
      db.exec('BEGIN');
      db.exec('DELETE FROM episodic_memories');
      db.exec('DELETE FROM semantic_memories');
      db.exec('DELETE FROM working_memories');
      db.exec('COMMIT');
      log.info(`[DATA] All memories cleared: ${epiCount.c} episodic, ${semCount.c} semantic, ${workCount.c} working memories deleted`);
      return { success: true, deletedEpisodic: epiCount.c, deletedSemantic: semCount.c, deletedWorking: workCount.c };
    } catch (error) {
      // M-41 修复: 失败时回滚事务，确保数据一致性
      try { db.exec('ROLLBACK'); } catch { /* 忽略回滚错误 */ }
      log.error('[DATA] clearMemories failed:', (error as Error).message);
      return { success: false, error: (error as Error).message };
    }
  });

  ipcMain.handle('data:clearAuditLogs', async () => {
    // 审计日志是 INSERT-ONLY，需要先禁用触发器
    const db = sessionService.getDB();
    try {
      db.exec('BEGIN');
      db.exec('DROP TRIGGER IF EXISTS audit_no_delete');
      db.exec('DELETE FROM audit_logs');
      db.exec(`
        CREATE TRIGGER audit_no_delete BEFORE DELETE ON audit_logs
        BEGIN
          SELECT RAISE(ABORT, 'Audit logs are INSERT-ONLY. Deletion is not allowed.');
        END
      `);
      db.exec('COMMIT');
      log.info('[DATA] Audit logs cleared');
      return { success: true };
    } catch (error) {
      try { db.exec('ROLLBACK'); } catch { /* 忽略回滚错误 */ }
      return { success: false, error: (error as Error).message };
    }
  });

  // ===== SearXNG =====

  ipcMain.handle('searxng:testConnection', async (_event, url: string, authKey: string, authType: string) => {
    try {
      if (!url || !/^https?:\/\//.test(url)) {
        return { success: false, error: 'URL 需以 http:// 或 https:// 开头' };
      }

      const startTime = Date.now();
      const headers: Record<string, string> = {};

      if (authKey) {
        if (authType === 'bearer') {
          headers['Authorization'] = `Bearer ${authKey}`;
        } else if (authType === 'basic') {
          headers['Authorization'] = `Basic ${Buffer.from(authKey).toString('base64')}`;
        }
      }

      const testUrl = `${url.replace(/\/$/, '')}/search?q=test&format=json&pageno=1`;
      const response = await fetch(testUrl, {
        headers,
        signal: AbortSignal.timeout(10_000),
      });

      const latencyMs = Date.now() - startTime;

      if (response.ok) {
        return { success: true, statusCode: response.status, latencyMs };
      }
      return { success: false, statusCode: response.status, latencyMs, error: `HTTP ${response.status} ${response.statusText}` };
    } catch (error) {
      return { success: false, error: (error as Error).message };
    }
  });

  // ===== v0.2.0: 工具确认响应（ConfirmationDialog → 主进程）=====
  // 使用 ipcMain.on（渲染进程通过 ipcRenderer.send 发送）
  ipcMain.on('tool:confirmationResponse', (_event, data: unknown) => {
    // M-43 修复: 校验 data 结构，防止 undefined/null 导致 TypeError
    if (!data || typeof data !== 'object') {
      log.warn('[IPC] tool:confirmationResponse rejected: invalid data');
      return;
    }
    const req = data as { toolCallId?: unknown; approved?: unknown; remember?: unknown; autoExecute?: unknown };
    if (typeof req.toolCallId !== 'string' || !req.toolCallId) {
      log.warn('[IPC] tool:confirmationResponse rejected: invalid toolCallId');
      return;
    }
    if (typeof req.approved !== 'boolean') {
      log.warn('[IPC] tool:confirmationResponse rejected: invalid approved');
      return;
    }
    const remember = typeof req.remember === 'boolean' ? req.remember : false;
    const autoExecute = typeof req.autoExecute === 'boolean' ? req.autoExecute : false;
    confirmationHook.resolveConfirmation(req.toolCallId, req.approved, remember, autoExecute);
    log.info(`[CONFIRM] Tool ${req.toolCallId} ${req.approved ? 'approved' : 'denied'}${remember ? ' (remembered)' : ''}${autoExecute ? ' (autoExecute)' : ''}`);
  });

  // ===== v0.3.2: 批量工具确认响应（并行工具调用一次性审批）=====
  // 设计原因：多个工具并行触发时，逐条 IPC 响应会产生 N 次往返，
  // 且前端 state 覆盖会导致部分请求丢失。批量响应一次解决。
  ipcMain.on('tool:confirmationResponseBatch', (_event, data: unknown) => {
    if (!data || typeof data !== 'object') {
      log.warn('[IPC] tool:confirmationResponseBatch rejected: invalid data');
      return;
    }
    const req = data as {
      toolCallIds?: unknown;
      approved?: unknown;
      remember?: unknown;
      autoExecute?: unknown;
    };
    // 严格校验 toolCallIds 数组
    if (!Array.isArray(req.toolCallIds) || req.toolCallIds.length === 0) {
      log.warn('[IPC] tool:confirmationResponseBatch rejected: toolCallIds must be non-empty array');
      return;
    }
    // 每个元素必须是字符串
    for (const id of req.toolCallIds) {
      if (typeof id !== 'string' || !id) {
        log.warn('[IPC] tool:confirmationResponseBatch rejected: invalid toolCallId in array');
        return;
      }
    }
    if (typeof req.approved !== 'boolean') {
      log.warn('[IPC] tool:confirmationResponseBatch rejected: invalid approved');
      return;
    }
    const remember = typeof req.remember === 'boolean' ? req.remember : false;
    const autoExecute = typeof req.autoExecute === 'boolean' ? req.autoExecute : false;
    const resolved = confirmationHook.resolveConfirmationsBatch(
      req.toolCallIds as string[],
      req.approved,
      remember,
      autoExecute,
    );
    log.info(`[CONFIRM] Batch ${req.approved ? 'approved' : 'denied'}: ${resolved.length}/${req.toolCallIds.length} resolved${remember ? ' (remembered)' : ''}${autoExecute ? ' (autoExecute)' : ''}`);
  });

  // ===== v0.3.2: 拉取当前所有 pending 确认（前端弹框打开时调用）=====
  // 用途：解决并行工具触发的多个 IPC 事件在前端 state 中互相覆盖导致丢失的问题。
  // 前端在弹框打开时主动拉取，确保拿到完整的 pending 列表。
  ipcMain.handle('tool:getPendingConfirmations', async () => {
    return { success: true, data: confirmationHook.getPendingConfirmations() };
  });

  // ===== v0.2.0: 持久化自动执行设置 =====
  ipcMain.handle('tool:setAutoExecute', async (_event, toolName: unknown, enabled: unknown) => {
    // M-44 修复: 校验 toolName 合法性和 enabled 类型，防止配置 key 污染
    if (typeof toolName !== 'string' || !toolName) {
      return { success: false, error: 'Invalid toolName' };
    }
    if (typeof enabled !== 'boolean') {
      return { success: false, error: 'Invalid enabled (must be boolean)' };
    }
    // 校验 toolName 在已注册工具列表中（与 tools:toggle 保持一致）
    const allTools = toolRegistry.listAllTools();
    if (!allTools.some((t) => t.name === toolName)) {
      log.warn(`[IPC] tool:setAutoExecute rejected: unknown tool "${toolName}"`);
      return { success: false, error: `Unknown tool: ${toolName}` };
    }
    try {
      confirmationHook.setAutoExecute(toolName, enabled);
      log.info(`[CONFIRM] Tool ${toolName} autoExecute set to ${enabled}`);
      return { success: true };
    } catch (error) {
      return { success: false, error: (error as Error).message };
    }
  });

  ipcMain.handle('tool:getAutoExecuteList', async () => {
    return { success: true, data: confirmationHook.getAutoExecuteList() };
  });

  // ===== v0.2.0: 审计日志链式哈希验证 =====
  ipcMain.handle('audit:verifyChain', async () => {
    try {
      const result = auditService.verifyChain();
      log.info(`[AUDIT] Chain verification: ${result.valid ? 'valid' : 'TAMPERED'} (${result.verifiedRecords}/${result.totalRecords})`);
      return { success: true, ...result };
    } catch (error) {
      log.error('[AUDIT] Chain verification failed:', error);
      return { success: false, error: (error as Error).message };
    }
  });

  ipcMain.handle('audit:query', async (_event, filters?: unknown) => {
    // M-45 修复: 校验 filters 参数类型和范围
    const VALID_AUDIT_EVENT_TYPES: readonly AuditEventType[] = [
      'tool_call', 'permission_check', 'error', 'llm_request',
      'llm_response', 'session_start', 'session_end', 'config_change',
    ];
    let safeFilters: { sessionId?: string; eventType?: AuditEventType; limit?: number } = {};
    if (filters && typeof filters === 'object') {
      const f = filters as Record<string, unknown>;
      if (typeof f.sessionId === 'string' && f.sessionId) safeFilters.sessionId = f.sessionId;
      if (typeof f.eventType === 'string' && f.eventType) {
        // 校验 eventType 是否在合法枚举内
        if (!(VALID_AUDIT_EVENT_TYPES as readonly string[]).includes(f.eventType)) {
          return { success: false, error: `Invalid eventType (must be one of: ${VALID_AUDIT_EVENT_TYPES.join(', ')})` };
        }
        safeFilters.eventType = f.eventType as AuditEventType;
      }
      if (f.limit !== undefined) {
        const limit = Number(f.limit);
        // 审计补充修复: limit 越界时返回 error（与 memory:listAll 策略一致，原为静默降级为 100）
        // 限制 1-1000 范围，防止过大查询拖慢性能
        if (!Number.isFinite(limit) || limit < 1 || limit > 1000) {
          return { success: false, error: 'Invalid limit (must be 1-1000)' };
        }
        safeFilters.limit = Math.floor(limit);
      }
    }
    try {
      return { success: true, data: auditService.query(safeFilters) };
    } catch (error) {
      return { success: false, error: (error as Error).message };
    }
  });

  // ===== v0.2.0: 任务管理 IPC（TaskList UI）=====
  const VALID_TASK_PRIORITIES: readonly string[] = ['low', 'medium', 'high', 'critical'];
  const VALID_TASK_STATUSES: readonly string[] = ['pending', 'in_progress', 'completed', 'blocked', 'cancelled'];

  ipcMain.handle('tasks:list', async (_event, sessionId?: unknown) => {
    // M-46 修复: 校验 sessionId 类型（可选参数）
    if (sessionId !== undefined && (typeof sessionId !== 'string' || !sessionId)) {
      return { success: false, error: 'Invalid sessionId' };
    }
    const db = sessionService.getDB();
    let sql = 'SELECT * FROM tasks';
    const params: unknown[] = [];
    if (sessionId) {
      sql += ' WHERE session_id = ?';
      params.push(sessionId);
    }
    sql += ' ORDER BY order_idx ASC, created_at ASC';
    return { success: true, data: db.prepare(sql).all(...params) };
  });

  ipcMain.handle('tasks:create', async (_event, data: unknown) => {
    // M-46 修复: 校验 data 结构和字段类型/枚举
    if (!data || typeof data !== 'object') {
      return { success: false, error: 'Invalid task data' };
    }
    const req = data as Record<string, unknown>;
    if (typeof req.sessionId !== 'string' || !req.sessionId.trim()) {
      return { success: false, error: 'Invalid sessionId' };
    }
    if (typeof req.title !== 'string' || !req.title.trim()) {
      return { success: false, error: 'Invalid title' };
    }
    if (req.priority !== undefined && !VALID_TASK_PRIORITIES.includes(req.priority as string)) {
      return { success: false, error: `Invalid priority (must be one of: ${VALID_TASK_PRIORITIES.join(', ')})` };
    }
    if (req.parentId !== undefined && req.parentId !== null && typeof req.parentId !== 'string') {
      return { success: false, error: 'Invalid parentId' };
    }
    const db = sessionService.getDB();
    // P4 统一（v0.3.13）: ID 生成方式与 task_manager 工具一致（nanoid）
    const id = `task_${nanoid(12)}`;
    try {
      // P4 统一: 计算 order_idx = MAX(同 session+parent 的 order_idx) + 1（与 task_manager 工具一致）
      // 注意：parent_id IS NULL 时需要单独处理，COALESCE(NULL, NULL) 仍为 NULL
      const parentId = (req.parentId as string | null) ?? null;
      let orderIdx = 0;
      if (parentId === null) {
        const orderRow = db.prepare(
          'SELECT COALESCE(MAX(order_idx), -1) AS maxOrder FROM tasks WHERE session_id = ? AND parent_id IS NULL'
        ).get(req.sessionId) as { maxOrder: number } | undefined;
        orderIdx = (orderRow?.maxOrder ?? -1) + 1;
      } else {
        const orderRow = db.prepare(
          'SELECT COALESCE(MAX(order_idx), -1) AS maxOrder FROM tasks WHERE session_id = ? AND parent_id = ?'
        ).get(req.sessionId, parentId) as { maxOrder: number } | undefined;
        orderIdx = (orderRow?.maxOrder ?? -1) + 1;
      }
      db.prepare(`
        INSERT INTO tasks (id, session_id, title, description, status, priority, parent_id, order_idx, created_at, updated_at)
        VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)
      `).run(
        id,
        req.sessionId,
        req.title,
        typeof req.description === 'string' ? req.description : '',
        (req.priority as string) ?? 'medium',
        parentId,
        orderIdx,
        Date.now(),
        Date.now(),
      );
      return { success: true, id };
    } catch (error) {
      return { success: false, error: (error as Error).message };
    }
  });

  ipcMain.handle('tasks:update', async (_event, id: unknown, updates: unknown, sessionId: unknown) => {
    // M-47 修复: 校验 id 和 updates 结构/枚举
    if (typeof id !== 'string' || !id) {
      return { success: false, error: 'Invalid task id' };
    }
    // P1 修复（v0.3.13）: 补 session_id 越权保护（与 task_manager 工具一致）
    if (typeof sessionId !== 'string' || !sessionId) {
      return { success: false, error: 'Invalid sessionId' };
    }
    if (!updates || typeof updates !== 'object') {
      return { success: false, error: 'Invalid updates' };
    }
    const u = updates as Record<string, unknown>;
    if (u.status !== undefined && !VALID_TASK_STATUSES.includes(u.status as string)) {
      return { success: false, error: `Invalid status (must be one of: ${VALID_TASK_STATUSES.join(', ')})` };
    }
    if (u.priority !== undefined && !VALID_TASK_PRIORITIES.includes(u.priority as string)) {
      return { success: false, error: `Invalid priority (must be one of: ${VALID_TASK_PRIORITIES.join(', ')})` };
    }
    if (u.assignedTo !== undefined && u.assignedTo !== null && typeof u.assignedTo !== 'string') {
      return { success: false, error: 'Invalid assignedTo' };
    }
    // 审计补充修复: 补全 title/description 类型校验（与 tasks:create 的严格校验一致）
    if (u.title !== undefined && typeof u.title !== 'string') {
      return { success: false, error: 'Invalid title (must be string)' };
    }
    if (u.description !== undefined && typeof u.description !== 'string') {
      return { success: false, error: 'Invalid description (must be string)' };
    }
    const db = sessionService.getDB();
    try {
      const fields: string[] = [];
      const values: unknown[] = [];
      if (u.title !== undefined) { fields.push('title = ?'); values.push(u.title); }
      if (u.description !== undefined) { fields.push('description = ?'); values.push(u.description); }
      if (u.status !== undefined) { fields.push('status = ?'); values.push(u.status); }
      if (u.priority !== undefined) { fields.push('priority = ?'); values.push(u.priority); }
      if (u.assignedTo !== undefined) { fields.push('assigned_to = ?'); values.push(u.assignedTo); }
      if (fields.length === 0) return { success: true };
      fields.push('updated_at = ?'); values.push(Date.now());
      if (u.status === 'completed') { fields.push('completed_at = ?'); values.push(Date.now()); }
      values.push(id, sessionId);
      // P1 修复（v0.3.13）: WHERE 补 session_id 校验，防越权修改其他会话任务
      db.prepare(`UPDATE tasks SET ${fields.join(', ')} WHERE id = ? AND session_id = ?`).run(...values);
      return { success: true };
    } catch (error) {
      return { success: false, error: (error as Error).message };
    }
  });

  ipcMain.handle('tasks:delete', async (_event, id: unknown, sessionId: unknown) => {
    // M-48 修复: 校验 id 类型
    if (typeof id !== 'string' || !id) {
      return { success: false, error: 'Invalid task id' };
    }
    // P1 修复（v0.3.13）: 补 session_id 越权保护
    if (typeof sessionId !== 'string' || !sessionId) {
      return { success: false, error: 'Invalid sessionId' };
    }
    const db = sessionService.getDB();
    try {
      // P1 修复: WHERE 补 session_id 校验
      db.prepare('DELETE FROM tasks WHERE id = ? AND session_id = ?').run(id, sessionId);
      return { success: true };
    } catch (error) {
      return { success: false, error: (error as Error).message };
    }
  });

  // ===== v0.2.0: 记忆系统增强查询（Memory Viewer UI）=====
  const VALID_MEMORY_TYPES_LIST: readonly string[] = ['episodic', 'semantic', 'working'];

  ipcMain.handle('memory:listAll', async (_event, options?: unknown) => {
    // M-49 修复: 校验 options.type 枚举和 limit 范围
    let limit = 100;
    let type: string | undefined;
    if (options && typeof options === 'object') {
      const opts = options as Record<string, unknown>;
      if (opts.type !== undefined) {
        if (typeof opts.type !== 'string' || !VALID_MEMORY_TYPES_LIST.includes(opts.type)) {
          return { success: false, error: `Invalid type (must be one of: ${VALID_MEMORY_TYPES_LIST.join(', ')})` };
        }
        type = opts.type;
      }
      if (opts.limit !== undefined) {
        const num = Number(opts.limit);
        // 限制 1-1000 范围，SQLite 中 LIMIT -1 表示无限制，需阻止
        if (!Number.isFinite(num) || num < 1 || num > 1000) {
          return { success: false, error: 'Invalid limit (must be 1-1000)' };
        }
        limit = Math.floor(num);
      }
    }
    const db = sessionService.getDB();
    const results: Record<string, unknown[]> = {};
    try {
      if (!type || type === 'episodic') {
        const rows = db.prepare('SELECT * FROM episodic_memories ORDER BY created_at DESC LIMIT ?').all(limit) as Array<Record<string, unknown>>;
        results.episodic = rows.map((r) => ({ ...r, type: 'episodic', content: r.content ?? '' }));
      }
      if (!type || type === 'semantic') {
        const rows = db.prepare('SELECT * FROM semantic_memories ORDER BY updated_at DESC LIMIT ?').all(limit) as Array<Record<string, unknown>>;
        results.semantic = rows.map((r) => ({ ...r, type: 'semantic', content: r.value ?? r.key ?? '', importance: r.confidence ?? 0, created_at: r.created_at ?? r.updated_at }));
      }
      if (!type || type === 'working') {
        const rows = db.prepare('SELECT * FROM working_memories ORDER BY updated_at DESC LIMIT ?').all(limit) as Array<Record<string, unknown>>;
        results.working = rows.map((r) => ({ ...r, type: 'working', content: r.value ?? r.key ?? '', importance: 0.5, created_at: r.updated_at ?? Date.now() }));
      }
      return { success: true, data: results };
    } catch (error) {
      return { success: false, error: (error as Error).message };
    }
  });

  ipcMain.handle('memory:delete', async (_event, type: unknown, id: unknown) => {
    // M-50 修复: 校验 type 枚举（防止三元表达式默认映射到 working_memories）和 id 类型
    if (typeof type !== 'string' || !VALID_MEMORY_TYPES_LIST.includes(type)) {
      return { success: false, error: `Invalid type (must be one of: ${VALID_MEMORY_TYPES_LIST.join(', ')})` };
    }
    if (typeof id !== 'string' || !id) {
      return { success: false, error: 'Invalid memory id' };
    }
    const db = sessionService.getDB();
    try {
      const table = type === 'episodic' ? 'episodic_memories' : type === 'semantic' ? 'semantic_memories' : 'working_memories';
      db.prepare(`DELETE FROM ${table} WHERE id = ?`).run(id);
      return { success: true };
    } catch (error) {
      return { success: false, error: (error as Error).message };
    }
  });

  log.info('[SYS] All IPC handlers registered');
}
