/**
 * Session 管理 REST 路由
 */
import { appendFileSync } from "fs";
import fs from "fs/promises";
import path from "path";
import { Hono } from "hono";
import { safeJson } from "../hono-helpers.ts";
import { t } from "../../lib/i18n.ts";
import { extractBlocks, resolveMediaGenerationBlocks } from "../block-extractors.ts";
import { buildDeferredResultInterludeBlock, resolveDeferredReceiverName } from "../deferred-result-interlude.ts";
import { BrowserManager } from "../../lib/browser/browser-manager.ts";
import { sessionIdFromFilename } from "../../lib/session-jsonl.ts";
import {
  DEFERRED_RESULT_MESSAGE_TYPE,
  DEFERRED_RESULT_RECORD_TYPE,
  buildDeferredResultRecord,
  parseDeferredResultNotification,
  parseDeferredResultRecord,
} from "../../lib/deferred-result-notification.ts";
import {
  materializeExecutorIdentity,
  readSubagentSessionMetaSync,
} from "../../lib/subagent-executor-metadata.ts";
import {
  extractTextContent,
  filterUnreferencedInlineImages,
  loadSessionHistoryMessages,
  loadLatestAssistantSummaryFromSessionFile,
  isValidSessionPath,
  isActiveDesktopSessionPath,
  isArchivedDesktopSessionPath,
} from "../../core/message-utils.ts";
import { sessionFileRevision } from "../../core/session-list-projection-cache.ts";
import {
  extractLatestTodos,
  loadLatestTodoSnapshotFromSessionFile,
} from "../../lib/tools/todo-compat.ts";
import { SessionManager } from "../../lib/pi-sdk/index.ts";
import { TODO_STATE_CUSTOM_TYPE } from "../../lib/tools/todo-constants.ts";
import { mergeWorkspaceHistory } from "../../shared/workspace-history.ts";
import {
  deleteSessionFileSidecarSync,
  moveSessionFileSidecarSync,
  sessionFileSidecarPath,
} from "../../lib/session-files/session-file-registry.ts";
import { serializeSessionFile } from "../../lib/session-files/session-file-response.ts";
import { browserScreenshotPath } from "../../lib/session-files/browser-screenshot-file.ts";
import { normalizeSessionThinkingLevel, modelSupportsXhigh } from "../../core/session-thinking-level.ts";
import {
  modelSupportsDirectAudioInput,
  modelSupportsDirectVideoInput,
  modelSupportsAudioInput,
  modelSupportsVideoInput,
  resolveModelAudioInputTransport,
  resolveModelVideoInputTransport,
} from "../../shared/model-capabilities.ts";
import { replayLatestUserTurn } from "../../core/session-turn-actions.ts";
import { createRequestContext } from "../http/boundary.ts";
import { createModuleLogger } from "../../lib/debug-log.ts";
import { searchSessions } from "../../lib/search/session-search.ts";
import { SessionSearchTokenizerUnavailableError } from "../../lib/search/session-search-tokenizer.ts";
import { MountAwareFileError, MountAwareFileService } from "../../core/mount-aware-file-service.ts";

const log = createModuleLogger("sessions");
const lifecycleLog = createModuleLogger("sessions/lifecycle");
const switchLog = createModuleLogger("sessions/switch");
const SESSION_SEARCH_QUERY_MAX_LENGTH = 512;

function rcPlatformFromSessionKey(sessionKey) {
  const match = /^([a-z]+)_/i.exec(sessionKey || "");
  return match ? match[1] : "bridge";
}

async function pathExists(filePath) {
  try {
    await fs.access(filePath);
    return true;
  } catch {
    return false;
  }
}

function completeTodoItems(todos) {
  return (Array.isArray(todos) ? todos : []).map((todo) => ({
    ...todo,
    status: "completed",
  }));
}

function getWritableSessionManager(engine, sessionPath) {
  const liveSession = engine.getSessionByPath?.(sessionPath);
  if (liveSession?.sessionManager) return liveSession.sessionManager;
  return SessionManager.open(sessionPath, path.dirname(sessionPath));
}

function authorizeSessionRoute(requestContext, capability, target) {
  if (requestContext.authPrincipal?.kind === "unknown") return { allowed: true, reason: "legacy_test_context" };
  if (typeof requestContext.authorize !== "function") return { allowed: false, reason: "missing_policy" };
  return requestContext.authorize(capability, target);
}

function resolveSessionWorkspaceSelection(engine, requestContext, body) {
  const mountId = typeof body?.workspaceMountId === "string" && body.workspaceMountId.trim()
    ? body.workspaceMountId.trim()
    : null;
  if (!mountId) {
    return {
      cwd: typeof body?.cwd === "string" && body.cwd.trim() ? body.cwd : null,
      mount: null,
    };
  }
  if (typeof body?.cwd === "string" && body.cwd.trim()) {
    throw routeError("cwd and workspaceMountId cannot be combined", "ambiguous_workspace", 400);
  }
  try {
    const files = new MountAwareFileService({
      hanakoHome: engine.hanakoHome,
      defaultRoot: engine.defaultDeskCwd || engine.homeCwd || engine.deskCwd,
      studioId: requestContext?.studioId || engine.getRuntimeContext?.()?.studioId || null,
    });
    const root = files.resolveRoot(mountId);
    return {
      cwd: files.resolveDirectory(mountId, ""),
      mount: {
        mountId: root.mountId || root.id || mountId,
        label: root.label || null,
      },
    };
  } catch (err) {
    if (err instanceof MountAwareFileError) {
      throw routeError(err.message, err.code, err.status);
    }
    throw err;
  }
}

function sessionWorkspaceMountFields(engine, sessionPath, fallback = null) {
  const mount = engine.getSessionWorkspaceMount?.(sessionPath) || fallback || null;
  if (!mount?.mountId) return {};
  return {
    workspaceMountId: mount.mountId,
    workspaceLabel: mount.label || null,
  };
}

function routeError(message, code, status) {
  const err: any = new Error(message);
  err.code = code;
  err.status = status;
  return err;
}

function classifySessionCreationError(err) {
  const message = err?.message || String(err);
  if (err?.status && Number.isInteger(err.status)) {
    return { status: err.status, body: { error: message, code: err.code || "session_create_failed" } };
  }
  if (
    /no available model/i.test(message)
    || /no available models/i.test(message)
    || /没有可用的模型/.test(message)
    || /沒有可用的模型/.test(message)
    || /利用可能なモデルがありません/.test(message)
    || /사용 가능한 모델이 없/.test(message)
  ) {
    return { status: 409, body: { error: message, code: "no_available_model" } };
  }
  return { status: 500, body: { error: message } };
}

const TODO_COMPLETE_MESSAGE =
  "[Hana Todo] The user marked the current todo list as completed and removed it from the session UI. Treat every item in that list as completed. Create a new todo list only if new work needs tracking.";

function stripInlineThinkText(text) {
  return String(text || "").replace(/<think(?:ing)?>([\s\S]*?)<\/think(?:ing)?>\n*/g, "");
}

function hasInlineImageContent(content) {
  if (!Array.isArray(content)) return false;
  return content.some(block => block?.type === "image" && (block.data || block.source?.data));
}

function hasTextBlockContent(content, { stripThink = false } = {}) {
  if (typeof content === "string") {
    const text = stripThink ? stripInlineThinkText(content) : content;
    return text.length > 0;
  }
  if (!Array.isArray(content)) return false;
  return content.some(block => block?.type === "text" && block.text);
}

function hasToolUseContent(content) {
  if (!Array.isArray(content)) return false;
  return content.some(block => (block?.type === "tool_use" || block?.type === "toolCall") && !!block.name);
}

function isDisplayableHistoryMessage(message) {
  if (!message || typeof message !== "object") return false;
  if (message.role === "user") {
    return hasTextBlockContent(message.content) || hasInlineImageContent(message.content);
  }
  if (message.role === "assistant") {
    return hasTextBlockContent(message.content, { stripThink: true }) || hasToolUseContent(message.content);
  }
  return false;
}

function resolveHistoryPageBounds(sourceMessages, { beforeId, limit, forceAll }) {
  let total = 0;
  for (const message of sourceMessages) {
    if (isDisplayableHistoryMessage(message)) total += 1;
  }
  if (forceAll) return { total, startIdx: 0, endIdx: total, hasMore: false };
  const endIdx = (beforeId != null && beforeId > 0)
    ? Math.min(beforeId, total)
    : total;
  const startIdx = Math.max(0, endIdx - limit);
  return { total, startIdx, endIdx, hasMore: startIdx > 0 };
}

/**
 * 读取会话文件的磁盘修订点（stat 签名，与 /api/sessions 列表投影同源同格式）。
 * stat 失败（请求竞态中文件被归档/删除）返回 null —— 显式的「修订点未知」，
 * 前端对 null 的策略是下次触发时重新校验，不会把差异静默吞掉。
 */
async function readSessionFileRevision(sessionPath) {
  if (!sessionPath) return null;
  try {
    return sessionFileRevision(await fs.stat(sessionPath));
  } catch {
    return null;
  }
}

export function createSessionsRoute(engine, hub = null) {
  const route = new Hono();

  // session-meta.json sidecar 按 session 目录共享；同一个 request 里遍历几十个 block
  // 时不必每个 block 都重复 readFileSync + JSON.parse。调用端构造一次 Map 当 cache。
  function createSubagentMetaCache() {
    const map = new Map();
    return (sessionPath) => {
      if (!sessionPath) return null;
      if (map.has(sessionPath)) return map.get(sessionPath);
      const meta = readSubagentSessionMetaSync(sessionPath);
      map.set(sessionPath, meta);
      return meta;
    };
  }

  function applySubagentIdentity(block, task, readSessionMeta) {
    const sessionPath = block.streamKey || task?.meta?.sessionPath || null;
    const sessionMeta = readSessionMeta(sessionPath);
    const resolved =
      materializeExecutorIdentity(sessionMeta, engine.getAgent?.bind(engine))
      || materializeExecutorIdentity(task?.meta, engine.getAgent?.bind(engine))
      || materializeExecutorIdentity(block, engine.getAgent?.bind(engine));

    if (resolved) {
      block.agentId = resolved.agentId;
      block.agentName = resolved.agentName;
      return;
    }

    const inferredAgentId = sessionPath
      ? engine.agentIdFromSessionPath?.(sessionPath) || null
      : null;
    if (!inferredAgentId) return;

    const inferredAgent = engine.getAgent?.(inferredAgentId) || null;
    block.agentId = inferredAgentId;
    block.agentName = inferredAgent?.agentName || "Unknown agent";
  }

  function patchBlockExecutorMetadata(block, task, readSessionMeta) {
    const sessionPath = block.streamKey || task?.meta?.sessionPath || null;
    const sessionMeta = readSessionMeta(sessionPath);
    const sources = [sessionMeta, task?.meta, block];

    for (const source of sources) {
      if (!source) continue;
      if (source.executorAgentId && !block.executorAgentId) {
        block.executorAgentId = source.executorAgentId;
      }
      if (source.executorAgentNameSnapshot && !block.executorAgentNameSnapshot) {
        block.executorAgentNameSnapshot = source.executorAgentNameSnapshot;
      }
      if (source.executorMetaVersion && !block.executorMetaVersion) {
        block.executorMetaVersion = source.executorMetaVersion;
      }
    }
  }

  function patchBlockRequestedMetadata(block, task = null) {
    const sources = [task?.meta, block];

    for (const source of sources) {
      if (!source) continue;
      if (source.requestedAgentId && !block.requestedAgentId) {
        block.requestedAgentId = source.requestedAgentId;
      }
      if (source.requestedAgentNameSnapshot && !block.requestedAgentName) {
        block.requestedAgentName = source.requestedAgentNameSnapshot;
      }
    }
  }

  function taskFromSubagentRun(run) {
    if (!run) return null;
    return {
      status: run.status,
      result: run.summary || null,
      reason: run.reason || run.summary || null,
      meta: {
        sessionPath: run.childSessionPath || null,
        requestedAgentId: run.requestedAgentId || null,
        requestedAgentNameSnapshot: run.requestedAgentNameSnapshot || null,
        executorAgentId: run.executorAgentId || null,
        executorAgentNameSnapshot: run.executorAgentNameSnapshot || null,
        executorMetaVersion: run.executorMetaVersion || null,
      },
    };
  }

  function mergeSubagentTaskMetadata(primary, fallback) {
    if (!primary) return fallback || null;
    if (!fallback) return primary;
    const primaryMeta = {};
    for (const [key, value] of Object.entries(primary.meta || {})) {
      if (value != null) primaryMeta[key] = value;
    }
    return {
      status: primary.status || fallback.status,
      result: primary.result ?? fallback.result,
      reason: primary.reason ?? fallback.reason,
      meta: {
        ...(fallback.meta || {}),
        ...primaryMeta,
      },
    };
  }

  function createSubagentSummaryCache() {
    const map = new Map();
    return async (sessionPath) => {
      if (!sessionPath) return null;
      if (!map.has(sessionPath)) {
        map.set(sessionPath, loadLatestAssistantSummaryFromSessionFile(sessionPath));
      }
      return await map.get(sessionPath);
    };
  }

  function getSessionSummaryRecord(sessionPath, agentIdHint = null) {
    if (!sessionPath) return null;
    const agentId = agentIdHint || engine.agentIdFromSessionPath?.(sessionPath) || null;
    if (!agentId) return null;
    const agent = engine.getAgent?.(agentId) || null;
    const summaryManager = agent?.summaryManager || null;
    if (!summaryManager || typeof summaryManager.getSummary !== "function") return null;

    const sessionId = sessionIdFromFilename(path.basename(sessionPath));
    const record = summaryManager.getSummary(sessionId);
    return record?.summary?.trim() ? record : null;
  }

  function serializeSessionSummaryRecord(record) {
    return {
      hasSummary: !!record,
      summary: record?.summary || null,
      createdAt: record?.created_at || null,
      updatedAt: record?.updated_at || null,
    };
  }

  function invalidateRcTarget(sessionPath) {
    const rcState = engine.rcState;
    if (!rcState?.invalidateDesktopSession) return;

    const { detachedAttachments } = rcState.invalidateDesktopSession(sessionPath);
    for (const attachment of detachedAttachments) {
      try {
        engine.emitEvent?.({
          type: "bridge_rc_detached",
          sessionKey: attachment.sessionKey,
          sessionPath: attachment.desktopSessionPath,
        }, attachment.desktopSessionPath);
      } catch {}
    }
  }

  function archivedPathForActiveSession(sessionPath) {
    return path.join(path.dirname(sessionPath), "archived", path.basename(sessionPath));
  }

  function activePathForArchivedSession(sessionPath) {
    return path.join(path.dirname(path.dirname(sessionPath)), path.basename(sessionPath));
  }

  function uniqueLifecyclePaths(paths) {
    return [...new Set((paths || []).filter((p) => typeof p === "string" && p.trim()))];
  }

  async function cleanupSessionLifecycle(sessionPaths, reason) {
    const bm = BrowserManager.instance();
    for (const sessionPath of uniqueLifecyclePaths(sessionPaths)) {
      try {
        engine.taskRegistry?.abortByParentSession?.(sessionPath, reason);
      } catch (err) {
        lifecycleLog.warn(`task cleanup failed for ${sessionPath}: ${err.message}`);
      }
      try {
        engine.subagentRuns?.abortByParentSession?.(sessionPath, reason);
      } catch (err) {
        lifecycleLog.warn(`subagent run cleanup failed for ${sessionPath}: ${err.message}`);
      }
      try {
        engine.subagentThreads?.removeBySession?.(sessionPath);
      } catch (err) {
        lifecycleLog.warn(`subagent thread cleanup failed for ${sessionPath}: ${err.message}`);
      }
      try {
        // 右侧 workflow 卡活动随对话退场（内存 + 持久化背书一并清，按 sessionPath 归属）。
        engine.activityHub?.clearBySession?.(sessionPath);
      } catch (err) {
        lifecycleLog.warn(`activity hub cleanup failed for ${sessionPath}: ${err.message}`);
      }
      try {
        engine.deferredResults?.suppressBySession?.(sessionPath, reason);
      } catch (err) {
        lifecycleLog.warn(`deferred cleanup failed for ${sessionPath}: ${err.message}`);
      }
      try {
        engine.confirmStore?.abortBySession?.(sessionPath);
      } catch (err) {
        lifecycleLog.warn(`confirm cleanup failed for ${sessionPath}: ${err.message}`);
      }
      try {
        if (typeof engine.discardSessionRuntime === "function") {
          await engine.discardSessionRuntime(sessionPath, reason);
        } else {
          await engine.abortSessionByPath?.(sessionPath);
        }
      } catch (err) {
        lifecycleLog.warn(`session runtime cleanup failed for ${sessionPath}: ${err.message}`);
      }
      try {
        await bm.closeBrowserForSession(sessionPath);
      } catch (err) {
        lifecycleLog.warn(`browser cleanup failed for ${sessionPath}: ${err.message}`);
      }
      try {
        engine.terminalSessions?.closeForSession?.(sessionPath);
      } catch (err) {
        lifecycleLog.warn(`terminal cleanup failed for ${sessionPath}: ${err.message}`);
      }
      invalidateRcTarget(sessionPath);
    }
  }

  function isDeletedAgentSessionPath(sessionPath) {
    if (!sessionPath) return false;
    const agentId = engine.agentIdFromSessionPath?.(sessionPath) || null;
    return !!agentId && engine.isAgentDeleted?.(agentId) === true;
  }

  function rejectDeletedAgentSession(c) {
    return c.json({ error: "agent_deleted", reason: "agent_deleted" }, 409);
  }

  function sessionFolderScopeResponse(scope) {
    return {
      ok: true,
      sessionPath: scope?.sessionPath || null,
      cwd: scope?.cwd || null,
      workspaceFolders: Array.isArray(scope?.workspaceFolders) ? scope.workspaceFolders : [],
      authorizedFolders: Array.isArray(scope?.authorizedFolders) ? scope.authorizedFolders : [],
      sandboxFolders: Array.isArray(scope?.sandboxFolders) ? scope.sandboxFolders : [],
    };
  }

  async function validateAuthorizedFolder(rawFolder) {
    if (typeof rawFolder !== "string" || !rawFolder.trim()) {
      throw new Error("folder is required");
    }
    const folder = path.resolve(rawFolder.trim());
    let stat;
    try {
      stat = await fs.stat(folder);
    } catch {
      throw new Error("folder does not exist");
    }
    if (!stat.isDirectory()) {
      throw new Error("folder must be a directory");
    }
    return folder;
  }

  function normalizeAuthorizedFolderPath(rawFolder) {
    if (typeof rawFolder !== "string" || !rawFolder.trim()) {
      throw new Error("folder is required");
    }
    return path.resolve(rawFolder.trim());
  }

  // 列出所有 agent 的历史 session
  route.get("/sessions", async (c) => {
    try {
      const requestContext = createRequestContext(c, engine);
      const auth = authorizeSessionRoute(requestContext, "sessions.read", {
        kind: "studio",
        studioId: requestContext.studioId,
      });
      if (!auth.allowed) return c.json({ error: "insufficient_scope", reason: auth.reason }, 403);
      const runtimeStudioId = requestContext.runtimeContext?.studioId || null;
      const principalStudioId = requestContext.authPrincipal?.studioId || null;
      // Same-Studio projection v0: paired clients may see the legacy session store
      // only when their authenticated Studio is the server's current Studio.
      if (runtimeStudioId && principalStudioId && runtimeStudioId !== principalStudioId) {
        return c.json({
          error: "studio_scope_mismatch",
          detail: "authenticated Studio does not match this server Studio",
        }, 403);
      }
      const sessions = await engine.listSessions();
      const attachments = engine.rcState?.listAttachments?.() || [];
      const rcAttachmentByPath = new Map(attachments.map((attachment) => [
        attachment.desktopSessionPath,
        {
          sessionKey: attachment.sessionKey,
          platform: rcPlatformFromSessionKey(attachment.sessionKey),
        },
      ]));
      return c.json(sessions.map(s => {
        const summaryRecord = getSessionSummaryRecord(s.path, s.agentId || null);
        return ({
          path: s.path,
          title: s.title || null,
          firstMessage: (s.firstMessage || "").slice(0, 100),
          modified: s.modified?.toISOString() || null,
          // 磁盘修订点（stat 签名）。web/mobile 端用它对比已缓存会话内容，
          // 决定是否补拉 /rc 接管等离线窗口写入的消息（issue #1610）。
          revision: typeof s.revision === "string" ? s.revision : null,
          messageCount: s.messageCount || 0,
          cwd: s.cwd || null,
          agentId: s.agentId || null,
          agentName: s.agentName || null,
          projectId: s.projectId || null,
          modelId: s.modelId || null,
          modelProvider: s.modelProvider || null,
          workspaceMountId: s.workspaceMountId || null,
          workspaceLabel: s.workspaceLabel || null,
          permissionMode: s.permissionMode || (typeof engine.getSessionPermissionMode === "function"
            ? engine.getSessionPermissionMode(s.path)
            : engine.permissionMode || null),
          pinnedAt: s.pinnedAt || null,
          agentDeleted: s.agentDeleted === true,
          readOnlyReason: s.readOnlyReason || (s.agentDeleted === true ? "agent_deleted" : null),
          continuationAvailable: s.continuationAvailable === true,
          deletedAt: s.deletedAt || null,
          hasSummary: !!summaryRecord,
          rcAttachment: rcAttachmentByPath.get(s.path)
            ? {
              ...(rcAttachmentByPath.get(s.path) as any),
              title: s.title || null,
            }
            : null,
        });
      }));
    } catch (err) {
      return c.json({ error: err.message }, err.status || 500);
    }
  });

  route.get("/sessions/search", async (c) => {
    try {
      const requestContext = createRequestContext(c, engine);
      const auth = authorizeSessionRoute(requestContext, "sessions.read", {
        kind: "studio",
        studioId: requestContext.studioId,
      });
      if (!auth.allowed) return c.json({ error: "insufficient_scope", reason: auth.reason }, 403);
      const runtimeStudioId = requestContext.runtimeContext?.studioId || null;
      const principalStudioId = requestContext.authPrincipal?.studioId || null;
      if (runtimeStudioId && principalStudioId && runtimeStudioId !== principalStudioId) {
        return c.json({
          error: "studio_scope_mismatch",
          detail: "authenticated Studio does not match this server Studio",
        }, 403);
      }

      const query = c.req.query("q") || "";
      const phase = c.req.query("phase") === "content" ? "content" : "title";
      const limit = c.req.query("limit") ? Number(c.req.query("limit")) : undefined;
      const trimmedQuery = query.trim();
      if (!trimmedQuery) return c.json({ query, phase, results: [] });
      if ([...trimmedQuery].length > SESSION_SEARCH_QUERY_MAX_LENGTH) {
        return c.json({
          error: "query_too_long",
          maxLength: SESSION_SEARCH_QUERY_MAX_LENGTH,
        }, 400);
      }

      const sessions = await engine.listSessions();
      const results = searchSessions(sessions, trimmedQuery, { phase, limit }).map((s) => ({
        path: s.path,
        title: s.title || null,
        firstMessage: (s.firstMessage || "").slice(0, 100),
        modified: s.modified?.toISOString?.() || s.modified || null,
        messageCount: s.messageCount || 0,
        cwd: s.cwd || null,
        agentId: s.agentId || null,
        agentName: s.agentName || null,
        projectId: s.projectId || null,
        modelId: s.modelId || null,
        modelProvider: s.modelProvider || null,
        workspaceMountId: s.workspaceMountId || null,
        workspaceLabel: s.workspaceLabel || null,
        pinnedAt: s.pinnedAt || null,
        agentDeleted: s.agentDeleted === true,
        readOnlyReason: s.readOnlyReason || (s.agentDeleted === true ? "agent_deleted" : null),
        continuationAvailable: s.continuationAvailable === true,
        deletedAt: s.deletedAt || null,
        matchKind: s.matchKind,
        snippet: s.snippet || "",
        score: s.score,
      }));
      return c.json({ query, phase, results });
    } catch (err) {
      if (err instanceof SessionSearchTokenizerUnavailableError) {
        log.error(`session search tokenizer unavailable: ${err.cause || err}`);
        return c.json({ error: err.message }, 503);
      }
      return c.json({ error: err.message }, 500);
    }
  });

  // 获取单个 session 的滚动摘要。列表只暴露 hasSummary，正文按需读取。
  route.get("/sessions/summary", async (c) => {
    try {
      const requestContext = createRequestContext(c, engine);
      const sessionPath = c.req.query("path") || null;
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      if (!isValidSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      const auth = authorizeSessionRoute(requestContext, "sessions.read", {
        kind: "session",
        studioId: requestContext.studioId,
        sessionPath,
      });
      if (!auth.allowed) return c.json({ error: "insufficient_scope", reason: auth.reason }, 403);

      const record = getSessionSummaryRecord(sessionPath);
      return c.json(serializeSessionSummaryRecord(record));
    } catch (err) {
      return c.json({ error: err.message }, err.status || 500);
    }
  });

  // 置顶 / 取消置顶 session
  route.post("/sessions/pin", async (c) => {
    try {
      const requestContext = createRequestContext(c, engine);
      const body = await safeJson(c);
      const { path: sessionPath, pinned } = body;
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      if (typeof pinned !== "boolean") {
        return c.json({ error: t("error.missingParam", { param: "pinned" }) }, 400);
      }
      if (!isValidSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      if (isDeletedAgentSessionPath(sessionPath)) {
        return rejectDeletedAgentSession(c);
      }
      const auth = authorizeSessionRoute(requestContext, "sessions.write", {
        kind: "session",
        studioId: requestContext.studioId,
        sessionPath,
      });
      if (!auth.allowed) return c.json({ error: "insufficient_scope", reason: auth.reason }, 403);
      const pinnedAt = await engine.setSessionPinned(sessionPath, pinned);
      return c.json({ ok: true, pinnedAt });
    } catch (err) {
      return c.json({ error: err.message, code: err.code || undefined }, err.status || 500);
    }
  });

  route.get("/sessions/authorized-folders", async (c) => {
    try {
      const requestContext = createRequestContext(c, engine);
      const sessionPath = c.req.query("path") || engine.currentSessionPath || null;
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      const auth = authorizeSessionRoute(requestContext, "sessions.read", {
        kind: "session",
        studioId: requestContext.studioId,
        sessionPath,
      });
      if (!auth.allowed) return c.json({ error: "insufficient_scope", reason: auth.reason }, 403);
      if (!isActiveDesktopSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      if (isDeletedAgentSessionPath(sessionPath)) {
        return rejectDeletedAgentSession(c);
      }
      if (!(await pathExists(sessionPath))) {
        return c.json({ error: "session not found" }, 404);
      }
      return c.json(sessionFolderScopeResponse(engine.getSessionFolderScope?.(sessionPath)));
    } catch (err) {
      return c.json({ error: err.message, code: err.code || undefined }, err.status || 500);
    }
  });

  route.patch("/sessions/authorized-folders", async (c) => {
    try {
      const requestContext = createRequestContext(c, engine);
      const body = await safeJson(c);
      const sessionPath = body?.path || body?.sessionPath || null;
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      const auth = authorizeSessionRoute(requestContext, "sessions.write", {
        kind: "session",
        studioId: requestContext.studioId,
        sessionPath,
      });
      if (!auth.allowed) return c.json({ error: "insufficient_scope", reason: auth.reason }, 403);
      if (!isActiveDesktopSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      if (isDeletedAgentSessionPath(sessionPath)) {
        return rejectDeletedAgentSession(c);
      }
      if (!(await pathExists(sessionPath))) {
        return c.json({ error: "session not found" }, 404);
      }

      const action = typeof body?.action === "string" ? body.action.trim() : "set";
      let scope;
      if (action === "add") {
        const folder = await validateAuthorizedFolder(body?.folder);
        scope = await engine.addSessionAuthorizedFolder?.(sessionPath, folder);
      } else if (action === "remove") {
        const folder = normalizeAuthorizedFolderPath(body?.folder);
        scope = await engine.removeSessionAuthorizedFolder?.(sessionPath, folder);
      } else if (action === "set") {
        const folders = Array.isArray(body?.folders) ? body.folders : [];
        const normalizedFolders = [];
        for (const folder of folders) {
          normalizedFolders.push(await validateAuthorizedFolder(folder));
        }
        scope = await engine.setSessionAuthorizedFolders?.(sessionPath, normalizedFolders);
      } else {
        return c.json({ error: "Invalid action" }, 400);
      }
      return c.json(sessionFolderScopeResponse(scope || engine.getSessionFolderScope?.(sessionPath)));
    } catch (err) {
      const message = err.message || String(err);
      if (/folder (is required|does not exist|must be a directory)/.test(message)) {
        return c.json({ error: message }, 400);
      }
      return c.json({ error: message }, 500);
    }
  });

  // 获取 session 的消息（支持 ?path= 指定 session，否则读焦点 session）
  route.get("/sessions/messages", async (c) => {
    try {
      const requestContext = createRequestContext(c, engine);
      const queryPath = c.req.query("path") || null;
      if (queryPath && !isValidSessionPath(queryPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      const auth = authorizeSessionRoute(requestContext, "sessions.read", {
        kind: "session",
        studioId: requestContext.studioId,
        sessionPath: queryPath || engine.currentSessionPath || null,
      });
      if (!auth.allowed) return c.json({ error: "insufficient_scope", reason: auth.reason }, 403);
      const resolvedSessionPath = queryPath || engine.currentSessionPath || null;
      // 修订点必须在读取内容之前取：读取期间若有新写入，revision 只会偏旧
      // （前端下次触发时多补拉一次，方向安全），不会偏新（把没读到的写入
      // 标成「已同步」会让 /rc 消息永久漏掉，issue #1610 的反方向竞态）。
      const revision = await readSessionFileRevision(resolvedSessionPath);
      const sourceMessages = await loadSessionHistoryMessages(engine, resolvedSessionPath);

      // 分页参数
      const beforeId = c.req.query("before") != null ? Number(c.req.query("before")) : null;
      const limit = Math.min(Number(c.req.query("limit")) || 50, 200);

      // all=1 强制全量返回（流式恢复等特殊场景）
      const forceAll = c.req.query("all") === "1";
      const pageBounds = resolveHistoryPageBounds(sourceMessages, { beforeId, limit, forceAll });

      // 提取可显示的消息（user/assistant 文本 + 文件/artifact 工具结果）。
      // 长会话只完整 hydrate 当前页面窗口；窗口外只做轻量可见性扫描，
      // 避免旧消息的 markdown/block/sidecar 解析拖慢当前模型运行。
      const messages = [];
      const blocks = [];
      const mediaGenerationResults = new Map();
      const standaloneMediaGenerationResults = [];
      const deferredInterludeTaskIds = new Set();
      const deferredStore = engine.deferredResults;
      const receiverName = resolveDeferredReceiverName(engine, resolvedSessionPath);
      const recordMediaGenerationResult = (parsed, afterIndex) => {
        if (!parsed?.taskId || !isMediaGenerationDeferredResult(parsed)) return;
        mediaGenerationResults.set(parsed.taskId, parsed);
        if (parsed.status === "success") {
          standaloneMediaGenerationResults.push({
            ...parsed,
            afterIndex,
          });
        }
      };
      const recordDeferredInterlude = (parsed, afterIndex) => {
        if (!parsed?.taskId || afterIndex < 0 || deferredInterludeTaskIds.has(parsed.taskId)) return;
        const task = deferredStore?.query?.(parsed.taskId) || null;
        const run = engine.subagentRuns?.query?.(parsed.taskId) || null;
        const runTask = taskFromSubagentRun(run);
        const metadataTask = mergeSubagentTaskMetadata(runTask, task);
        const metadataMeta = metadataTask?.meta || {};
        const meta = {
          ...metadataMeta,
          type: parsed.type || metadataMeta.type || task?.meta?.type || "background-task",
        };
        const event = {
          taskId: parsed.taskId,
          status: parsed.status === "failed" || parsed.status === "aborted" ? parsed.status : "success",
          result: Object.prototype.hasOwnProperty.call(parsed, "result") ? parsed.result : metadataTask?.result,
          reason: parsed.reason || metadataTask?.reason || null,
          meta,
        };
        if (!meta.interlude) return;
        const block = buildDeferredResultInterludeBlock(event, { receiverName });
        if (!block) return;
        blocks.push({ ...block, afterIndex });
        deferredInterludeTaskIds.add(parsed.taskId);
      };
      let displayIdx = 0;

      for (const m of sourceMessages) {
        if (m.role === "user") {
          if (!isDisplayableHistoryMessage(m)) continue;
          const currentIndex = displayIdx;
          displayIdx += 1;
          if (currentIndex >= pageBounds.startIdx && currentIndex < pageBounds.endIdx) {
            const { text, images } = extractTextContent(m.content);
            const visibleImages = filterUnreferencedInlineImages(text, images);
            messages.push({
              id: String(currentIndex),
              ...(m.id ? { entryId: m.id } : {}),
              role: "user",
              content: text,
              images: visibleImages.length ? visibleImages : undefined,
              ...(m.timestamp ? { timestamp: m.timestamp } : {}),
            });
          }
        } else if (m.role === "assistant") {
          if (!isDisplayableHistoryMessage(m)) continue;
          const currentIndex = displayIdx;
          displayIdx += 1;
          if (currentIndex >= pageBounds.startIdx && currentIndex < pageBounds.endIdx) {
            const { text, thinking, toolUses } = extractTextContent(m.content, { stripThink: true });
            messages.push({
              id: String(currentIndex),
              ...(m.id ? { entryId: m.id } : {}),
              role: "assistant",
              content: text,
              thinking: thinking || undefined,
              toolCalls: toolUses.length ? toolUses : undefined,
              ...(m.timestamp ? { timestamp: m.timestamp } : {}),
            });
          }
        } else if (m.role === "toolResult") {
          const afterIndex = displayIdx - 1;
          if (afterIndex >= pageBounds.startIdx && afterIndex < pageBounds.endIdx) {
            const extracted = extractBlocks(m.toolName, m.details, m);
            for (const b of extracted) {
              blocks.push({ ...b, afterIndex });
            }
          }
        } else if (m.role === "custom") {
          const afterIndex = displayIdx - 1;
          if (m.display !== false && afterIndex >= pageBounds.startIdx && afterIndex < pageBounds.endIdx) {
            const extracted = extractBlocks(m.customType, m.details, m);
            for (const b of extracted) {
              blocks.push({ ...b, afterIndex });
            }
          }
          const parsed = parseHistoryDeferredResult(m);
          recordMediaGenerationResult(parsed, afterIndex);
          recordDeferredInterlude(parsed, afterIndex);
        }
      }

      if (resolvedSessionPath && typeof deferredStore?.listBySession === "function") {
        for (const task of deferredStore.listBySession(resolvedSessionPath)) {
          if (!isTerminalDeferredTask(task)) continue;
          const parsed = buildDeferredResultRecord(task.taskId, task);
          recordMediaGenerationResult(parsed, pageBounds.total - 1);
          recordDeferredInterlude(parsed, pageBounds.total - 1);
        }
      }
      const resolvedBlocks = resolveMediaGenerationBlocks(
        blocks,
        mediaGenerationResults,
        standaloneMediaGenerationResults,
      );

      // 重映射 afterIndex 到切片内偏移，过滤超出范围的
      const slicedBlocks = forceAll
        ? resolvedBlocks
        : resolvedBlocks
          .filter(b => b.afterIndex >= pageBounds.startIdx && b.afterIndex < pageBounds.endIdx)
          .map(b => ({ ...b, afterIndex: b.afterIndex - pageBounds.startIdx }));
      const hasMore = pageBounds.hasMore;

      // 修正 subagent blocks 的状态：优先从 durable run registry 读长期映射，
      // 再用 deferred store 作为实时投递队列。deferred 会清理，不再承担历史事实源。
      {
        const deferredStore = engine.deferredResults;
        const runStore = engine.subagentRuns;
        const readSessionMeta = createSubagentMetaCache();
        const readSessionSummary = createSubagentSummaryCache();
        for (const b of slicedBlocks) {
          if (b.type !== "subagent" || !b.taskId) continue;
          const task = deferredStore?.query?.(b.taskId) || null;
          const run = runStore?.query?.(b.taskId) || null;
          const runTask = taskFromSubagentRun(run);
          const metadataTask = mergeSubagentTaskMetadata(runTask, task);
          const durableSessionPath = run?.childSessionPath || null;
          const deferredSessionPath = task?.meta?.sessionPath || null;
          if (!b.streamKey && durableSessionPath) b.streamKey = durableSessionPath;
          if (!b.streamKey && deferredSessionPath) b.streamKey = deferredSessionPath;
          patchBlockRequestedMetadata(b, metadataTask);
          patchBlockExecutorMetadata(b, metadataTask, readSessionMeta);
          applySubagentIdentity(b, metadataTask, readSessionMeta);

          if (b.streamStatus !== "running") continue;

          const terminalTask = run && run.status !== "pending" ? runTask : task;

          // subagent 完成状态只能由 durable run registry 或 deferred store 的任务终态确认。
          // 子 session 可能有多轮输出，尾部 assistant 文本只能作为 resolved 后的摘要来源。
          if (terminalTask?.status === "aborted") {
            b.streamStatus = "aborted";
            b.summary = terminalTask.reason || "aborted";
            if (terminalTask.meta?.sessionPath) b.streamKey = terminalTask.meta.sessionPath;
            patchBlockRequestedMetadata(b, terminalTask);
            patchBlockExecutorMetadata(b, terminalTask, readSessionMeta);
            applySubagentIdentity(b, terminalTask, readSessionMeta);
            continue;
          }
          if (terminalTask?.status === "failed") {
            b.streamStatus = "failed";
            b.summary = terminalTask.reason || "failed";
            if (terminalTask.meta?.sessionPath) b.streamKey = terminalTask.meta.sessionPath;
            patchBlockRequestedMetadata(b, terminalTask);
            patchBlockExecutorMetadata(b, terminalTask, readSessionMeta);
            applySubagentIdentity(b, terminalTask, readSessionMeta);
            continue;
          }
          if (terminalTask?.status === "resolved") {
            b.streamStatus = "done";
            if (terminalTask.meta?.sessionPath) b.streamKey = terminalTask.meta.sessionPath;
            patchBlockRequestedMetadata(b, terminalTask);
            patchBlockExecutorMetadata(b, terminalTask, readSessionMeta);
            applySubagentIdentity(b, terminalTask, readSessionMeta);

            const sp = b.streamKey || terminalTask.meta?.sessionPath || null;
            const summary = await readSessionSummary(sp);
            b.summary = summary || (typeof terminalTask.result === "string" ? terminalTask.result.slice(0, 200) : b.summary);
            continue;
          }

          if (run?.status === "pending" && !task) {
            b.streamStatus = "failed";
            b.summary = t("session.subagentRunStateUnrecoverable");
            continue;
          }

          if (!b.streamKey && !run && !task) {
            b.streamStatus = "failed";
            b.summary = t("session.subagentLinkUnrecoverable");
          }
        }
      }

      // workflow inline 概览块回填：block_update patch 是前端瞬时事件、未持久化进 toolResult details，
      // 重启后块保留派单时的 streamStatus:"running" + startedAt，会显示离谱「已运行 Xm」时长。
      // 从 durable runStore 读终态修正，并用 completedAt 补 finishedAt（inline 卡算总时长用）。
      {
        const wfRunStore = engine.subagentRuns;
        const wfDeferredStore = engine.deferredResults;
        for (const b of slicedBlocks) {
          if (b.type !== "workflow" || !b.taskId) continue;
          if (b.streamStatus !== "running") continue;
          const run = wfRunStore?.query?.(b.taskId) || null;
          const task = wfDeferredStore?.query?.(b.taskId) || null;
          const status = run?.status || task?.status || null;
          if (status === "resolved" || status === "done") b.streamStatus = "done";
          else if (status === "failed") b.streamStatus = "failed";
          else if (status === "aborted") b.streamStatus = "aborted";
          else continue; // 仍 pending / 无记录：保持 running，不误判完成
          if (!b.finishedAt && run?.completedAt) {
            const ts = Date.parse(run.completedAt);
            if (Number.isFinite(ts)) b.finishedAt = ts;
          }
          if (!b.summary && typeof run?.summary === "string") b.summary = run.summary;
        }
      }

      patchSessionFileLifecycleBlocks(slicedBlocks, engine, resolvedSessionPath);
      const sessionFiles = listSessionRegistryFiles(engine, resolvedSessionPath);

      // 从历史中提取最新 todo 状态：branch-aware，沿当前 leaf 回溯到 root，
      // 只在当前分支路径上找最新合法快照。避免从抛弃的分支取到错误状态。
      const todos = extractLatestTodos(sourceMessages);

      // 重启后右侧 workflow 卡复原：ActivityHub 已从持久化背书回灌该会话的 workflow 活动，
      // 这里在「首屏载入」（非翻页）时重发一遍，让前端 agent-activity slice 重新填充。
      // 翻页（beforeId != null）不重发，避免重复广播。WS 是全局广播、前端按 sessionPath 入库。
      if (beforeId == null && resolvedSessionPath) {
        engine.activityHub?.rebroadcastSession?.(resolvedSessionPath);
      }

      return c.json({ messages, blocks: slicedBlocks, todos, hasMore, sessionFiles, revision });
    } catch (err) {
      return c.json({ error: err.message }, 500);
    }
  });

  route.post("/sessions/latest-user-message/replay", async (c) => {
    try {
      const requestContext = createRequestContext(c, engine);
      const body = await safeJson(c);
      const sessionPath = body?.path || body?.sessionPath || null;
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      const auth = authorizeSessionRoute(requestContext, "sessions.write", {
        kind: "session",
        studioId: requestContext.studioId,
        sessionPath,
      });
      if (!auth.allowed) return c.json({ error: "insufficient_scope", reason: auth.reason }, 403);
      if (!isActiveDesktopSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      if (isDeletedAgentSessionPath(sessionPath)) {
        return rejectDeletedAgentSession(c);
      }
      if (!(await pathExists(sessionPath))) {
        return c.json({ error: "session not found" }, 404);
      }
      if (engine.isSessionStreaming?.(sessionPath)) {
        return c.json({ error: "session_busy" }, 409);
      }

      const result = await replayLatestUserTurn(engine, {
        sessionPath,
        sourceEntryId: body.sourceEntryId || null,
        clientMessageId: body.clientMessageId || null,
        replacementText: typeof body.text === "string" ? body.text : undefined,
        displayMessage: body.displayMessage || null,
        uiContext: body.uiContext ?? null,
      });
      return c.json({ ok: true, ...result });
    } catch (err) {
      const status = err.message === "session_busy" ? 409 : 400;
      return c.json({ error: err.message }, status);
    }
  });

  route.post("/sessions/todos/complete", async (c) => {
    try {
      const requestContext = createRequestContext(c, engine);
      const body = await safeJson(c);
      const sessionPath = body?.path;
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      const auth = authorizeSessionRoute(requestContext, "sessions.write", {
        kind: "session",
        studioId: requestContext.studioId,
        sessionPath,
      });
      if (!auth.allowed) return c.json({ error: "insufficient_scope", reason: auth.reason }, 403);
      if (!isActiveDesktopSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      if (isDeletedAgentSessionPath(sessionPath)) {
        return rejectDeletedAgentSession(c);
      }
      try {
        await fs.access(sessionPath);
      } catch {
        return c.json({ error: t("error.sessionNotFound") }, 404);
      }
      if (engine.isSessionStreaming?.(sessionPath)) {
        return c.json({ error: "Cannot complete todos while session is streaming" }, 409);
      }

      const snapshot = await loadLatestTodoSnapshotFromSessionFile(sessionPath);
      const completedTodos = completeTodoItems(snapshot?.todos || []);
      if (!snapshot?.removed && completedTodos.length > 0) {
        const manager = getWritableSessionManager(engine, sessionPath);
        manager.appendCustomMessageEntry(
          TODO_STATE_CUSTOM_TYPE,
          TODO_COMPLETE_MESSAGE,
          false,
          {
            action: "complete_all",
            source: "user",
            removed: true,
            todos: completedTodos,
          },
        );
      }

      engine.emitEvent?.({ type: "todo_update", todos: [] }, sessionPath);
      return c.json({ ok: true, todos: [] });
    } catch (err) {
      return c.json({ error: err.message }, 500);
    }
  });

  // 新建 session（可选指定工作目录和 agentId）
  route.post("/sessions/new", async (c) => {
    try {
      const requestContext = createRequestContext(c, engine);
      const auth = authorizeSessionRoute(requestContext, "sessions.write", {
        kind: "studio",
        studioId: requestContext.studioId,
      });
      if (!auth.allowed) return c.json({ error: "insufficient_scope", reason: auth.reason }, 403);
      const body = await safeJson(c);
      const { memoryEnabled, agentId, currentSessionPath: oldSessionPath, thinkingLevel } = body;
      const workspaceSelection = resolveSessionWorkspaceSelection(engine, requestContext, body);
      const cwd = workspaceSelection.cwd;
      const workspaceFolders = Array.isArray(body.workspaceFolders)
        ? body.workspaceFolders.filter(p => typeof p === "string" && p.trim())
        : [];
      const projectId = Object.prototype.hasOwnProperty.call(body, "projectId")
        ? (
            typeof engine.normalizeSessionProjectAssignmentId === "function"
              ? engine.normalizeSessionProjectAssignmentId(body.projectId)
              : (typeof body.projectId === "string" && body.projectId.trim() ? body.projectId.trim() : null)
          )
        : null;
      const memFlag = memoryEnabled !== false; // 默认 true
      log.log(`新建 session ${JSON.stringify({
        hasCwd: !!cwd,
        memoryEnabled: memFlag,
        customAgent: !!agentId,
      })}`);

      // 新建前挂起浏览器（保存当前 session 的浏览器状态）
      const bm = BrowserManager.instance();
      if (oldSessionPath && bm.isRunning(oldSessionPath)) {
        await bm.suspendForSession(oldSessionPath);
      }

      const createOptions: {
        workspaceFolders: any;
        visibleInSessionList: boolean;
        thinkingLevel?: any;
        workspaceMountId?: string;
        workspaceLabel?: string | null;
      } = { workspaceFolders, visibleInSessionList: true };
      if (thinkingLevel !== undefined && thinkingLevel !== null) {
        createOptions.thinkingLevel = thinkingLevel;
      }
      if (workspaceSelection.mount?.mountId) {
        createOptions.workspaceMountId = workspaceSelection.mount.mountId;
        createOptions.workspaceLabel = workspaceSelection.mount.label || null;
      }
      let newSessionPath, newAgentId;
      if (agentId && agentId !== (body.currentAgentId || engine.currentAgentId)) {
        ({ sessionPath: newSessionPath, agentId: newAgentId } = await engine.createSessionForAgent(
          agentId,
          cwd || undefined,
          memFlag,
          undefined,
          createOptions,
        ));
      } else {
        ({ sessionPath: newSessionPath, agentId: newAgentId } = await engine.createSession(
          null,
          cwd || undefined,
          memFlag,
          undefined,
          createOptions,
        ));
      }
      engine.persistSessionMeta();
      if (projectId && typeof engine.setSessionProjectAssignment === "function") {
        await engine.setSessionProjectAssignment({ sessionPath: newSessionPath, projectId });
      }

      // 记住工作目录 + 更新历史
      if (cwd) {
        const history = mergeWorkspaceHistory(engine.config.cwd_history, [cwd]);
        await engine.updateConfig({ last_cwd: cwd, cwd_history: history });
      }

      log.log("session 创建完成");
      const response = {
        ok: true,
        path: newSessionPath,
        cwd: engine.cwd,
        workspaceFolders: engine.getSessionWorkspaceFolders?.(newSessionPath) || [],
        authorizedFolders: engine.getSessionAuthorizedFolders?.(newSessionPath) || [],
        agentId: newAgentId,
        agentName: engine.getAgent(newAgentId)?.agentName || engine.agentName,
        projectId,
        planMode: engine.planMode,
        permissionMode: engine.permissionMode,
        accessMode: engine.accessMode,
        thinkingLevel: normalizeSessionThinkingLevel(engine.getSessionThinkingLevel?.(newSessionPath) || engine.getThinkingLevel?.()),
        memoryModelUnavailableReason: engine.memoryModelUnavailableReason || null,
        ...sessionWorkspaceMountFields(engine, newSessionPath, workspaceSelection.mount),
      };
      hub?.eventBus?.emit?.({
        type: "session_created",
        session: response,
      }, newSessionPath);
      return c.json(response);
    } catch (err) {
      const classified = classifySessionCreationError(err);
      return c.json(classified.body, classified.status);
    }
  });

  route.post("/sessions/new-detached", async (c) => {
    try {
      const requestContext = createRequestContext(c, engine);
      const auth = authorizeSessionRoute(requestContext, "sessions.write", {
        kind: "studio",
        studioId: requestContext.studioId,
      });
      if (!auth.allowed) return c.json({ error: "insufficient_scope", reason: auth.reason }, 403);
      if (typeof engine.createDetachedSession !== "function") {
        return c.json({ error: "detached session creation unavailable" }, 500);
      }

      const body = await safeJson(c);
      const { memoryEnabled, agentId, permissionMode, thinkingLevel } = body;
      const workspaceSelection = resolveSessionWorkspaceSelection(engine, requestContext, body);
      const cwd = workspaceSelection.cwd;
      const workspaceFolders = Array.isArray(body.workspaceFolders)
        ? body.workspaceFolders.filter(p => typeof p === "string" && p.trim())
        : [];
      const memFlag = memoryEnabled !== false;

      const detachedOptions: {
        cwd: any;
        memoryEnabled: boolean;
        agentId: string | null;
        workspaceFolders: any;
        visibleInSessionList: boolean;
        permissionMode: any;
        thinkingLevel?: any;
        workspaceMountId?: string;
        workspaceLabel?: string | null;
      } = {
        cwd: cwd || undefined,
        memoryEnabled: memFlag,
        agentId: typeof agentId === "string" && agentId.trim() ? agentId.trim() : null,
        workspaceFolders,
        visibleInSessionList: true,
        permissionMode: permissionMode || null,
      };
      if (thinkingLevel !== undefined && thinkingLevel !== null) {
        detachedOptions.thinkingLevel = thinkingLevel;
      }
      if (workspaceSelection.mount?.mountId) {
        detachedOptions.workspaceMountId = workspaceSelection.mount.mountId;
        detachedOptions.workspaceLabel = workspaceSelection.mount.label || null;
      }

      const result = await engine.createDetachedSession(detachedOptions);
      const newSessionPath = result.sessionPath;
      const newAgentId = result.agentId;
      engine.persistSessionMeta?.();

      const resolvedPermissionMode = engine.getSessionPermissionMode?.(newSessionPath)
        || permissionMode
        || engine.permissionMode
        || "ask";
      const response = {
        ok: true,
        path: newSessionPath,
        cwd: result.session?.sessionManager?.getCwd?.() || cwd || engine.cwd || null,
        workspaceFolders: engine.getSessionWorkspaceFolders?.(newSessionPath) || workspaceFolders,
        authorizedFolders: engine.getSessionAuthorizedFolders?.(newSessionPath) || [],
        agentId: newAgentId,
        agentName: engine.getAgent?.(newAgentId)?.agentName || newAgentId || engine.agentName,
        currentSessionPath: engine.currentSessionPath || null,
        planMode: resolvedPermissionMode === "read_only",
        permissionMode: resolvedPermissionMode,
        accessMode: resolvedPermissionMode === "read_only" ? "read_only" : "operate",
        thinkingLevel: normalizeSessionThinkingLevel(engine.getSessionThinkingLevel?.(newSessionPath) || engine.getThinkingLevel?.()),
        memoryModelUnavailableReason: engine.memoryModelUnavailableReason || null,
        ...sessionWorkspaceMountFields(engine, newSessionPath, workspaceSelection.mount),
      };
      hub?.eventBus?.emit?.({
        type: "session_created",
        session: response,
      }, newSessionPath);
      return c.json(response);
    } catch (err) {
      return c.json({ error: err.message }, 500);
    }
  });

  route.post("/sessions/continue-deleted-agent", async (c) => {
    try {
      const requestContext = createRequestContext(c, engine);
      const body = await safeJson(c);
      const sessionPath = body?.path || body?.sessionPath || null;
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      const auth = authorizeSessionRoute(requestContext, "sessions.write", {
        kind: "session",
        studioId: requestContext.studioId,
        sessionPath,
      });
      if (!auth.allowed) return c.json({ error: "insufficient_scope", reason: auth.reason }, 403);
      if (!isActiveDesktopSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      if (!isDeletedAgentSessionPath(sessionPath)) {
        return c.json({ error: "agent_not_deleted" }, 400);
      }
      if (!(await pathExists(sessionPath))) {
        return c.json({ error: t("error.sessionNotFound") }, 404);
      }

      const result = await engine.continueDeletedAgentSession(sessionPath);
      const newSessionPath = result.sessionPath;
      const newAgentId = result.agentId;
      const response = {
        ok: true,
        path: newSessionPath,
        cwd: result.cwd || engine.cwd || null,
        workspaceFolders: result.workspaceFolders || engine.getSessionWorkspaceFolders?.(newSessionPath) || [],
        authorizedFolders: result.authorizedFolders || engine.getSessionAuthorizedFolders?.(newSessionPath) || [],
        agentId: newAgentId,
        agentName: result.agentName || engine.getAgent?.(newAgentId)?.agentName || newAgentId,
        projectId: null,
        planMode: engine.planMode,
        permissionMode: engine.permissionMode,
        accessMode: engine.accessMode,
        thinkingLevel: normalizeSessionThinkingLevel(engine.getSessionThinkingLevel?.(newSessionPath) || engine.getThinkingLevel?.()),
        memoryModelUnavailableReason: engine.memoryModelUnavailableReason || null,
        compacted: result.compacted === true,
      };
      hub?.eventBus?.emit?.({
        type: "session_created",
        session: response,
      }, newSessionPath);
      return c.json(response);
    } catch (err) {
      return c.json({ error: err.message }, 500);
    }
  });

  // 切换 session（支持跨 agent）
  route.post("/sessions/switch", async (c) => {
    try {
      const body = await safeJson(c);
      const { path: sessionPath, currentSessionPath: oldSessionPath } = body;
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      // 运行路径只允许 active desktop session。归档会话必须先 restore。
      if (!isActiveDesktopSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      if (isDeletedAgentSessionPath(sessionPath)) {
        return rejectDeletedAgentSession(c);
      }
      // 切换前挂起浏览器（保存当前 session 的浏览器状态）
      const bm = BrowserManager.instance();
      const suspendPath = oldSessionPath;
      if (suspendPath && bm.isRunning(suspendPath)) {
        await bm.suspendForSession(suspendPath);
      }

      await engine.switchSession(sessionPath);

      // 恢复目标 session 的浏览器（若有）
      await bm.resumeForSession(sessionPath);

      const session = engine.getSessionByPath(sessionPath);

      // 从 sessionPath 解析 agentId，避免依赖 engine 焦点指针的时序
      const switchedAgentId = engine.agentIdFromSessionPath(sessionPath) || engine.currentAgentId;
      const switchedAgent = engine.getAgent(switchedAgentId);

      // switchSession 已同步设置焦点到目标 session。
      // cwd/planMode/model 是 session 级状态，此时读焦点是安全的。
      // memoryEnabled 需要返回 session 自身冻结下来的值，而不是当前
      // master && session 的临时组合态；否则现有 session 的缓存前缀身份
      // 会被全局 gate 混淆。
      // agentId/agentName 已从 sessionPath 解析，不依赖焦点。
      const activeModel = engine.activeSessionModel ?? engine.currentModel;
      const frozenSessionMemoryEnabled = typeof engine.getSessionMemoryEnabled === "function"
        ? engine.getSessionMemoryEnabled(sessionPath)
        : (switchedAgent?.isSessionMemoryEnabledFor?.(sessionPath) ?? engine.memoryEnabled);
      return c.json({
        ok: true,
        messageCount: session?.messages?.length || 0,
        memoryEnabled: frozenSessionMemoryEnabled,
        planMode: engine.planMode,
        permissionMode: engine.permissionMode,
        accessMode: engine.accessMode,
        thinkingLevel: normalizeSessionThinkingLevel(engine.getSessionThinkingLevel?.(sessionPath) || engine.getThinkingLevel?.()),
        memoryModelUnavailableReason: engine.memoryModelUnavailableReason || null,
        cwd: engine.cwd,
        workspaceFolders: engine.getSessionWorkspaceFolders?.(sessionPath) || [],
        authorizedFolders: engine.getSessionAuthorizedFolders?.(sessionPath) || [],
        ...sessionWorkspaceMountFields(engine, sessionPath),
        agentId: switchedAgentId,
        agentName: switchedAgent?.agentName || switchedAgentId,
        browserRunning: bm.isRunning(sessionPath),
        browserUrl: bm.currentUrl(sessionPath) || null,
        isStreaming: engine.isSessionStreaming(sessionPath),
        currentModelId: activeModel?.id || null,
        currentModelProvider: activeModel?.provider || null,
        currentModelName: activeModel?.name || null,
        currentModelInput: Array.isArray(activeModel?.input) ? activeModel.input : null,
        currentModelVideo: modelSupportsVideoInput(activeModel),
        currentModelVideoTransport: resolveModelVideoInputTransport(activeModel),
        currentModelVideoTransportSupported: modelSupportsDirectVideoInput(activeModel),
        currentModelAudio: modelSupportsAudioInput(activeModel),
        currentModelAudioTransport: resolveModelAudioInputTransport(activeModel),
        currentModelAudioTransportSupported: modelSupportsDirectAudioInput(activeModel),
        currentModelReasoning: activeModel?.reasoning ?? null,
        currentModelXhigh: modelSupportsXhigh(activeModel),
        currentModelContextWindow: activeModel?.contextWindow ?? null,
        // #1624：restore 时算好的工具/prompt 漂移提示（无漂移或已 dismiss → null）
        capabilityDrift: engine.getSessionCapabilityDriftNotice?.(sessionPath) || null,
      });
    } catch (err) {
      const errDetail = `${err.message}\n${err.stack || ""}`;
      switchLog.error(`error: ${errDetail}`);
      try { appendFileSync(path.join(engine.hanakoHome, "switch-error.log"), `${new Date().toISOString()}\n${errDetail}\n---\n`); } catch {}
      return c.json({ error: err.message }, 500);
    }
  });

  // #1624：关闭当前 fingerprint 的"工具能力有更新"提示（跟 session 走，指纹再变才重新提示）
  route.post("/sessions/capability-drift/dismiss", async (c) => {
    try {
      const body = await safeJson(c);
      const { path: sessionPath, fingerprint } = body || {};
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      if (typeof fingerprint !== "string" || !fingerprint) {
        return c.json({ error: t("error.missingParam", { param: "fingerprint" }) }, 400);
      }
      if (!isActiveDesktopSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      await engine.dismissSessionCapabilityDrift(sessionPath, fingerprint);
      return c.json({ ok: true });
    } catch (err) {
      return c.json({ error: err.message }, 500);
    }
  });

  // #1624：显式刷新 Agent 工具——fresh compact：压缩旧对话 + 用当前配置重建 prompt/工具快照
  route.post("/sessions/fresh-compact", async (c) => {
    try {
      const body = await safeJson(c);
      const { path: sessionPath } = body || {};
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      if (!isActiveDesktopSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      if (isDeletedAgentSessionPath(sessionPath)) {
        return rejectDeletedAgentSession(c);
      }
      const result = await engine.freshCompactDesktopSession(sessionPath);
      return c.json({
        ok: true,
        ...result,
        capabilityDrift: engine.getSessionCapabilityDriftNotice?.(sessionPath) || null,
      });
    } catch (err) {
      lifecycleLog.error(`fresh-compact failed: ${err.message}`);
      return c.json({ error: err.message }, 500);
    }
  });

  // 获取所有有浏览器的 session
  route.get("/browser/sessions", async (c) => {
    const bm = BrowserManager.instance();
    return c.json(bm.getBrowserSessions());
  });

  // 获取所有有浏览器痕迹的 session 状态（活跃 / 可恢复 / 不可用）
  route.get("/browser/session-states", async (c) => {
    const bm = BrowserManager.instance();
    return c.json(bm.getBrowserSessionStates());
  });

  // 关闭指定 session 的浏览器
  route.post("/browser/close-session", async (c) => {
    const body = await safeJson(c);
    const { sessionPath } = body;
    if (!sessionPath) return c.json({ error: "missing sessionPath" });
    const bm = BrowserManager.instance();
    await bm.closeBrowserForSession(sessionPath);
    hub?.eventBus?.emit?.({ type: "browser_status", running: false, url: null }, sessionPath);
    return c.json({ ok: true, sessions: bm.getBrowserSessionStates() });
  });

  // 重命名 session
  route.post("/sessions/rename", async (c) => {
    try {
      const body = await safeJson(c);
      const { path: sessionPath, title } = body;
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      if (typeof title !== "string" || !title.trim()) {
        return c.json({ error: t("error.missingParam", { param: "title" }) }, 400);
      }
      if (!isValidSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      if (isDeletedAgentSessionPath(sessionPath)) {
        return rejectDeletedAgentSession(c);
      }
      await engine.saveSessionTitle(sessionPath, title.trim());
      return c.json({ ok: true });
    } catch (err) {
      return c.json({ error: err.message }, 500);
    }
  });

  // 清理过期归档 session
  route.post("/sessions/cleanup", async (c) => {
    try {
      const body = await safeJson(c);
      const { maxAgeDays = 90 } = body;
      const cutoff = Date.now() - maxAgeDays * 86400000;
      let deleted = 0;

      // 遍历所有 agent 的 sessions/archived/ 目录
      const agentsDir = engine.agentsDir;
      const agents = await fs.readdir(agentsDir).catch(() => []);
      for (const agentId of agents) {
        const archiveDir = path.join(agentsDir, agentId, "sessions", "archived");
        let files;
        try { files = await fs.readdir(archiveDir); } catch { continue; }
        for (const f of files) {
          if (!f.endsWith(".jsonl")) continue;
          const fp = path.join(archiveDir, f);
          try {
            const stat = await fs.stat(fp);
            if (stat.mtime.getTime() < cutoff) {
              const activeKey = path.join(agentsDir, agentId, "sessions", f);
              await cleanupSessionLifecycle([activeKey, fp], "parent session deleted");
              await fs.unlink(fp);
              deleteSessionFileSidecarSync(fp);
              deleted++;
              // 清理 titles.json 孤儿（key = 对应的活跃路径）
              try { await engine.clearSessionTitle(activeKey); } catch {}
            }
          } catch {}
        }
      }

      return c.json({ ok: true, deleted, maxAgeDays });
    } catch (err) {
      return c.json({ error: err.message }, 500);
    }
  });

  // 列出所有已归档 session（聚合各 agent 的 archived/ 目录）
  route.get("/sessions/archived", async (c) => {
    try {
      const list = await engine.listArchivedSessions();
      return c.json(list);
    } catch (err) {
      return c.json({ error: err.message }, 500);
    }
  });

  // 归档 session（支持跨 agent）
  route.post("/sessions/archive", async (c) => {
    try {
      const body = await safeJson(c);
      const { path: sessionPath } = body;
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      // archive 是 lifecycle transition，只允许 active desktop session。
      if (!isActiveDesktopSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      if (isDeletedAgentSessionPath(sessionPath)) {
        return rejectDeletedAgentSession(c);
      }

      // 确认文件存在
      try {
        await fs.access(sessionPath);
      } catch {
        return c.json({ error: t("error.sessionNotFound") }, 404);
      }

      // 从 session 路径推导归档目录（同 agent 的 sessions/archived/）
      const destPath = archivedPathForActiveSession(sessionPath);
      const archiveDir = path.dirname(destPath);
      if (await pathExists(destPath)) {
        return c.json({ error: "Archived path already exists" }, 409);
      }
      if (await pathExists(sessionFileSidecarPath(destPath))) {
        return c.json({ error: "Stage file sidecar destination already exists" }, 409);
      }
      await cleanupSessionLifecycle([sessionPath, destPath], "parent session archived");

      // 再从 engine 的 session map 中移除。
      await engine.setSessionPinned(sessionPath, false);
      await engine.closeSession(sessionPath);

      await fs.mkdir(archiveDir, { recursive: true });
      await fs.rename(sessionPath, destPath);
      moveSessionFileSidecarSync(sessionPath, destPath);

      // 将 mtime 置为归档瞬间，使 cleanup 按"归档时间"而非"最后活动时间"判断
      const nowSec = Date.now() / 1000;
      await fs.utimes(destPath, nowSec, nowSec);

      return c.json({ ok: true });
    } catch (err) {
      return c.json({ error: err.message }, 500);
    }
  });

  // 恢复归档 session → 移回 sessions/
  route.post("/sessions/restore", async (c) => {
    try {
      const body = await safeJson(c);
      const { path: sessionPath } = body;
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      if (!isArchivedDesktopSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      // 必须位于 /archived/ 目录下，防止把活跃 session 当归档路径调用
      const archDir = path.dirname(sessionPath);
      if (path.basename(archDir) !== "archived") {
        return c.json({ error: "Not an archived session path" }, 403);
      }
      try {
        await fs.access(sessionPath);
      } catch {
        return c.json({ error: t("error.sessionNotFound") }, 404);
      }

      const activeDir = path.dirname(archDir);
      const destPath = path.join(activeDir, path.basename(sessionPath));

      // 冲突检测：目标位置已存在，不自动改名（违背"禁止非用户预期的 fallback"）
      try {
        await fs.access(destPath);
        return c.json({ error: "Active path already exists" }, 409);
      } catch { /* 目标不存在，可以恢复 */ }
      if (await pathExists(sessionFileSidecarPath(destPath))) {
        return c.json({ error: "Stage file sidecar destination already exists" }, 409);
      }

      await fs.rename(sessionPath, destPath);
      moveSessionFileSidecarSync(sessionPath, destPath);
      return c.json({ ok: true, restoredPath: destPath });
    } catch (err) {
      return c.json({ error: err.message }, 500);
    }
  });

  // 永久删除一条归档 session
  route.post("/sessions/archived/delete", async (c) => {
    try {
      const body = await safeJson(c);
      const { path: sessionPath } = body;
      if (!sessionPath) {
        return c.json({ error: t("error.missingParam", { param: "path" }) }, 400);
      }
      if (!isArchivedDesktopSessionPath(sessionPath, engine.agentsDir)) {
        return c.json({ error: "Invalid session path" }, 403);
      }
      const archDir = path.dirname(sessionPath);
      if (path.basename(archDir) !== "archived") {
        return c.json({ error: "Not an archived session path" }, 403);
      }
      const activeKey = activePathForArchivedSession(sessionPath);
      await cleanupSessionLifecycle([activeKey, sessionPath], "parent session deleted");
      try {
        await fs.unlink(sessionPath);
        deleteSessionFileSidecarSync(sessionPath);
      } catch (err) {
        if (err.code === "ENOENT") {
          return c.json({ error: t("error.sessionNotFound") }, 404);
        }
        throw err;
      }
      // 清理 titles.json 孤儿（key = 对应的活跃路径）
      try { await engine.clearSessionTitle(activeKey); } catch {}
      return c.json({ ok: true });
    } catch (err) {
      return c.json({ error: err.message }, 500);
    }
  });

  return route;
}

function patchSessionFileLifecycleBlocks(blocks, engine, sessionPath) {
  if (!sessionPath) return;
  for (const block of blocks || []) {
    if (!block) continue;
    if (!["file", "artifact", "skill", "screenshot"].includes(block.type)) continue;
    let file = null;
    if (block.fileId && typeof engine?.getSessionFile === "function") {
      file = engine.getSessionFile(block.fileId, { sessionPath });
    }
    if (!file && block.filePath && typeof engine?.getSessionFileByPath === "function") {
      file = engine.getSessionFileByPath(block.filePath, { sessionPath });
    }
    if (!file && block.type === "screenshot" && block.base64 && engine?.hanakoHome && typeof engine?.getSessionFileByPath === "function") {
      try {
        const filePath = browserScreenshotPath(engine.hanakoHome, sessionPath, {
          base64: block.base64,
          mimeType: block.mimeType,
        });
        file = engine.getSessionFileByPath(filePath, { sessionPath });
        if (file) block.type = "file";
      } catch {}
    }
    if (!file) continue;
    const patch = sessionFileLifecycleFields(file, engine);
    Object.assign(block, patch);
    if (block.type === "skill" && block.installedFile) {
      block.installedFile = { ...block.installedFile, ...patch };
    }
  }
}

function listSessionRegistryFiles(engine, sessionPath) {
  if (!sessionPath || typeof engine?.listSessionFiles !== "function") return [];
  return engine.listSessionFiles(sessionPath)
    .map(file => {
      if (typeof engine.serializeSessionFile === "function") return engine.serializeSessionFile(file);
      return serializeSessionFile(file, { runtimeContext: engine?.runtimeContext || null });
    })
    .filter(Boolean);
}

function isMediaGenerationDeferredResult(result) {
  return result?.type === "image-generation" || result?.type === "video-generation";
}

function parseHistoryDeferredResult(message) {
  if (message?.customType === DEFERRED_RESULT_RECORD_TYPE) {
    return parseDeferredResultRecord(message.data);
  }
  if (message?.customType === DEFERRED_RESULT_MESSAGE_TYPE) {
    return parseDeferredResultNotification(message.content);
  }
  return null;
}

function isTerminalDeferredTask(task) {
  return task?.status === "resolved" || task?.status === "failed" || task?.status === "aborted";
}

function sessionFileLifecycleFields(file, engine) {
  const serialized = typeof engine?.serializeSessionFile === "function"
    ? engine.serializeSessionFile(file)
    : file;
  const source = serialized || file;
  const fileId = source.fileId || source.id || file.fileId || file.id || null;
  return {
    ...(fileId ? { fileId } : {}),
    ...(source.filePath ? { filePath: source.filePath } : {}),
    ...(source.label || source.displayName ? { label: source.label || source.displayName } : {}),
    ...(source.ext !== undefined ? { ext: source.ext } : {}),
    ...(source.mime ? { mime: source.mime } : {}),
    ...(source.kind ? { kind: source.kind } : {}),
    ...(source.storageKind ? { storageKind: source.storageKind } : {}),
    ...(source.presentation ? { presentation: source.presentation } : {}),
    ...(source.listed !== undefined ? { listed: source.listed !== false } : {}),
    ...(source.status ? { status: source.status } : {}),
    ...(source.missingAt !== undefined ? { missingAt: source.missingAt } : {}),
    ...(source.mtimeMs !== undefined ? { mtimeMs: source.mtimeMs } : {}),
    ...(source.size !== undefined ? { size: source.size } : {}),
    ...(source.version ? { version: source.version } : {}),
    ...(source.resource ? { resource: source.resource } : {}),
  };
}
