#!/usr/bin/env node
/**
 * Align keepRecentTokens with OpenClaw native session/store accounting.
 *
 * Usage:
 *   node set-keep-recent.mjs --turns 5 [--workspace /path/to/ws] [--session /path/to/file.jsonl]
 *   node set-keep-recent.mjs --pct 30   [--workspace /path/to/ws]
 *   node set-keep-recent.mjs --tokens 20000 [--workspace /path/to/ws]
 *   node set-keep-recent.mjs --restore  [--workspace /path/to/ws]
 *   node set-keep-recent.mjs --info     [--workspace /path/to/ws]
 *
 * Extra selectors / diagnostics:
 *   --session-key <agent:...>
 *   --session-id <uuid>
 *   --state-dir <path>
 *   --debug
 *   --require-current-session
 *   --allow-latest-updated
 */

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";

export const BACKUP_KEY = "__compactContextSkillBackup";
export const LAST_RUN_KEY = "__compactContextSkillLastRun";
export const LAST_RESTORE_KEY = "__compactContextSkillLastRestore";
export const BACKUP_VERSION = 2;
export const INTERACTION_TURN_ALLOWANCE = 2;
export const DISPLAY_LABEL = "message_estimate";
export const PROMPT_TRUTH_SOURCE = "next_real_usage";
const DEFAULT_CONTEXT_TOKENS = 200000;
const OPENCLAW_JSON_ARGS = ["sessions", "--json"];
const ANSI_PATTERN = /\u001b\[[0-9;?]*[ -/]*[@-~]/g;
const SESSION_ROLES = new Set(["user", "human"]);
const TURN_KEEP_TOKENS_SAFETY_RATIO = 1.10;
const TURN_KEEP_TOKENS_SAFETY_BUFFER = 256;
const SESSION_REQUIRED_ERROR = "无法定位当前对话线程：缺少 session-key/session-id，拒绝执行局部压缩设置。";

function getArg(args, name) {
  const index = args.indexOf(name);
  return index >= 0 ? args[index + 1] : null;
}

function hasArg(args, name) {
  return args.includes(name);
}

export function parseArgs(argv) {
  const keepTurnsValue = getArg(argv, "--turns");
  const keepPctValue = getArg(argv, "--pct");
  const keepTokensValue = getArg(argv, "--tokens");

  return {
    keepTurns: keepTurnsValue !== null ? parseInt(keepTurnsValue, 10) : null,
    keepPct: keepPctValue !== null ? parseFloat(keepPctValue) : null,
    keepTokens: keepTokensValue !== null ? parseInt(keepTokensValue, 10) : null,
    restore: hasArg(argv, "--restore"),
    infoMode: hasArg(argv, "--info"),
    debug: hasArg(argv, "--debug"),
    workspace: getArg(argv, "--workspace") ?? process.cwd(),
    sessionFile: getArg(argv, "--session"),
    sessionKey: getArg(argv, "--session-key"),
    sessionId: getArg(argv, "--session-id"),
    stateDir: getArg(argv, "--state-dir"),
    requireCurrentSession: hasArg(argv, "--require-current-session"),
    allowLatestUpdated: hasArg(argv, "--allow-latest-updated"),
  };
}

export function stripAnsi(text) {
  return String(text ?? "").replace(ANSI_PATTERN, "");
}

function toPositiveNumber(value) {
  const parsed = typeof value === "number" ? value : Number(value);
  if (!Number.isFinite(parsed) || parsed <= 0) return null;
  return parsed;
}

function toFiniteNumber(value) {
  const parsed = typeof value === "number" ? value : Number(value);
  if (!Number.isFinite(parsed)) return null;
  return parsed;
}

function uniq(values) {
  return [...new Set(values.filter(Boolean))];
}

export function parseAgentIdFromSessionKey(sessionKey) {
  if (!sessionKey || typeof sessionKey !== "string") return null;
  const parts = sessionKey.split(":");
  if (parts[0] !== "agent" || parts.length < 2) return null;
  return parts[1] || null;
}

function formatDebugValue(value) {
  if (value === null || value === undefined || value === "") return "none";
  return String(value).replace(/\s+/g, "_");
}

function printDebug(debug, entries) {
  if (!debug) return;
  for (const [key, value] of Object.entries(entries)) {
    if (value === undefined) continue;
    console.log(`debug ${key}=${formatDebugValue(value)}`);
  }
}

function readJsonFile(filePath) {
  if (!filePath || !fs.existsSync(filePath)) return null;
  try {
    return JSON.parse(fs.readFileSync(filePath, "utf8"));
  } catch {
    return null;
  }
}

function resolveConfigPath() {
  return process.env.OPENCLAW_CONFIG_PATH || path.join(os.homedir(), ".openclaw", "openclaw.json");
}

function readOpenClawConfig() {
  return readJsonFile(resolveConfigPath()) ?? {};
}

function getCandidateStateDirs(overrideStateDir = null) {
  return uniq([
    overrideStateDir,
    process.env.OPENCLAW_STATE_DIR,
    process.env.CLAWDBOT_STATE_DIR,
    path.join(os.homedir(), ".openclaw"),
    path.join(os.homedir(), ".clawdbot"),
    path.join(os.homedir(), ".moltbot"),
  ]);
}

export function extractFirstJsonValue(text) {
  const source = String(text ?? "");
  for (let start = 0; start < source.length; start += 1) {
    const startChar = source[start];
    if (startChar !== "{" && startChar !== "[") continue;

    const stack = [];
    let inString = false;
    let escaped = false;

    for (let index = start; index < source.length; index += 1) {
      const char = source[index];

      if (inString) {
        if (escaped) {
          escaped = false;
        } else if (char === "\\") {
          escaped = true;
        } else if (char === "\"") {
          inString = false;
        }
        continue;
      }

      if (char === "\"") {
        inString = true;
        continue;
      }

      if (char === "{" || char === "[") {
        stack.push(char === "{" ? "}" : "]");
        continue;
      }

      if (char === "}" || char === "]") {
        if (stack.length === 0) break;
        const expected = stack.pop();
        if (expected !== char) break;
        if (stack.length === 0) {
          const candidate = source.slice(start, index + 1);
          try {
            JSON.parse(candidate);
            return candidate;
          } catch {
            break;
          }
        }
      }
    }
  }

  return null;
}

export function parseSessionsCommandOutput(outputText) {
  const source = String(outputText ?? "");
  for (let start = 0; start < source.length; start += 1) {
    const startChar = source[start];
    if (startChar !== "{" && startChar !== "[") continue;
    const jsonText = extractFirstJsonValue(source.slice(start));
    if (!jsonText) continue;

    try {
      const parsed = JSON.parse(jsonText);
      if (!parsed || typeof parsed !== "object") continue;
      if (!Array.isArray(parsed?.sessions) && typeof parsed?.path !== "string") continue;
      return {
        path: typeof parsed?.path === "string" ? parsed.path : null,
        sessions: Array.isArray(parsed?.sessions) ? parsed.sessions : [],
      };
    } catch {
      continue;
    }
  }

  return null;
}

function deriveStateDirFromStorePath(storePath) {
  if (!storePath) return null;
  const marker = `${path.sep}agents${path.sep}`;
  const markerIndex = storePath.indexOf(marker);
  if (markerIndex === -1) return null;
  return storePath.slice(0, markerIndex);
}

export function normalizeSessionRow(row, meta = {}) {
  const key = row?.key ?? meta.key ?? null;
  const agentId = row?.agentId ?? parseAgentIdFromSessionKey(key) ?? meta.agentId ?? null;

  return {
    key,
    sessionId: row?.sessionId ?? row?.session_id ?? null,
    updatedAt: toFiniteNumber(row?.updatedAt ?? row?.updated_at ?? null),
    totalTokens: toFiniteNumber(row?.totalTokens ?? row?.total_tokens ?? null),
    totalTokensFresh: typeof row?.totalTokensFresh === "boolean" ? row.totalTokensFresh : true,
    inputTokens: toFiniteNumber(row?.inputTokens ?? row?.input_tokens ?? null),
    outputTokens: toFiniteNumber(row?.outputTokens ?? row?.output_tokens ?? null),
    contextTokens: toPositiveNumber(row?.contextTokens ?? row?.context_tokens ?? null),
    model: row?.model ?? null,
    modelProvider: row?.modelProvider ?? row?.provider ?? null,
    sessionFile: row?.sessionFile ?? row?.session_file ?? null,
    stateDir: meta.stateDir ?? null,
    storeFile: meta.storeFile ?? null,
    agentId,
  };
}

function runOpenClawSessionsJson(workspace) {
  const result = spawnSync("openclaw", OPENCLAW_JSON_ARGS, {
    cwd: workspace,
    encoding: "utf8",
    env: process.env,
    maxBuffer: 10 * 1024 * 1024,
  });

  const combined = [result.stdout, result.stderr].filter(Boolean).join("\n");
  const parsed = parseSessionsCommandOutput(combined);
  if (!parsed) {
    return {
      available: false,
      sessions: [],
      stateDir: null,
      error: result.error?.message ?? `unable_to_parse_openclaw_sessions_output`,
      rawOutput: combined,
    };
  }

  const stateDir = deriveStateDirFromStorePath(parsed.path);
  return {
    available: true,
    sessions: parsed.sessions.map((session) =>
      normalizeSessionRow(session, { stateDir }),
    ),
    stateDir,
    error: result.status === 0 ? null : result.error?.message ?? `openclaw_sessions_exit_${result.status}`,
    rawOutput: combined,
  };
}

function loadSessionsFromStoreFiles(stateDirOverride = null) {
  const rows = [];

  for (const stateDir of getCandidateStateDirs(stateDirOverride)) {
    const agentsDir = path.join(stateDir, "agents");
    if (!fs.existsSync(agentsDir)) continue;

    for (const agentId of fs.readdirSync(agentsDir, { withFileTypes: true })) {
      if (!agentId.isDirectory()) continue;
      const storeFile = path.join(agentsDir, agentId.name, "sessions", "sessions.json");
      const store = readJsonFile(storeFile);
      if (!store || typeof store !== "object") continue;

      for (const [key, entry] of Object.entries(store)) {
        if (key === "global" || key === "unknown") continue;
        if (!entry || typeof entry !== "object") continue;
        rows.push(normalizeSessionRow(entry, {
          key,
          agentId: agentId.name,
          stateDir,
          storeFile,
        }));
      }
    }
  }

  return rows;
}

function buildSessionSelectors(options = {}) {
  return [
    { value: options.sessionKey, type: "key", resolvedBy: "session-key" },
    { value: options.sessionId, type: "id", resolvedBy: "session-id" },
    { value: process.env.OPENCLAW_TASK_SESSION_KEY, type: "key", resolvedBy: "env-session-key" },
    { value: process.env.OPENCLAW_TASK_SESSION_ID, type: "id", resolvedBy: "env-session-id" },
  ].filter((selector) => selector.value);
}

function hasCliSessionSelector(options = {}) {
  return Boolean(options.sessionKey || options.sessionId);
}

function sessionSelectorValue(options = {}) {
  if (options.sessionKey) return `session-key=${options.sessionKey}`;
  if (options.sessionId) return `session-id=${options.sessionId}`;
  if (process.env.OPENCLAW_TASK_SESSION_KEY) return `env-session-key=${process.env.OPENCLAW_TASK_SESSION_KEY}`;
  if (process.env.OPENCLAW_TASK_SESSION_ID) return `env-session-id=${process.env.OPENCLAW_TASK_SESSION_ID}`;
  return "unknown";
}

export function chooseSessionRow(rows, options = {}) {
  const selectors = buildSessionSelectors(options);

  for (const selector of selectors) {
    const candidates = rows.filter((row) =>
      selector.type === "key" ? row.key === selector.value : row.sessionId === selector.value,
    );
    if (candidates.length > 0) {
      const row = [...candidates].sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0))[0];
      return { row, resolvedBy: selector.resolvedBy, missing: false };
    }
  }

  if (selectors.length > 0) {
    const lastSelector = selectors[selectors.length - 1];
    return {
      row: null,
      resolvedBy: `${lastSelector.resolvedBy}-missing`,
      missing: true,
    };
  }

  if (rows.length === 0 || options.allowLatestUpdated === false) return null;
  const latest = [...rows].sort((left, right) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0))[0];
  return { row: latest, resolvedBy: "latest-updated", missing: false };
}

function modelMatches(modelId, candidateModel) {
  if (!modelId || !candidateModel) return false;
  const normalizedModelId = String(modelId).toLowerCase();
  const normalizedCandidate = String(candidateModel).toLowerCase();
  if (normalizedModelId === normalizedCandidate) return true;
  const bareModelId = normalizedModelId.includes("/") ? normalizedModelId.split("/").pop() : normalizedModelId;
  const bareCandidate = normalizedCandidate.includes("/") ? normalizedCandidate.split("/").pop() : normalizedCandidate;
  return bareModelId === bareCandidate;
}

function lookupModelContextWindow(cfg, provider, model) {
  const providerMap = cfg?.models?.providers ?? {};
  const providerKeys = provider ? [provider, provider.toLowerCase()] : Object.keys(providerMap);

  for (const providerKey of providerKeys) {
    const providerConfig = providerMap?.[providerKey];
    const models = Array.isArray(providerConfig?.models) ? providerConfig.models : [];
    for (const entry of models) {
      if (!modelMatches(entry?.id, model) && !modelMatches(`${providerKey}/${entry?.id}`, model)) {
        continue;
      }
      const contextWindow = toPositiveNumber(entry?.contextWindow ?? entry?.contextTokens ?? null);
      if (contextWindow) return contextWindow;
    }
  }

  return null;
}

export function resolveContextTokens({ cfg, provider, model, contextTokensOverride = null }) {
  const explicitOverride = toPositiveNumber(contextTokensOverride);
  if (explicitOverride) return explicitOverride;

  const configuredDefault = toPositiveNumber(cfg?.agents?.defaults?.contextTokens ?? null);
  const modelContextWindow = lookupModelContextWindow(cfg, provider, model);
  if (modelContextWindow) return modelContextWindow;
  if (configuredDefault) return configuredDefault;

  const envContextWindow = toPositiveNumber(process.env.CONTEXT_WINDOW);
  if (envContextWindow) return envContextWindow;

  return DEFAULT_CONTEXT_TOKENS;
}

export function resolveSessionFile(options, snapshot = null) {
  if (options?.sessionFile && fs.existsSync(options.sessionFile)) {
    return options.sessionFile;
  }

  const sessionId = snapshot?.sessionId ?? options?.sessionId ?? process.env.OPENCLAW_TASK_SESSION_ID ?? null;
  const agentId = snapshot?.agentId ?? parseAgentIdFromSessionKey(snapshot?.key ?? options?.sessionKey ?? process.env.OPENCLAW_TASK_SESSION_KEY ?? null) ?? "main";
  const candidateStateDirs = uniq([
    snapshot?.stateDir,
    options?.stateDir,
    ...getCandidateStateDirs(options?.stateDir),
  ]);

  if (snapshot?.sessionFile && fs.existsSync(snapshot.sessionFile)) {
    return snapshot.sessionFile;
  }

  if (!sessionId) return null;

  for (const stateDir of candidateStateDirs) {
    const sessionsDir = path.join(stateDir, "agents", agentId, "sessions");
    if (!fs.existsSync(sessionsDir)) continue;

    const exactPath = path.join(sessionsDir, `${sessionId}.jsonl`);
    if (fs.existsSync(exactPath)) return exactPath;

    const topicCandidates = fs.readdirSync(sessionsDir)
      .filter((entry) => entry.startsWith(`${sessionId}-`) && entry.endsWith(".jsonl"))
      .map((entry) => path.join(sessionsDir, entry))
      .sort((left, right) => fs.statSync(right).mtimeMs - fs.statSync(left).mtimeMs);

    if (topicCandidates.length > 0) return topicCandidates[0];
  }

  for (const stateDir of candidateStateDirs) {
    const agentsDir = path.join(stateDir, "agents");
    if (!fs.existsSync(agentsDir)) continue;

    for (const agentDir of fs.readdirSync(agentsDir, { withFileTypes: true })) {
      if (!agentDir.isDirectory()) continue;
      const agentSessionsDir = path.join(agentsDir, agentDir.name, "sessions");
      if (!fs.existsSync(agentSessionsDir)) continue;
      const exactPath = path.join(agentSessionsDir, `${sessionId}.jsonl`);
      if (fs.existsSync(exactPath)) return exactPath;
      const topicCandidates = fs.readdirSync(agentSessionsDir)
        .filter((entry) => entry.startsWith(`${sessionId}-`) && entry.endsWith(".jsonl"))
        .map((entry) => path.join(agentSessionsDir, entry))
        .sort((left, right) => fs.statSync(right).mtimeMs - fs.statSync(left).mtimeMs);
      if (topicCandidates.length > 0) return topicCandidates[0];
    }
  }

  return null;
}

function resolveSessionSnapshot(options) {
  const config = readOpenClawConfig();
  const explicitSessionFile = options.sessionFile && fs.existsSync(options.sessionFile) ? options.sessionFile : null;
  const selectors = buildSessionSelectors(options);
  const requireBoundSession = options.requireCurrentSession === true;
  const allowLatestUpdated = options.allowLatestUpdated === true;

  if (requireBoundSession && !explicitSessionFile && selectors.length === 0) {
    throw new Error(SESSION_REQUIRED_ERROR);
  }

  if (explicitSessionFile && !options.sessionKey && !options.sessionId) {
    return {
      key: null,
      sessionId: null,
      updatedAt: null,
      totalTokens: null,
      totalTokensFresh: false,
      inputTokens: null,
      outputTokens: null,
      contextTokens: resolveContextTokens({ cfg: config, provider: null, model: null, contextTokensOverride: null }),
      model: null,
      modelProvider: null,
      sessionFile: explicitSessionFile,
      stateDir: options.stateDir ?? getCandidateStateDirs(options.stateDir)[0] ?? null,
      storeFile: null,
      agentId: "main",
      resolvedBy: "session-file",
      config,
      nativeAvailable: false,
    };
  }

  const nativeResult = runOpenClawSessionsJson(options.workspace);
  let selectorMiss = null;

  if (nativeResult.sessions.length > 0) {
    const selection = chooseSessionRow(nativeResult.sessions, { ...options, allowLatestUpdated });
    if (selection?.row) {
      const row = selection.row;
      return {
        ...row,
        config,
        contextTokens: resolveContextTokens({
          cfg: config,
          provider: row.modelProvider,
          model: row.model,
          contextTokensOverride: row.contextTokens,
        }),
        resolvedBy: selection.resolvedBy,
        nativeAvailable: true,
      };
    }
    if (selection?.missing) selectorMiss = selection.resolvedBy;
  }

  const storeRows = loadSessionsFromStoreFiles(options.stateDir);
  const storeSelection = chooseSessionRow(storeRows, { ...options, allowLatestUpdated });
  if (storeSelection?.row) {
    const row = storeSelection.row;
    return {
      ...row,
      config,
      contextTokens: resolveContextTokens({
        cfg: config,
        provider: row.modelProvider,
        model: row.model,
        contextTokensOverride: row.contextTokens,
      }),
      resolvedBy: `store-${storeSelection.resolvedBy}`,
      nativeAvailable: true,
    };
  }
  if (storeSelection?.missing) selectorMiss = selectorMiss ?? storeSelection.resolvedBy;

  if (explicitSessionFile) {
    return {
      key: options.sessionKey ?? process.env.OPENCLAW_TASK_SESSION_KEY ?? null,
      sessionId: options.sessionId ?? process.env.OPENCLAW_TASK_SESSION_ID ?? null,
      updatedAt: null,
      totalTokens: null,
      totalTokensFresh: false,
      inputTokens: null,
      outputTokens: null,
      contextTokens: resolveContextTokens({ cfg: config, provider: null, model: null, contextTokensOverride: null }),
      model: null,
      modelProvider: null,
      sessionFile: explicitSessionFile,
      stateDir: options.stateDir ?? getCandidateStateDirs(options.stateDir)[0] ?? null,
      storeFile: null,
      agentId: parseAgentIdFromSessionKey(options.sessionKey ?? process.env.OPENCLAW_TASK_SESSION_KEY ?? null) ?? "main",
      resolvedBy: selectorMiss ?? "session-file",
      config,
      nativeAvailable: false,
    };
  }

  if (hasCliSessionSelector(options)) {
    throw new Error(`未找到指定 session：${sessionSelectorValue(options)}`);
  }

  if (requireBoundSession) {
    throw new Error(SESSION_REQUIRED_ERROR);
  }

  return {
    key: options.sessionKey ?? process.env.OPENCLAW_TASK_SESSION_KEY ?? null,
    sessionId: options.sessionId ?? process.env.OPENCLAW_TASK_SESSION_ID ?? null,
    updatedAt: null,
    totalTokens: null,
    totalTokensFresh: false,
    inputTokens: null,
    outputTokens: null,
    contextTokens: resolveContextTokens({ cfg: config, provider: null, model: null, contextTokensOverride: null }),
    model: null,
    modelProvider: null,
    sessionFile: null,
    stateDir: options.stateDir ?? getCandidateStateDirs(options.stateDir)[0] ?? null,
    storeFile: null,
    agentId: parseAgentIdFromSessionKey(options.sessionKey ?? process.env.OPENCLAW_TASK_SESSION_KEY ?? null) ?? "main",
    resolvedBy: selectorMiss ?? "none",
    config,
    nativeAvailable: false,
  };
}

function normalizeText(text) {
  return stripAnsi(String(text ?? "")).replace(/\r\n/g, "\n").trim();
}

function safeStringify(value) {
  try {
    return JSON.stringify(value);
  } catch {
    return "";
  }
}

function extractTextParts(content, allowedTypes = new Set(["text"])) {
  if (typeof content === "string") {
    const normalized = normalizeText(content);
    return normalized ? [normalized] : [];
  }

  if (!Array.isArray(content)) return [];

  const parts = [];
  for (const item of content) {
    if (typeof item === "string") {
      const normalized = normalizeText(item);
      if (normalized) parts.push(normalized);
      continue;
    }
    if (!item || typeof item !== "object") continue;
    if (!allowedTypes.has(item.type)) continue;
    if (item.type === "text" && typeof item.text === "string") {
      const normalized = normalizeText(item.text);
      if (normalized) parts.push(normalized);
      continue;
    }
    if (item.type === "thinking") {
      const thinkingText = typeof item.thinking === "string" ? item.thinking : item.text;
      const normalized = normalizeText(thinkingText);
      if (normalized) parts.push(normalized);
      continue;
    }
    if (item.type === "toolCall") {
      const name = normalizeText(item.name);
      if (name) parts.push(name);
      const serializedArguments = normalizeText(safeStringify(item.arguments));
      if (serializedArguments) parts.push(serializedArguments);
    }
  }
  return parts;
}

function messageTextForEstimate(entry) {
  const message = entry?.message;
  if (!message || typeof message !== "object") return "";

  if (message.role === "toolResult") {
    const aggregated = normalizeText(entry?.details?.aggregated);
    if (aggregated) return aggregated;
    return extractTextParts(message.content).join("\n");
  }

  if (message.role === "assistant") {
    return extractTextParts(message.content, new Set(["text", "thinking", "toolCall"])).join("\n");
  }

  return extractTextParts(message.content, new Set(["text"])).join("\n");
}

function estimateTextTokens(text) {
  if (!text) return 0;
  return Math.ceil(text.length / 4);
}

function messageObservedTotalTokens(entry) {
  const message = entry?.message;
  if (!message || message.role !== "assistant") return null;
  return toPositiveNumber(
    message?.usage?.totalTokens
    ?? message?.usage?.total_tokens
    ?? entry?.usage?.totalTokens
    ?? entry?.usage?.total_tokens
    ?? null,
  );
}

export function parseTranscriptText(text) {
  const items = [];
  const source = String(text ?? "").trim();
  if (!source) return items;

  for (const line of source.split("\n")) {
    if (!line.trim()) continue;
    let entry;
    try {
      entry = JSON.parse(line);
    } catch {
      continue;
    }

    if ((entry.type !== "message" && entry.type !== "custom_message") || !entry.message) {
      continue;
    }

    const textForEstimate = messageTextForEstimate(entry);
    items.push({
      role: entry.message.role ?? null,
      text: textForEstimate,
      estimatedTokens: estimateTextTokens(textForEstimate),
      observedTotalTokens: messageObservedTotalTokens(entry),
    });
  }

  return items;
}

export function summarizeTranscript(items) {
  let latestAssistantUsageTotalTokens = null;
  for (const item of items) {
    if (item.role !== "assistant") continue;
    const observedTotalTokens = toPositiveNumber(item.observedTotalTokens);
    if (observedTotalTokens !== null) latestAssistantUsageTotalTokens = observedTotalTokens;
  }

  return {
    totalTurns: items.filter((item) => SESSION_ROLES.has(item.role)).length,
    transcriptTotalEstimate: items.reduce((sum, item) => sum + item.estimatedTokens, 0),
    latestAssistantUsageTotalTokens,
  };
}

export function estimateTailTokens(items, keepTurns) {
  if (!Number.isFinite(keepTurns) || keepTurns <= 0) return 0;

  let turnCount = 0;
  let tokenCount = 0;

  for (let index = items.length - 1; index >= 0; index -= 1) {
    const item = items[index];
    tokenCount += item.estimatedTokens;
    if (SESSION_ROLES.has(item.role)) {
      turnCount += 1;
      if (turnCount >= keepTurns) break;
    }
  }

  return tokenCount;
}

function loadTranscriptStats(options, snapshot) {
  const sessionFile = resolveSessionFile(options, snapshot);
  if (!sessionFile || !fs.existsSync(sessionFile)) {
    if (options.requireTranscript === true) {
      if (options.sessionFile) {
        throw new Error(`未找到指定 transcript：${options.sessionFile}`);
      }
      if (snapshot?.sessionId || snapshot?.key) {
        throw new Error("已定位当前 session，但找不到对应 transcript 文件。");
      }
      throw new Error("无法读取当前对话 transcript，拒绝继续。");
    }
    return {
      sessionFile: null,
      items: [],
      totalTurns: 0,
      transcriptTotalEstimate: 0,
      latestAssistantUsageTotalTokens: null,
      tailEstimate: 0,
    };
  }

  let transcriptText;
  try {
    transcriptText = fs.readFileSync(sessionFile, "utf8");
  } catch (error) {
    throw new Error(`读取 transcript 失败：${error instanceof Error ? error.message : String(error)}`);
  }
  const items = parseTranscriptText(transcriptText);
  const summary = summarizeTranscript(items);
  return {
    sessionFile,
    items,
    totalTurns: summary.totalTurns,
    transcriptTotalEstimate: summary.transcriptTotalEstimate,
    latestAssistantUsageTotalTokens: summary.latestAssistantUsageTotalTokens,
    tailEstimate: 0,
  };
}

function valueOrUnknown(value) {
  return value === null || value === undefined ? "unknown" : String(value);
}

function toNonNegativeNumber(value) {
  const parsed = typeof value === "number" ? value : Number(value);
  if (!Number.isFinite(parsed) || parsed < 0) return null;
  return parsed;
}

function sourceDisplayName(source) {
  if (source === "native") return "native";
  if (source === "assistant-usage") return "assistant_usage_fallback";
  if (source === "transcript-estimate") return "transcript_estimate_fallback";
  return "unknown";
}

function resolveObservedTotalTokens(snapshot, transcriptStats) {
  const nativeTotalTokens = snapshot?.totalTokensFresh !== false ? toFiniteNumber(snapshot?.totalTokens) : null;
  if (nativeTotalTokens !== null) {
    return {
      observedTotalTokens: nativeTotalTokens,
      observedTotalSource: "native",
      isNative: true,
    };
  }

  const latestAssistantUsageTotalTokens = toPositiveNumber(transcriptStats?.latestAssistantUsageTotalTokens);
  if (latestAssistantUsageTotalTokens !== null) {
    return {
      observedTotalTokens: latestAssistantUsageTotalTokens,
      observedTotalSource: "assistant-usage",
      isNative: false,
    };
  }

  const transcriptTotalEstimate = transcriptStats?.transcriptTotalEstimate > 0
    ? transcriptStats.transcriptTotalEstimate
    : null;
  return {
    observedTotalTokens: transcriptTotalEstimate,
    observedTotalSource: "transcript-estimate",
    isNative: false,
  };
}

export function buildInfoResult(snapshot, transcriptStats) {
  const observedTotal = resolveObservedTotalTokens(snapshot, transcriptStats);
  const totalTokens = observedTotal.observedTotalTokens;
  const contextTokens = toPositiveNumber(snapshot?.contextTokens);
  const usedPct = totalTokens !== null && contextTokens ? Math.round(totalTokens / contextTokens * 100) : null;
  const source = observedTotal.observedTotalSource;
  const scaleRatio = observedTotal.observedTotalTokens !== null
    && transcriptStats?.transcriptTotalEstimate
    && observedTotal.observedTotalSource !== "transcript-estimate"
    ? observedTotal.observedTotalTokens / transcriptStats.transcriptTotalEstimate
    : null;

  return {
    turns: transcriptStats?.totalTurns ?? 0,
    totalTokens,
    contextTokens,
    usedPct,
    source,
    sourceLabel: sourceDisplayName(source),
    resolvedBy: snapshot?.resolvedBy ?? "none",
    nativeTotalTokens: observedTotal.isNative ? observedTotal.observedTotalTokens : null,
    observedTotalSource: observedTotal.observedTotalSource,
    transcriptTotalEstimate: transcriptStats?.transcriptTotalEstimate ?? 0,
    scaleRatio,
    estimationMode: observedTotal.observedTotalSource,
    messageEstimateBefore: transcriptStats?.transcriptTotalEstimate ?? 0,
    messageEstimateAfter: null,
    displayLabel: DISPLAY_LABEL,
    promptTruthSource: PROMPT_TRUTH_SOURCE,
  };
}

export function calculateTurnKeepTokens({
  requestedTurns,
  transcriptStats,
  observedTotalTokens,
  observedTotalSource,
}) {
  const requestedKeepTurns = Math.max(0, requestedTurns);
  const allowanceAdjustedTurns = requestedKeepTurns + INTERACTION_TURN_ALLOWANCE;
  const totalTurns = transcriptStats.totalTurns || requestedKeepTurns;
  const effectiveKeepTurns = Math.max(0, Math.min(allowanceAdjustedTurns, totalTurns));
  const tailEstimate = estimateTailTokens(transcriptStats.items, effectiveKeepTurns);
  const transcriptTotalEstimate = transcriptStats.transcriptTotalEstimate;

  let baseKeepTokens = tailEstimate;
  let source = "estimated";
  let scaleRatio = null;
  const displayTotalTurns = totalTurns > INTERACTION_TURN_ALLOWANCE
    ? totalTurns - INTERACTION_TURN_ALLOWANCE
    : totalTurns;
  const displayKeepTurns = Math.max(0, Math.min(requestedKeepTurns, displayTotalTurns));
  const displayCompressTurns = Math.max(0, displayTotalTurns - displayKeepTurns);

  if (observedTotalTokens !== null && transcriptTotalEstimate > 0 && observedTotalSource !== "transcript-estimate") {
    scaleRatio = observedTotalTokens / transcriptTotalEstimate;
    baseKeepTokens = Math.round(tailEstimate * scaleRatio);
    source = "calibrated";
  }

  const keepTokens = Math.max(
    baseKeepTokens,
    Math.ceil(baseKeepTokens * TURN_KEEP_TOKENS_SAFETY_RATIO),
    baseKeepTokens + TURN_KEEP_TOKENS_SAFETY_BUFFER,
  );

  return {
    requestedTurns: requestedKeepTurns,
    effectiveKeepTurns,
    compressTurns: Math.max(0, totalTurns - effectiveKeepTurns),
    displayKeepTurns,
    displayCompressTurns,
    keepTokens: Math.max(0, keepTokens),
    baseKeepTokens,
    tailEstimate,
    transcriptTotalEstimate,
    scaleRatio,
    source,
  };
}

function calculatePctKeepTokens({
  keepPct,
  transcriptStats,
  observedTotalTokens,
  observedTotalSource,
}) {
  const baseTotal = observedTotalTokens;
  if (baseTotal === null || baseTotal === 0) {
    throw new Error("无法根据百分比计算 keepRecentTokens：缺少可用的总 token 数据。");
  }

  return {
    keepTokens: Math.ceil(baseTotal * keepPct / 100),
    source: observedTotalSource === "native" ? "native_pct" : "fallback_pct",
    scaleRatio: observedTotalTokens !== null
      && transcriptStats.transcriptTotalEstimate > 0
      && observedTotalSource !== "transcript-estimate"
      ? observedTotalTokens / transcriptStats.transcriptTotalEstimate
      : null,
    transcriptTotalEstimate: transcriptStats.transcriptTotalEstimate,
  };
}

function readSettings(settingsFile) {
  return readJsonFile(settingsFile) ?? {};
}

function writeSettings(settingsFile, data) {
  fs.mkdirSync(path.dirname(settingsFile), { recursive: true });
  fs.writeFileSync(settingsFile, JSON.stringify(data, null, 2));
}

function sessionDetailsFromRecord(record = {}) {
  return {
    sessionKey: record?.sessionKey ?? record?.key ?? null,
    sessionId: record?.sessionId ?? null,
  };
}

function sessionMatches(expected = {}, actual = {}) {
  const expectedSessionId = expected?.sessionId ?? null;
  const expectedSessionKey = expected?.sessionKey ?? null;
  const actualSessionId = actual?.sessionId ?? null;
  const actualSessionKey = actual?.sessionKey ?? null;

  let compared = false;

  if (expectedSessionId && actualSessionId) {
    compared = true;
    if (expectedSessionId !== actualSessionId) return false;
  }

  if (expectedSessionKey && actualSessionKey) {
    compared = true;
    if (expectedSessionKey !== actualSessionKey) return false;
  }

  if (compared) return true;
  if (expectedSessionId || expectedSessionKey) return false;
  return true;
}

function buildAuditSnapshot(metadata = {}, appliedKeepRecentTokens = null) {
  const totalTokensBefore = toFiniteNumber(metadata.totalTokensBefore);
  const contextTokensBefore = toPositiveNumber(metadata.contextTokensBefore);
  const usedPctBefore = totalTokensBefore !== null && contextTokensBefore
    ? Math.round(totalTokensBefore / contextTokensBefore * 100)
    : null;

  return {
    version: BACKUP_VERSION,
    sessionKey: metadata.sessionKey ?? null,
    sessionId: metadata.sessionId ?? null,
    resolvedBy: metadata.resolvedBy ?? null,
    source: metadata.source ?? null,
    requestedTurns: metadata.requestedTurns ?? null,
    effectiveKeepTurns: metadata.effectiveKeepTurns ?? null,
    displayKeepTurns: metadata.displayKeepTurns ?? null,
    displayCompressTurns: metadata.displayCompressTurns ?? null,
    keepRecentTokens: appliedKeepRecentTokens,
    totalTokensBefore,
    contextTokensBefore,
    usedPctBefore,
    transcriptTotalEstimateBefore: toFiniteNumber(metadata.transcriptTotalEstimateBefore),
    nativeTotalTokensBefore: toFiniteNumber(metadata.nativeTotalTokensBefore),
    messageEstimateBefore: toNonNegativeNumber(metadata.messageEstimateBefore),
    messageEstimateAfter: toNonNegativeNumber(metadata.messageEstimateAfter),
    displayLabel: metadata.displayLabel ?? DISPLAY_LABEL,
    promptTruthSource: metadata.promptTruthSource ?? PROMPT_TRUTH_SOURCE,
    createdAt: metadata.createdAt ?? new Date().toISOString(),
  };
}

function createBackup(currentKeepRecentTokens, metadata, appliedKeepRecentTokens) {
  const audit = buildAuditSnapshot(metadata, appliedKeepRecentTokens);
  return {
    version: BACKUP_VERSION,
    previousKeepRecentTokens: typeof currentKeepRecentTokens === "number" ? currentKeepRecentTokens : null,
    appliedKeepRecentTokens,
    requestedTurns: audit.requestedTurns,
    effectiveKeepTurns: audit.effectiveKeepTurns,
    sessionKey: audit.sessionKey,
    sessionId: audit.sessionId,
    source: audit.source,
    audit,
    updatedAt: new Date().toISOString(),
  };
}

export function applyKeepRecentTokensSetting(settingsFile, targetKeepTokens, metadata = {}) {
  const current = readSettings(settingsFile);
  const compaction = { ...(current.compaction ?? {}) };
  const existingBackup = compaction[BACKUP_KEY];
  const currentKeepRecentTokens = typeof compaction.keepRecentTokens === "number" ? compaction.keepRecentTokens : null;
  const audit = buildAuditSnapshot(metadata, targetKeepTokens);

  const canReuseBackup = Boolean(
    existingBackup
    && typeof existingBackup === "object"
    && currentKeepRecentTokens === existingBackup.appliedKeepRecentTokens
  );

  compaction[BACKUP_KEY] = canReuseBackup
    ? {
        ...existingBackup,
        appliedKeepRecentTokens: targetKeepTokens,
        requestedTurns: metadata.requestedTurns ?? existingBackup.requestedTurns ?? null,
        effectiveKeepTurns: metadata.effectiveKeepTurns ?? existingBackup.effectiveKeepTurns ?? null,
        sessionKey: metadata.sessionKey ?? existingBackup.sessionKey ?? null,
        sessionId: metadata.sessionId ?? existingBackup.sessionId ?? null,
        source: metadata.source ?? existingBackup.source ?? null,
        audit,
        updatedAt: new Date().toISOString(),
      }
    : createBackup(currentKeepRecentTokens, metadata, targetKeepTokens);

  compaction.keepRecentTokens = targetKeepTokens;
  compaction[LAST_RUN_KEY] = audit;
  current.compaction = compaction;
  writeSettings(settingsFile, current);

  return {
    backup: compaction[BACKUP_KEY],
    appliedKeepRecentTokens: targetKeepTokens,
    reusedBackup: canReuseBackup,
  };
}

function recordLastRestore(compaction, restoreResult) {
  compaction[LAST_RESTORE_KEY] = {
    version: BACKUP_VERSION,
    sessionKey: restoreResult.sessionKey ?? null,
    sessionId: restoreResult.sessionId ?? null,
    restoreStatus: restoreResult.status,
    restoredKeepRecentTokens: restoreResult.restoredValue ?? null,
    restoredAt: new Date().toISOString(),
  };
}

export function restoreKeepRecentTokensSetting(settingsFile, expectedSession = null) {
  if (!fs.existsSync(settingsFile)) {
    return { status: "no_backup", restoredValue: null };
  }

  const current = readSettings(settingsFile);
  const compaction = current.compaction ?? {};
  const backup = compaction[BACKUP_KEY];

  if (!backup || typeof backup !== "object") {
    return { status: "no_backup", restoredValue: null };
  }

  if (expectedSession && !sessionMatches(expectedSession, sessionDetailsFromRecord(backup))) {
    recordLastRestore(compaction, {
      status: "session_mismatch",
      restoredValue: current.compaction?.keepRecentTokens ?? null,
      ...sessionDetailsFromRecord(backup),
    });
    current.compaction = compaction;
    writeSettings(settingsFile, current);
    return { status: "session_mismatch", restoredValue: current.compaction?.keepRecentTokens ?? null };
  }

  const currentKeepRecentTokens = typeof compaction.keepRecentTokens === "number" ? compaction.keepRecentTokens : null;
  if (currentKeepRecentTokens !== backup.appliedKeepRecentTokens) {
    recordLastRestore(compaction, {
      status: "manual_change",
      restoredValue: currentKeepRecentTokens,
      ...sessionDetailsFromRecord(backup),
    });
    current.compaction = compaction;
    writeSettings(settingsFile, current);
    return { status: "manual_change", restoredValue: currentKeepRecentTokens };
  }

  if (typeof backup.previousKeepRecentTokens === "number") {
    compaction.keepRecentTokens = backup.previousKeepRecentTokens;
  } else {
    delete compaction.keepRecentTokens;
  }

  delete compaction[BACKUP_KEY];
  recordLastRestore(compaction, {
    status: "restored",
    restoredValue: typeof backup.previousKeepRecentTokens === "number" ? backup.previousKeepRecentTokens : null,
    ...sessionDetailsFromRecord(backup),
  });
  current.compaction = compaction;
  writeSettings(settingsFile, current);
  return {
    status: "restored",
    restoredValue: typeof backup.previousKeepRecentTokens === "number" ? backup.previousKeepRecentTokens : null,
  };
}

function validateOptions(options) {
  const modes = [
    options.infoMode,
    options.restore,
    options.keepTurns !== null,
    options.keepPct !== null,
    options.keepTokens !== null,
  ].filter(Boolean).length;

  if (modes === 0) {
    throw new Error(
      "Usage:\n" +
      "  set-keep-recent.mjs --turns N   [--workspace <path>] [--session <path>]\n" +
      "  set-keep-recent.mjs --pct N     [--workspace <path>]\n" +
      "  set-keep-recent.mjs --tokens N  [--workspace <path>]\n" +
      "  set-keep-recent.mjs --restore   [--workspace <path>]\n" +
      "  set-keep-recent.mjs --info      [--workspace <path>]\n" +
      "  Optional selectors: --session-key <key> --session-id <id> --state-dir <dir> --debug\n" +
      "  Safety flags: --require-current-session --allow-latest-updated",
    );
  }

  if (modes > 1) {
    throw new Error("一次只能使用一种模式：--info、--restore、--turns、--pct、--tokens 互斥。");
  }

  if (options.keepTurns !== null && (!Number.isInteger(options.keepTurns) || options.keepTurns < 0)) {
    throw new Error("--turns 必须是大于等于 0 的整数。");
  }

  if (options.keepPct !== null && (!Number.isFinite(options.keepPct) || options.keepPct < 0 || options.keepPct > 100)) {
    throw new Error("--pct 必须在 0 到 100 之间。");
  }

  if (options.keepTokens !== null && (!Number.isInteger(options.keepTokens) || options.keepTokens < 0)) {
    throw new Error("--tokens 必须是大于等于 0 的整数。");
  }
}

export function run(options) {
  validateOptions(options);

  const settingsFile = path.join(options.workspace, ".pi", "settings.json");
  const snapshot = resolveSessionSnapshot(options);
  const transcriptStats = loadTranscriptStats({
    ...options,
    requireTranscript: options.infoMode || options.keepTurns !== null || options.keepPct !== null,
  }, snapshot);
  const observedTotal = resolveObservedTotalTokens(snapshot, transcriptStats);
  const nativeTotalTokens = observedTotal.isNative ? observedTotal.observedTotalTokens : null;
  const settings = readSettings(settingsFile);
  const lastRun = settings?.compaction?.[LAST_RUN_KEY] ?? null;
  const snapshotStatus = !lastRun
    ? "none"
    : sessionMatches(sessionDetailsFromRecord(snapshot), sessionDetailsFromRecord(lastRun))
      ? "matches-current-session"
      : "session-mismatch";
  const baseDebug = {
    resolved_session_key: snapshot.key,
    resolved_session_id: snapshot.sessionId,
    resolved_by: snapshot.resolvedBy,
    resolved_session_file: transcriptStats.sessionFile,
    source: observedTotal.observedTotalSource,
    native_total_tokens: nativeTotalTokens,
    native_context_tokens: snapshot.contextTokens,
    transcript_total_estimate: transcriptStats.transcriptTotalEstimate,
    assistant_usage_total_tokens: transcriptStats.latestAssistantUsageTotalTokens,
    observed_total_tokens: observedTotal.observedTotalTokens,
    observed_total_source: observedTotal.observedTotalSource,
    snapshot_status: snapshotStatus,
    message_estimate_before: transcriptStats.transcriptTotalEstimate,
    prompt_truth_source: PROMPT_TRUTH_SOURCE,
  };

  if (options.infoMode) {
    const info = buildInfoResult(snapshot, transcriptStats);
    console.log(
      `turns=${info.turns} total_tokens=${valueOrUnknown(info.totalTokens)} context_tokens=${valueOrUnknown(info.contextTokens)} used_pct=${valueOrUnknown(info.usedPct)} source=${info.source} estimation_mode=${info.estimationMode} source_label=${info.sourceLabel} message_estimate_before=${valueOrUnknown(info.messageEstimateBefore)} message_estimate_after=${valueOrUnknown(info.messageEstimateAfter)} display_label=${info.displayLabel} prompt_truth_source=${info.promptTruthSource} resolved_by=${info.resolvedBy} snapshot_status=${snapshotStatus}`,
    );
    printDebug(options.debug, {
      ...baseDebug,
      source: info.source,
      scale_ratio: info.scaleRatio,
    });
    return 0;
  }

  if (options.restore) {
    const restoreResult = restoreKeepRecentTokensSetting(settingsFile, sessionDetailsFromRecord(snapshot));
    const statusLabel = restoreResult.status === "restored"
      ? "✔ 已还原"
      : restoreResult.status === "session_mismatch"
        ? "✘ 当前 session 与备份不一致，拒绝还原"
      : restoreResult.status === "manual_change"
        ? "✔ 检测到手动修改，跳过还原"
        : "✔ 无需还原";
    console.log(`${statusLabel} restore_status=${restoreResult.status} snapshot_status=${snapshotStatus}`);
    printDebug(options.debug, {
      ...baseDebug,
      restore_status: restoreResult.status,
      restored_value: restoreResult.restoredValue,
      settings_file: settingsFile,
    });
    return 0;
  }

  if (options.keepTokens !== null) {
    const applyResult = applyKeepRecentTokensSetting(settingsFile, options.keepTokens, {
      requestedTurns: null,
      effectiveKeepTurns: null,
      displayKeepTurns: null,
      displayCompressTurns: null,
      sessionKey: snapshot.key,
      sessionId: snapshot.sessionId,
      source: "explicit",
      resolvedBy: snapshot.resolvedBy,
      totalTokensBefore: observedTotal.observedTotalTokens,
      contextTokensBefore: snapshot.contextTokens,
      transcriptTotalEstimateBefore: transcriptStats.transcriptTotalEstimate,
      nativeTotalTokensBefore: nativeTotalTokens,
      messageEstimateBefore: transcriptStats.transcriptTotalEstimate,
      messageEstimateAfter: null,
      displayLabel: DISPLAY_LABEL,
      promptTruthSource: PROMPT_TRUTH_SOURCE,
    });
    console.log(`keep_tokens=${options.keepTokens} source=explicit estimation_mode=explicit message_estimate_before=${valueOrUnknown(transcriptStats.transcriptTotalEstimate)} message_estimate_after=unknown display_label=${DISPLAY_LABEL} prompt_truth_source=${PROMPT_TRUTH_SOURCE} snapshot_status=${applyResult.reusedBackup ? "updated" : "created"}`);
    printDebug(options.debug, {
      ...baseDebug,
      scale_ratio: null,
      settings_file: settingsFile,
    });
    return 0;
  }

  if (options.keepPct !== null) {
    const pctResult = calculatePctKeepTokens({
      keepPct: options.keepPct,
      transcriptStats,
      observedTotalTokens: observedTotal.observedTotalTokens,
      observedTotalSource: observedTotal.observedTotalSource,
    });
    const applyResult = applyKeepRecentTokensSetting(settingsFile, pctResult.keepTokens, {
      requestedTurns: null,
      effectiveKeepTurns: null,
      displayKeepTurns: null,
      displayCompressTurns: null,
      sessionKey: snapshot.key,
      sessionId: snapshot.sessionId,
      source: observedTotal.observedTotalSource,
      resolvedBy: snapshot.resolvedBy,
      totalTokensBefore: observedTotal.observedTotalTokens,
      contextTokensBefore: snapshot.contextTokens,
      transcriptTotalEstimateBefore: transcriptStats.transcriptTotalEstimate,
      nativeTotalTokensBefore: nativeTotalTokens,
      messageEstimateBefore: transcriptStats.transcriptTotalEstimate,
      messageEstimateAfter: null,
      displayLabel: DISPLAY_LABEL,
      promptTruthSource: PROMPT_TRUTH_SOURCE,
    });
    console.log(`keep_tokens=${pctResult.keepTokens} source=${observedTotal.observedTotalSource} estimation_mode=${pctResult.source} message_estimate_before=${valueOrUnknown(transcriptStats.transcriptTotalEstimate)} message_estimate_after=unknown display_label=${DISPLAY_LABEL} prompt_truth_source=${PROMPT_TRUTH_SOURCE} snapshot_status=${applyResult.reusedBackup ? "updated" : "created"}`);
    printDebug(options.debug, {
      ...baseDebug,
      scale_ratio: pctResult.scaleRatio,
      settings_file: settingsFile,
    });
    return 0;
  }

  const turnResult = calculateTurnKeepTokens({
    requestedTurns: options.keepTurns,
    transcriptStats,
    observedTotalTokens: observedTotal.observedTotalTokens,
    observedTotalSource: observedTotal.observedTotalSource,
  });

  const applyResult = applyKeepRecentTokensSetting(settingsFile, turnResult.keepTokens, {
    requestedTurns: turnResult.requestedTurns,
    effectiveKeepTurns: turnResult.effectiveKeepTurns,
    displayKeepTurns: turnResult.displayKeepTurns,
    displayCompressTurns: turnResult.displayCompressTurns,
    sessionKey: snapshot.key,
    sessionId: snapshot.sessionId,
    source: observedTotal.observedTotalSource,
    resolvedBy: snapshot.resolvedBy,
    totalTokensBefore: observedTotal.observedTotalTokens,
    contextTokensBefore: snapshot.contextTokens,
    transcriptTotalEstimateBefore: transcriptStats.transcriptTotalEstimate,
    nativeTotalTokensBefore: nativeTotalTokens,
    messageEstimateBefore: transcriptStats.transcriptTotalEstimate,
    messageEstimateAfter: turnResult.baseKeepTokens,
    displayLabel: DISPLAY_LABEL,
    promptTruthSource: PROMPT_TRUTH_SOURCE,
  });

  console.log(
    `keep_turns=${turnResult.displayKeepTurns} compress_turns=${turnResult.displayCompressTurns} display_keep_turns=${turnResult.displayKeepTurns} display_compress_turns=${turnResult.displayCompressTurns} keep_tokens=${turnResult.keepTokens} source=${observedTotal.observedTotalSource} estimation_mode=${turnResult.source} message_estimate_before=${valueOrUnknown(transcriptStats.transcriptTotalEstimate)} message_estimate_after=${valueOrUnknown(turnResult.baseKeepTokens)} display_label=${DISPLAY_LABEL} prompt_truth_source=${PROMPT_TRUTH_SOURCE} snapshot_status=${applyResult.reusedBackup ? "updated" : "created"}`,
  );
  printDebug(options.debug, {
    ...baseDebug,
    requested_turns: turnResult.requestedTurns,
    effective_keep_turns: turnResult.effectiveKeepTurns,
    tail_estimate: turnResult.tailEstimate,
    message_estimate_after: turnResult.baseKeepTokens,
    scale_ratio: turnResult.scaleRatio,
    settings_file: settingsFile,
  });
  return 0;
}

function main() {
  const options = parseArgs(process.argv.slice(2));
  try {
    const exitCode = run(options);
    process.exit(exitCode);
  } catch (error) {
    console.error(error instanceof Error ? error.message : String(error));
    process.exit(1);
  }
}

const isDirectRun = process.argv[1]
  && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);

if (isDirectRun) {
  main();
}
