'use strict';

const fs = require('fs');
const path = require('path');
const { StringDecoder } = require('string_decoder');
const { getResumeSpec: getAgentResumeSpec, normalizeAgentType } = require('./agent-capabilities');
const structuredCapture = require('./structured-capture');
const codexZst = require('./codex-zst');

const DEFAULT_CODEX_LOOKBACK_SEC = 10;
const DEFAULT_CODEX_MAX_DELAY_SEC = 180;
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const CODEX_ROLLOUT_BASENAME_RE = /^rollout-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.bak)?(?:\.zst)?$/i;

// Rollout candidates on disk: live `.jsonl` plus zstd-compressed cold
// archives (`.jsonl.zst`) that newer Codex versions write.
function isCodexRolloutCandidateName(name) {
  const n = String(name || '');
  return n.endsWith('.jsonl') || codexZst.isZstRolloutFileName(n);
}

// Compressed archives are materialized to a disk cache before any read; a
// failure (no zstd, too large) makes the file invisible to the reader.
function _codexReadablePath(filePath) {
  if (!codexZst.isZstRolloutFileName(filePath)) return filePath;
  const mat = codexZst.materializeCodexRollout(filePath);
  return mat.ok ? mat.path : null;
}

function getResumeSpec(agentType, resumeId) {
  return getAgentResumeSpec(agentType || 'claude', resumeId);
}

function extractResumeTarget(agentType, args) {
  agentType = normalizeAgentType(agentType) || agentType;
  const list = Array.isArray(args) ? args : [];
  if (agentType === 'codex') {
    const idx = list.indexOf('resume');
    return idx >= 0 && list[idx + 1] ? { index: idx, valueIndex: idx + 1, sessionId: list[idx + 1] } : null;
  }
  if (agentType === 'gemini') {
    const idx = list.indexOf('--session');
    return idx >= 0 && list[idx + 1] ? { index: idx, valueIndex: idx + 1, sessionId: list[idx + 1] } : null;
  }
  const idx = list.indexOf('--resume');
  return idx >= 0 && list[idx + 1] ? { index: idx, valueIndex: idx + 1, sessionId: list[idx + 1] } : null;
}

function codexRolloutFileInfo(filePath) {
  if (!filePath) return null;
  const match = path.basename(filePath).match(CODEX_ROLLOUT_BASENAME_RE);
  if (!match) return null;
  return {
    rolloutPrefix: match[1],
    threadId: match[2].toLowerCase(),
  };
}

function codexRolloutIdFromPath(filePath) {
  return codexRolloutFileInfo(filePath)?.threadId || '';
}

function codexRolloutFileMatchesThread(filePath, threadId) {
  const wanted = String(threadId || '').trim().toLowerCase();
  if (!UUID_RE.test(wanted)) return false;
  const pathId = codexRolloutIdFromPath(filePath);
  if (pathId && pathId !== wanted) {
    const meta = _readCodexRolloutMetadata(filePath);
    return String(meta?.id || '').toLowerCase() === wanted;
  }
  if (pathId === wanted) {
    const meta = _readCodexRolloutMetadata(filePath);
    return !meta?.id || String(meta.id).toLowerCase() === wanted;
  }
  const meta = _readCodexRolloutMetadata(filePath);
  return String(meta?.id || '').toLowerCase() === wanted;
}

function codexSessionSearchRoots(sessionsRoot, homeDir = process.env.HOME) {
  if (sessionsRoot) return fs.existsSync(sessionsRoot) ? [sessionsRoot] : [];
  return [
    path.join(homeDir, '.codex', 'sessions'),
    path.join(homeDir, '.codex', 'archived_sessions'),
  ].filter((root) => fs.existsSync(root));
}

function sortCodexRolloutFiles(files) {
  return [...new Set(files)]
    .map((filePath) => {
      let mtimeMs = 0;
      try { mtimeMs = fs.statSync(filePath).mtimeMs || 0; } catch {}
      return { filePath, info: codexRolloutFileInfo(filePath), mtimeMs };
    })
    .sort((a, b) => {
      if (a.info && b.info) {
        const byPrefix = b.info.rolloutPrefix.localeCompare(a.info.rolloutPrefix);
        return byPrefix || b.filePath.localeCompare(a.filePath);
      }
      if (a.info && !b.info) return -1;
      if (!a.info && b.info) return 1;
      return (b.mtimeMs - a.mtimeMs) || b.filePath.localeCompare(a.filePath);
    })
    .map((entry) => entry.filePath);
}

function codexStateDbPath(homeDir = process.env.HOME) {
  return path.join(homeDir, '.codex', 'state_5.sqlite');
}

function openCodexStateDb(homeDir = process.env.HOME, options = {}) {
  const Database = require('better-sqlite3');
  const dbPath = codexStateDbPath(homeDir);
  if (!fs.existsSync(dbPath)) return null;
  return new Database(dbPath, { readonly: options.readonly !== false });
}

function getCodexStateThreadById(threadId, homeDir = process.env.HOME) {
  const wanted = String(threadId || '').trim().toLowerCase();
  if (!UUID_RE.test(wanted)) return null;
  let db = null;
  try {
    db = openCodexStateDb(homeDir);
    if (!db) return null;
    return db.prepare('SELECT * FROM threads WHERE id = ?').get(wanted) || null;
  } catch {
    return null;
  } finally {
    try { if (db) db.close(); } catch {}
  }
}

function findCodexStateRolloutPath(threadId, homeDir = process.env.HOME) {
  const row = getCodexStateThreadById(threadId, homeDir);
  try {
    const rolloutPath = row?.rollout_path || '';
    if (!rolloutPath || !fs.existsSync(rolloutPath)) return '';
    // Codex `resume <thread-id>` resolves through this table. In real state
    // files, threads.id can point at a rollout whose filename/session_meta id is
    // different, so treating the filename id as authoritative makes CTM read a
    // stale transcript while the live process writes elsewhere.
    return rolloutPath;
  } catch {
    return '';
  }
}

function findCodexSessionFiles(threadId, sessionsRoot, homeDir = process.env.HOME) {
  const wanted = String(threadId || '').trim().toLowerCase();
  if (!UUID_RE.test(wanted)) return [];
  const matches = [];
  const metadataFallbackCandidates = [];
  const indexedPath = sessionsRoot ? '' : findCodexStateRolloutPath(wanted, homeDir);
  const indexedPathMatchesWanted = indexedPath && codexRolloutFileMatchesThread(indexedPath, wanted);
  if (indexedPathMatchesWanted) {
    matches.push(indexedPath);
  }
  const stack = codexSessionSearchRoots(sessionsRoot, homeDir);
  while (stack.length > 0) {
    const dir = stack.pop();
    let entries = [];
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        stack.push(fullPath);
        continue;
      }
      if (entry.isFile() && codexRolloutIdFromPath(fullPath) === wanted) {
        if (!codexRolloutFileMatchesThread(fullPath, wanted)) continue;
        matches.push(fullPath);
      } else if (entry.isFile() && isCodexRolloutCandidateName(entry.name)) {
        metadataFallbackCandidates.push(fullPath);
      }
    }
  }
  if (matches.length === 0) {
    for (const fullPath of metadataFallbackCandidates) {
      if (codexRolloutFileMatchesThread(fullPath, wanted)) matches.push(fullPath);
    }
  }
  const sorted = sortCodexRolloutFiles(codexZst.dropZstWithLiveSibling(matches));
  return indexedPathMatchesWanted
    ? [indexedPath, ...sorted.filter((filePath) => filePath !== indexedPath)]
    : sorted;
}

function findCodexSessionFilesByFilename(threadId, sessionsRoot, homeDir = process.env.HOME) {
  const wanted = String(threadId || '').trim().toLowerCase();
  if (!UUID_RE.test(wanted)) return [];
  const matches = [];
  const stack = codexSessionSearchRoots(sessionsRoot, homeDir);
  while (stack.length > 0) {
    const dir = stack.pop();
    let entries = [];
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        stack.push(fullPath);
        continue;
      }
      if (entry.isFile() && codexRolloutIdFromPath(fullPath) === wanted) {
        if (!codexRolloutFileMatchesThread(fullPath, wanted)) continue;
        matches.push(fullPath);
      }
    }
  }
  return sortCodexRolloutFiles(codexZst.dropZstWithLiveSibling(matches));
}

function parseSessionStartMs(value) {
  if (value == null || value === '') return null;
  if (typeof value === 'number' && Number.isFinite(value)) {
    return value < 1e12 ? value * 1000 : value;
  }
  const raw = String(value).trim();
  if (!raw) return null;
  if (/^\d+$/.test(raw)) return parseSessionStartMs(Number(raw));
  const normalized = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(raw)
    ? raw.replace(' ', 'T') + 'Z'
    : raw;
  const ms = Date.parse(normalized);
  return Number.isFinite(ms) ? ms : null;
}

function cleanCodexCwdPath(value) {
  const raw = String(value || '').trim();
  if (!raw) return '';
  const normalized = path.normalize(path.isAbsolute(raw) ? raw : path.resolve(raw));
  return normalized.replace(/\/+$/, '') || path.parse(normalized).root;
}

function realpathCodexCwdPath(value) {
  const cleaned = cleanCodexCwdPath(value);
  if (!cleaned) return '';
  try {
    const realpath = fs.realpathSync.native ? fs.realpathSync.native(cleaned) : fs.realpathSync(cleaned);
    return cleanCodexCwdPath(realpath);
  } catch {
    // The cwd may point at an already-removed worktree; fall back to lexical aliases.
    return cleaned;
  }
}

function codexCwdVariants(value) {
  const variants = new Set();
  const add = (candidate) => {
    const cleaned = cleanCodexCwdPath(candidate);
    if (!cleaned) return;
    variants.add(cleaned);
    if (cleaned === '/tmp') variants.add('/private/tmp');
    else if (cleaned.startsWith('/tmp/')) variants.add(`/private${cleaned}`);
    else if (cleaned === '/private/tmp') variants.add('/tmp');
    else if (cleaned.startsWith('/private/tmp/')) variants.add(`/tmp/${cleaned.slice('/private/tmp/'.length)}`);
  };
  add(value);
  add(realpathCodexCwdPath(value));
  return [...variants];
}

function codexCwdMatches(a, b) {
  const bVariants = new Set(codexCwdVariants(b));
  return codexCwdVariants(a).some((variant) => bVariants.has(variant));
}

function getCodexThreadById(threadId, homeDir = process.env.HOME) {
  if (!threadId) return null;
  const rolloutFiles = findCodexSessionFilesByFilename(threadId, null, homeDir);
  for (const filePath of rolloutFiles) {
    const row = _readCodexRolloutMetadata(filePath);
    if (row) return _codexRolloutRowWithFileInfo(row, filePath, { source: 'codex_rollout' });
  }
  let db = null;
  try {
    db = openCodexStateDb(homeDir);
    if (!db) return null;
    const row = db.prepare(
      'SELECT * FROM threads WHERE id = ?'
    ).get(threadId) || null;
    return _hydrateCodexStateThreadRow(row, homeDir);
  } catch {
    return null;
  } finally {
    try { if (db) db.close(); } catch {}
  }
}

function sameResolvedPath(a, b) {
  const left = String(a || '').trim();
  const right = String(b || '').trim();
  if (!left || !right) return false;
  try {
    return fs.realpathSync(left) === fs.realpathSync(right);
  } catch {
    return path.resolve(left) === path.resolve(right);
  }
}

function checkCodexStateDbHealth(homeDir = process.env.HOME) {
  const dbPath = codexStateDbPath(homeDir);
  if (!fs.existsSync(dbPath)) {
    return { ok: false, reason: 'state_db_missing', dbPath };
  }
  let db = null;
  try {
    db = openCodexStateDb(homeDir);
    if (!db) return { ok: false, reason: 'state_db_missing', dbPath };
    const rows = db.prepare('PRAGMA integrity_check').all();
    const messages = rows
      .map((row) => String(Object.values(row)[0] || '').trim())
      .filter(Boolean);
    const ok = messages.length === 1 && messages[0].toLowerCase() === 'ok';
    return {
      ok,
      reason: ok ? 'ok' : 'state_db_corrupt',
      dbPath,
      messages: messages.slice(0, 20),
    };
  } catch (e) {
    return { ok: false, reason: 'state_db_unreadable', dbPath, error: e.message };
  } finally {
    try { if (db) db.close(); } catch {}
  }
}

function checkCodexStateIntegrity(homeDir = process.env.HOME) {
  const health = checkCodexStateDbHealth(homeDir);
  if (health.ok) return { ok: true, reason: 'ok', messages: health.messages || ['ok'] };
  return {
    ok: false,
    reason: health.reason || 'state_db_unreadable',
    messages: Array.isArray(health.messages) && health.messages.length
      ? health.messages
      : (health.error ? [health.error] : []),
    dbPath: health.dbPath || codexStateDbPath(homeDir),
    error: health.error || '',
  };
}

function inspectCodexStateThreadRollout(threadId, expectedRolloutPath, opts = {}) {
  const wanted = String(threadId || '').trim().toLowerCase();
  const rolloutPath = String(expectedRolloutPath || '').trim();
  const homeDir = opts.homeDir || process.env.HOME;
  if (!UUID_RE.test(wanted)) return { ok: false, mismatch: false, reason: 'invalid_thread_id' };
  if (!rolloutPath) return { ok: false, mismatch: false, reason: 'missing_expected_rollout_path' };
  if (!fs.existsSync(rolloutPath)) return { ok: false, mismatch: false, reason: 'expected_rollout_missing' };
  if (codexRolloutIdFromPath(rolloutPath) !== wanted) {
    return { ok: false, mismatch: false, reason: 'expected_rollout_filename_mismatch' };
  }
  if (!codexRolloutFileMatchesThread(rolloutPath, wanted)) {
    return { ok: false, mismatch: false, reason: 'expected_rollout_metadata_mismatch' };
  }

  const stateRow = getCodexStateThreadById(wanted, homeDir);
  if (!stateRow) return { ok: false, mismatch: false, reason: 'state_thread_missing' };
  const currentRolloutPath = String(stateRow.rollout_path || '').trim();
  const currentRolloutId = codexRolloutIdFromPath(currentRolloutPath);
  if (sameResolvedPath(currentRolloutPath, rolloutPath)) {
    return {
      ok: true,
      mismatch: false,
      reason: 'already_matches',
      stateRolloutPath: currentRolloutPath,
      expectedRolloutPath: rolloutPath,
      stateRolloutId: currentRolloutId,
      expectedRolloutId: wanted,
    };
  }
  return {
    ok: false,
    mismatch: true,
    reason: currentRolloutPath ? 'state_rollout_mismatch' : 'state_rollout_missing',
    stateRolloutPath: currentRolloutPath,
    expectedRolloutPath: rolloutPath,
    stateRolloutId: currentRolloutId || '',
    expectedRolloutId: wanted,
  };
}

function backupCodexStateDb(dbPath, opts = {}) {
  if (!dbPath || !fs.existsSync(dbPath)) return '';
  const backupDir = opts.backupDir || path.dirname(dbPath);
  fs.mkdirSync(backupDir, { recursive: true });
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
  const backupPath = path.join(backupDir, `state_5.sqlite.ctm-backup-${stamp}.bak`);
  fs.copyFileSync(dbPath, backupPath);
  for (const suffix of ['-wal', '-shm']) {
    const sidecar = `${dbPath}${suffix}`;
    if (fs.existsSync(sidecar)) {
      fs.copyFileSync(sidecar, `${backupPath}${suffix}`);
    }
  }
  return backupPath;
}

function _codexStateRepairValues(threadId, rolloutPath, existing = {}) {
  const rollout = _codexRolloutRowWithFileInfo(_readCodexRolloutMetadata(rolloutPath), rolloutPath);
  if (!rollout) return null;
  const createdAt = rollout.created_at || existing.created_at || Math.floor(Date.now() / 1000);
  const updatedAt = rollout.updated_at || existing.updated_at || createdAt;
  const createdAtMs = rollout._preciseCreatedMs || existing.created_at_ms || (createdAt * 1000);
  const updatedAtMs = Math.max(updatedAt * 1000, createdAtMs);
  return {
    id: String(threadId || '').trim().toLowerCase(),
    rollout_path: rolloutPath,
    created_at: createdAt,
    updated_at: updatedAt,
    source: rollout.source || existing.source || 'cli',
    model_provider: rollout.model_provider || existing.model_provider || 'openai',
    cwd: rollout.cwd || existing.cwd || '',
    title: rollout.title || existing.title || '',
    sandbox_policy: existing.sandbox_policy || '',
    approval_mode: existing.approval_mode || '',
    tokens_used: existing.tokens_used || 0,
    has_user_event: rollout.first_user_message || existing.has_user_event ? 1 : 0,
    archived: existing.archived || 0,
    archived_at: existing.archived_at || null,
    git_sha: existing.git_sha || null,
    git_branch: rollout.git_branch || existing.git_branch || null,
    git_origin_url: existing.git_origin_url || null,
    cli_version: rollout.cli_version || existing.cli_version || '',
    first_user_message: rollout.first_user_message || existing.first_user_message || '',
    agent_nickname: existing.agent_nickname || null,
    agent_role: existing.agent_role || null,
    memory_mode: existing.memory_mode || 'enabled',
    model: rollout.model || existing.model || null,
    reasoning_effort: existing.reasoning_effort || null,
    agent_path: existing.agent_path || null,
    created_at_ms: createdAtMs,
    updated_at_ms: updatedAtMs,
  };
}

function _sqliteDefaultValueForColumn(name) {
  if (name === 'source') return 'cli';
  if (name === 'model_provider') return 'openai';
  if (name === 'cwd' || name === 'title' || name === 'sandbox_policy' || name === 'approval_mode') return '';
  if (name === 'first_user_message' || name === 'cli_version') return '';
  if (name === 'memory_mode') return 'enabled';
  if (name === 'tokens_used' || name === 'has_user_event' || name === 'archived') return 0;
  if (name.endsWith('_at') || name.endsWith('_at_ms')) return Math.floor(Date.now() / 1000);
  return '';
}

function _writeCodexStateThread(db, values, exists) {
  const cols = db.prepare('PRAGMA table_info(threads)').all();
  const colNames = new Set(cols.map((col) => col.name));
  if (exists) {
    const names = Object.keys(values).filter((name) => name !== 'id' && colNames.has(name) && values[name] !== undefined);
    const sql = `UPDATE threads SET ${names.map((name) => `${name} = ?`).join(', ')} WHERE id = ?`;
    db.prepare(sql).run(...names.map((name) => values[name]), values.id);
    return 'updated';
  }

  const insertValues = {};
  for (const col of cols) {
    if (values[col.name] !== undefined) {
      insertValues[col.name] = values[col.name];
    } else if (col.notnull && col.dflt_value == null) {
      insertValues[col.name] = _sqliteDefaultValueForColumn(col.name);
    }
  }
  const names = Object.keys(insertValues);
  const placeholders = names.map(() => '?').join(', ');
  db.prepare(`INSERT INTO threads (${names.join(', ')}) VALUES (${placeholders})`)
    .run(...names.map((name) => insertValues[name]));
  return 'inserted';
}

function repairCodexStateThreadRollout(threadId, expectedRolloutPath, opts = {}) {
  const wanted = String(threadId || '').trim().toLowerCase();
  const rolloutPath = String(expectedRolloutPath || '').trim();
  const homeDir = opts.homeDir || process.env.HOME;
  if (!UUID_RE.test(wanted)) return { ok: false, repaired: false, mismatch: false, blocked: false, reason: 'invalid_thread_id' };
  if (!rolloutPath || !fs.existsSync(rolloutPath)) return { ok: false, repaired: false, mismatch: false, blocked: false, reason: 'expected_rollout_missing' };
  if (codexRolloutIdFromPath(rolloutPath) !== wanted) return { ok: false, repaired: false, mismatch: false, blocked: false, reason: 'expected_rollout_filename_mismatch' };
  if (!codexRolloutFileMatchesThread(rolloutPath, wanted)) return { ok: false, repaired: false, mismatch: false, blocked: false, reason: 'expected_rollout_metadata_mismatch' };

  const before = inspectCodexStateThreadRollout(wanted, rolloutPath, { homeDir });
  if (before.ok) return { ...before, repaired: false, blocked: false };
  if (!['state_rollout_mismatch', 'state_rollout_missing', 'state_thread_missing'].includes(before.reason)) {
    return { ...before, repaired: false, blocked: false };
  }

  const health = checkCodexStateDbHealth(homeDir);
  if (!health.ok) {
    const integrity = {
      ok: false,
      reason: health.reason,
      messages: Array.isArray(health.messages) && health.messages.length
        ? health.messages
        : (health.error ? [health.error] : []),
      dbPath: health.dbPath || codexStateDbPath(homeDir),
      error: health.error || '',
    };
    return {
      ...before,
      ok: false,
      repaired: false,
      blocked: false,
      repairAttempted: false,
      repairReason: health.reason,
      reason: health.reason,
      error: health.error || '',
      integrity,
      integrityMessages: integrity.messages,
      expectedRolloutPath: rolloutPath,
    };
  }
  if (opts.repair === false) return { ...before, repaired: false, blocked: false, repairAttempted: false };

  let db = null;
  let action = '';
  let backupPath = '';
  try {
    backupPath = backupCodexStateDb(health.dbPath, opts);
    db = openCodexStateDb(homeDir, { readonly: false });
    if (!db) {
      return { ...before, ok: false, repaired: false, blocked: false, repairAttempted: false, repairReason: 'state_db_missing', reason: 'state_db_missing', backupPath };
    }
    try { db.pragma('busy_timeout = 3000'); } catch {}
    const existing = db.prepare('SELECT * FROM threads WHERE id = ?').get(wanted) || null;
    const values = _codexStateRepairValues(wanted, rolloutPath, existing || {});
    if (!values) {
      return { ...before, ok: false, repaired: false, blocked: false, repairAttempted: false, repairReason: 'rollout_metadata_unreadable', reason: 'rollout_metadata_unreadable', backupPath };
    }
    const tx = db.transaction(() => {
      action = _writeCodexStateThread(db, values, !!existing);
    });
    tx();
  } catch (e) {
    return { ...before, ok: false, repaired: false, blocked: false, repairAttempted: true, repairReason: 'state_repair_failed', reason: 'state_repair_failed', error: e.message, backupPath };
  } finally {
    try { if (db) db.close(); } catch {}
  }

  const after = inspectCodexStateThreadRollout(wanted, rolloutPath, { homeDir });
  return {
    ...after,
    reason: after.ok ? 'state_rollout_repaired' : after.reason,
    repaired: !!after.ok,
    blocked: false,
    repairAttempted: true,
    repairAction: action,
    previousStateRolloutPath: before.stateRolloutPath || '',
    previousStateRolloutId: before.stateRolloutId || '',
    backupPath,
  };
}

function getCodexThreadResumeCwd(threadId, {
  fallbackCwd = '',
  expectedCwd = '',
  homeDir = process.env.HOME,
} = {}) {
  return getCodexThreadResumeCwdInfo(threadId, { fallbackCwd, expectedCwd, homeDir }).cwd;
}

function getCodexThreadResumeCwdInfo(threadId, {
  fallbackCwd = '',
  expectedCwd = '',
  homeDir = process.env.HOME,
} = {}) {
  const fallback = fallbackCwd || '';
  if (!threadId) {
    return {
      cwd: fallback,
      source: fallback ? 'fallback' : 'missing',
      expectedCwd: expectedCwd || '',
      stateCwd: '',
      rolloutCwd: '',
      rolloutPath: '',
      mismatch: false,
    };
  }

  // Codex `resume` uses the provider state DB as the resume locator. A rollout
  // file's session_meta.cwd is only the cwd at rollout creation time and can be
  // stale after Codex has moved the resumable thread to a different worktree.
  const stateThread = getCodexStateThreadById(threadId, homeDir);
  const stateCwd = stateThread?.cwd || '';
  let rolloutCwd = '';
  let rolloutPath = '';
  const files = findCodexSessionFiles(threadId, null, homeDir);
  for (const filePath of files) {
    const row = _readCodexRolloutMetadata(filePath);
    if (row?.cwd) {
      rolloutCwd = row.cwd;
      rolloutPath = filePath;
      break;
    }
  }

  // CTM owns the tab-to-agent-session mapping. If the provider state cwd
  // disagrees but the immutable rollout metadata agrees with CTM's mapped cwd,
  // do not let mutable provider state move the tab into another worktree.
  if (expectedCwd && stateCwd && !codexCwdMatches(expectedCwd, stateCwd)
      && rolloutCwd && codexCwdMatches(expectedCwd, rolloutCwd)) {
    return {
      cwd: expectedCwd,
      source: 'ctm_expected_rollout',
      expectedCwd,
      stateCwd,
      rolloutCwd,
      rolloutPath,
      mismatch: true,
    };
  }

  if (stateCwd) {
    return {
      cwd: stateCwd,
      source: 'codex_state',
      expectedCwd: expectedCwd || '',
      stateCwd,
      rolloutCwd,
      rolloutPath,
      mismatch: !!(expectedCwd && !codexCwdMatches(expectedCwd, stateCwd)),
    };
  }
  if (rolloutCwd) {
    return {
      cwd: rolloutCwd,
      source: 'codex_rollout',
      expectedCwd: expectedCwd || '',
      stateCwd,
      rolloutCwd,
      rolloutPath,
      mismatch: false,
    };
  }
  return {
    cwd: fallback,
    source: fallback ? 'fallback' : 'missing',
    expectedCwd: expectedCwd || '',
    stateCwd,
    rolloutCwd,
    rolloutPath,
    mismatch: false,
  };
}

function readFilePrefix(filePath, maxBytes = 64 * 1024) {
  let fd = null;
  try {
    fd = fs.openSync(filePath, 'r');
    const buf = Buffer.alloc(maxBytes);
    const bytesRead = fs.readSync(fd, buf, 0, maxBytes, 0);
    return buf.toString('utf8', 0, bytesRead);
  } catch {
    return '';
  } finally {
    try { if (fd != null) fs.closeSync(fd); } catch {}
  }
}

function codexThreadPreciseCreatedMs(row, homeDir = process.env.HOME) {
  const fallback = Number.isFinite(row?.created_at) ? row.created_at * 1000 : null;
  if (!row?.id) return fallback;
  const files = findCodexSessionFiles(row.id, null, homeDir);
  for (const filePath of files) {
    const prefix = readFilePrefix(filePath);
    if (!prefix) continue;
    for (const line of prefix.split('\n')) {
      if (!line.trim()) continue;
      let entry;
      try { entry = JSON.parse(line); } catch { continue; }
      if (entry?.type !== 'session_meta') continue;
      if (entry.payload?.id && entry.payload.id !== row.id) continue;
      return parseSessionStartMs(entry.payload?.timestamp || entry.timestamp) || fallback;
    }
  }
  return fallback;
}

function codexSessionLineageFromPayload(payload = {}) {
  const sourcePayload = payload.source && typeof payload.source === 'object' ? payload.source : null;
  const subagent = sourcePayload?.subagent || {};
  const threadSpawn = subagent.thread_spawn || {};
  const parentThreadId = payload.parent_thread_id
    || payload.parent_agent_session_id
    || threadSpawn.parent_thread_id
    || '';
  const threadSource = payload.thread_source
    || (parentThreadId ? 'subagent' : '');
  return {
    source: typeof payload.source === 'string' ? payload.source : (threadSource || ''),
    thread_source: threadSource,
    parent_agent_session_id: parentThreadId,
    parent_thread_id: parentThreadId,
    agent_nickname: payload.agent_nickname || subagent.agent_nickname || threadSpawn.agent_nickname || '',
    agent_role: payload.agent_role || subagent.agent_role || threadSpawn.agent_role || '',
  };
}

function _readCodexRolloutMetadata(filePath) {
  const readPath = _codexReadablePath(filePath);
  if (!readPath) return null;
  // 512KB covers the large turn_context blob that Codex injects before the user_message event
  const prefix = readFilePrefix(readPath, 512 * 1024);
  if (!prefix) return null;
  let meta = null;
  let firstUser = '';
  for (const line of prefix.split('\n')) {
    if (!line.trim()) continue;
    let entry;
    try { entry = JSON.parse(line); } catch { continue; }
    if (!meta && entry?.type === 'session_meta') {
      const payload = entry.payload || {};
      if (!payload.id) return null;
      const lineage = codexSessionLineageFromPayload(payload);
      meta = {
        id: payload.id,
        cwd: payload.cwd || '',
        model: payload.model || payload.model_id || '',
        model_provider: payload.model_provider || '',
        source: lineage.source,
        thread_source: lineage.thread_source,
        parent_agent_session_id: lineage.parent_agent_session_id,
        agent_nickname: lineage.agent_nickname,
        agent_role: lineage.agent_role,
        cli_version: payload.cli_version || '',
        git_branch: payload.git_branch || '',
        createdMs: parseSessionStartMs(payload.timestamp || entry.timestamp),
      };
    }
    if (!firstUser) {
      const msg = codexMessageFromEntry(entry);
      if (msg?.role === 'user') firstUser = msg.text || '';
    }
    if (meta && firstUser) break;
  }
  if (!meta || !Number.isFinite(meta.createdMs)) return null;
  return {
    id: meta.id,
    title: firstUser,
    first_user_message: firstUser,
    model: meta.model,
    model_provider: meta.model_provider,
    source: meta.source,
    thread_source: meta.thread_source,
    parent_agent_session_id: meta.parent_agent_session_id,
    parent_thread_id: meta.parent_agent_session_id,
    agent_nickname: meta.agent_nickname,
    agent_role: meta.agent_role,
    cli_version: meta.cli_version,
    cwd: meta.cwd,
    git_branch: meta.git_branch,
    created_at: Math.floor(meta.createdMs / 1000),
    updated_at: Math.floor(meta.createdMs / 1000),
    _preciseCreatedMs: meta.createdMs,
  };
}

function readCodexRolloutMetadata(filePath) {
  return _readCodexRolloutMetadata(filePath);
}

function _codexRolloutRowWithFileInfo(row, filePath, extra = {}) {
  if (!row) return null;
  const out = { ...row, ...extra };
  try {
    const st = fs.statSync(filePath);
    out.updated_at = Math.max(out.updated_at || 0, Math.floor(st.mtimeMs / 1000));
    out._jsonlPath = filePath;
    out._fileSize = st.size || 0;
    out._modifiedAt = st.mtime ? st.mtime.toISOString() : '';
  } catch {
    out._jsonlPath = filePath;
  }
  return out;
}

function _hydrateCodexStateThreadRow(row, homeDir = process.env.HOME) {
  if (!row) return null;
  const base = { ...row, _source: 'codex_state' };
  const rolloutPath = String(row.rollout_path || '').trim();
  if (!rolloutPath || !fs.existsSync(rolloutPath)) return base;
  const rollout = _readCodexRolloutMetadata(rolloutPath);
  if (!rollout) return base;
  const hydrated = _codexRolloutRowWithFileInfo(rollout, rolloutPath, {
    _source: 'codex_rollout_from_state',
    _originalThreadId: row.id || '',
    _rolloutSessionId: rollout.id || '',
  });
  return {
    ...base,
    ...hydrated,
    id: row.id || hydrated.id,
    title: hydrated.title || '',
    first_user_message: hydrated.first_user_message || '',
    model: hydrated.model || row.model || '',
    cwd: hydrated.cwd || row.cwd || '',
    git_branch: hydrated.git_branch || row.git_branch || '',
    providerResumeId: row.id && hydrated.id && row.id !== hydrated.id ? row.id : '',
  };
}

function codexRolloutSearchRoots(homeDir, minMs, maxMs) {
  const root = path.join(homeDir, '.codex', 'sessions');
  if (!fs.existsSync(root)) return [];
  const dayMs = 24 * 60 * 60 * 1000;
  const firstDay = Math.floor(minMs / dayMs) * dayMs;
  const roots = [];
  for (let day = firstDay; day <= maxMs; day += dayMs) {
    const date = new Date(day);
    const dir = path.join(
      root,
      String(date.getUTCFullYear()),
      String(date.getUTCMonth() + 1).padStart(2, '0'),
      String(date.getUTCDate()).padStart(2, '0')
    );
    if (fs.existsSync(dir)) roots.push(dir);
  }
  return roots.length > 0 ? roots : [root];
}

function findCodexThreadFromRolloutsForSession({
  cwd,
  createdAtMs,
  homeDir = process.env.HOME,
  lookbackSec = DEFAULT_CODEX_LOOKBACK_SEC,
  maxDelaySec = DEFAULT_CODEX_MAX_DELAY_SEC,
  allowAmbiguous = false,
} = {}) {
  if (!cwd) return null;
  const startMs = parseSessionStartMs(createdAtMs);
  if (!Number.isFinite(startMs)) return null;
  const minMs = startMs - (lookbackSec * 1000);
  const maxMs = startMs + (maxDelaySec * 1000);

  const matches = [];
  const stack = codexRolloutSearchRoots(homeDir, minMs, maxMs);
  while (stack.length > 0) {
    const dir = stack.pop();
    let entries = [];
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        stack.push(fullPath);
        continue;
      }
      if (!entry.isFile() || !isCodexRolloutCandidateName(entry.name)) continue;
      const row = _readCodexRolloutMetadata(fullPath);
      if (!row || !codexCwdMatches(row.cwd, cwd)) continue;
      const ms = row._preciseCreatedMs;
      if (ms < minMs || ms > maxMs) continue;
      matches.push({ row, distance: Math.abs(ms - startMs) });
    }
  }

  if (matches.length === 0) return null;
  matches.sort((a, b) => a.distance - b.distance || a.row.created_at - b.row.created_at);
  const best = matches[0];
  const tied = matches.filter(m => m.distance === best.distance);
  if (tied.length > 1 && !allowAmbiguous) return null;
  const row = { ...best.row };
  delete row._preciseCreatedMs;
  return row;
}

function findCodexThreadForSession({
  cwd,
  createdAtMs,
  homeDir = process.env.HOME,
  lookbackSec = DEFAULT_CODEX_LOOKBACK_SEC,
  maxDelaySec = DEFAULT_CODEX_MAX_DELAY_SEC,
  allowAmbiguous = false,
} = {}) {
  if (!cwd) return null;
  const startMs = parseSessionStartMs(createdAtMs);
  if (!Number.isFinite(startMs)) return null;
  const startSec = Math.floor(startMs / 1000);
  const cwdVariants = codexCwdVariants(cwd);
  const fallbackFromRollouts = () => findCodexThreadFromRolloutsForSession({
    cwd,
    createdAtMs,
    homeDir,
    lookbackSec,
    maxDelaySec,
    allowAmbiguous,
  });
  if (cwdVariants.length === 0) return null;
  const rolloutMatch = fallbackFromRollouts();
  if (rolloutMatch) return rolloutMatch;
  let db = null;
  try {
    db = openCodexStateDb(homeDir);
    if (!db) return null;
    const cwdPlaceholders = cwdVariants.map(() => '?').join(', ');
    const rows = db.prepare(
      `SELECT rowid, id, title, first_user_message, model, cwd, git_branch, created_at, updated_at
         FROM threads
        WHERE cwd IN (${cwdPlaceholders}) AND created_at >= ? AND created_at <= ?
        ORDER BY ABS(created_at - ?) ASC, created_at ASC`
    ).all(...cwdVariants, startSec - lookbackSec, startSec + maxDelaySec, startSec);
    if (rows.length === 0) return null;
    const best = rows[0];
    const bestDistance = Math.abs((best.created_at || 0) - startSec);
    const tied = rows.filter(r => Math.abs((r.created_at || 0) - startSec) === bestDistance);
    if (tied.length > 1 && !allowAmbiguous) {
      const precise = tied
        .map(row => ({ row, distance: Math.abs((codexThreadPreciseCreatedMs(row, homeDir) || 0) - startMs) }))
        .sort((a, b) => a.distance - b.distance);
      const preciseBest = precise[0];
      const preciseTied = precise.filter(p => p.distance === preciseBest.distance);
      if (preciseTied.length === 1) return _hydrateCodexStateThreadRow(preciseBest.row, homeDir);
      return null;
    }
    return _hydrateCodexStateThreadRow(best, homeDir);
  } catch {
    return null;
  } finally {
    try { if (db) db.close(); } catch {}
  }
}

const CODEX_PROVIDER_CONTEXT_PATTERNS = [
  /^<environment_context>/i,
  /^<permissions instructions>/i,
  /^<collaboration_mode>/i,
  /^<skills_instructions>/i,
  /^<plugins_instructions>/i,
  /^<skill>\s*<name>/i,
  /^# AGENTS\.md instructions for\b/i,
];

function classifyCodexUserText(text) {
  const cleaned = String(text || '').trim();
  if (!cleaned) return 'empty';
  // Codex stores some model-input context as user-role response_items. That is
  // useful for resume replay, but it is not a prompt the human typed.
  if (CODEX_PROVIDER_CONTEXT_PATTERNS.some((pattern) => pattern.test(cleaned))) {
    return 'provider_context';
  }
  // Interrupted turns are lifecycle/control state. Codex may persist them as
  // user-role response_items so the model sees resume context; CTM must not
  // surface them as human prompts.
  if (/^<turn_aborted>\s*[\s\S]*?<\/turn_aborted>\s*$/i.test(cleaned)) {
    return 'provider_control';
  }
  return 'human_prompt';
}

function isCodexProviderControlUserText(text) {
  return classifyCodexUserText(text) === 'provider_control';
}

function isCodexSyntheticUserText(text) {
  const kind = classifyCodexUserText(text);
  return kind === 'provider_control' || kind === 'provider_context';
}

function cleanCodexUserText(text) {
  let cleaned = String(text || '').trim();
  if (!cleaned) return '';
  if (classifyCodexUserText(cleaned) !== 'human_prompt') return '';

  // Image turns include a wrapper plus the typed prompt. Drop only the image
  // wrapper/placeholder so the prompt itself remains navigable.
  cleaned = cleaned
    .replace(/<image\b[\s\S]*?<\/image>\s*/gi, '')
    .replace(/\[input_image\]\s*/g, '')
    .trim();

  return classifyCodexUserText(cleaned) === 'human_prompt' ? cleaned : '';
}

function codexUserKey(text) {
  return String(text || '').replace(/\s+/g, ' ').trim().slice(0, 500);
}

const CODEX_USER_ECHO_DEDUP_WINDOW_MS = 5000;

function _codexTimestampMs(timestamp) {
  const value = String(timestamp || '').trim();
  if (!value) return null;
  const ms = Date.parse(value);
  return Number.isFinite(ms) ? ms : null;
}

function createCodexUserDeduper(seedMessages = [], options = {}) {
  const windowMs = Math.max(0, Number(options.windowMs ?? CODEX_USER_ECHO_DEDUP_WINDOW_MS));
  const byText = new Map();

  const rememberKey = (key, timestamp) => {
    if (!key) return false;
    const tsMs = _codexTimestampMs(timestamp);
    let seen = byText.get(key);
    if (!seen) {
      seen = [];
      byText.set(key, seen);
    }
    for (const prior of seen) {
      if (tsMs == null || prior == null) return false;
      if (Math.abs(tsMs - prior) <= windowMs) return false;
    }
    seen.push(tsMs);
    if (seen.length > 256) seen.splice(0, seen.length - 256);
    return true;
  };

  const deduper = {
    remember(text, timestamp) {
      return rememberKey(codexUserKey(text), timestamp);
    },
    has(text, timestamp) {
      const key = codexUserKey(text);
      if (!key) return false;
      const tsMs = _codexTimestampMs(timestamp);
      const seen = byText.get(key) || [];
      return seen.some((prior) => {
        if (tsMs == null || prior == null) return true;
        return Math.abs(tsMs - prior) <= windowMs;
      });
    },
    seed(message) {
      if (!message) return false;
      const text = message.text || message.content || message.message || '';
      const timestamp = message.timestamp || message.created_at || message.createdAt || '';
      return this.remember(text, timestamp);
    },
  };

  if (seedMessages instanceof Set) {
    for (const key of seedMessages) rememberKey(key, null);
  } else {
    for (const message of Array.isArray(seedMessages) ? seedMessages : []) deduper.seed(message);
  }
  return deduper;
}

function normalizeCodexUserDeduper(value) {
  if (value && typeof value.remember === 'function') return value;
  return createCodexUserDeduper(value instanceof Set ? value : []);
}

function codexInputText(content) {
  if (typeof content === 'string') return content;
  if (!Array.isArray(content)) return '';
  return content
    .filter((block) => block && (block.type === 'input_text' || block.type === 'text') && block.text)
    .map((block) => block.text)
    .join('\n');
}

function codexAssistantText(content) {
  if (!Array.isArray(content)) return '';
  return content
    .filter((block) => block && block.type === 'output_text' && block.text)
    .map((block) => block.text)
    .join('\n')
    .trim();
}

// Structured capture for Codex rollout items the legacy text parser drops:
// reasoning, function/tool/shell calls + outputs, apply_patch, web search,
// and compaction markers. Returns ONE message, an ARRAY of messages, or null.
// Kept separate from codexMessageFromEntry on purpose — session-stream,
// transcript-store, and relink-audit consume that function directly and must
// keep their user/assistant-only behavior. Only appendCodexEntryMessage (the
// conversation parse path) consults this.
function codexStructuredMessageFromEntry(entry) {
  if (!entry || typeof entry !== 'object') return null;
  const timestamp = entry.timestamp || '';

  if (entry.type === 'compacted') {
    const out = [structuredCapture.compactBoundaryMessage({ provider: 'codex', timestamp })];
    const summaryText = typeof entry.payload?.message === 'string' ? entry.payload.message.trim() : '';
    if (summaryText) {
      out.push(structuredCapture.compactSummaryMessage({ provider: 'codex', text: summaryText, timestamp }));
    }
    return out;
  }

  if (entry.type !== 'response_item' || !entry.payload || typeof entry.payload !== 'object') return null;
  const p = entry.payload;

  switch (p.type) {
    case 'reasoning': {
      const blockText = (blocks) => Array.isArray(blocks)
        ? blocks.filter((b) => b && typeof b.text === 'string' && b.text).map((b) => b.text).join('\n')
        : '';
      const text = (blockText(p.summary) || blockText(p.content) || '').trim();
      if (text) return structuredCapture.reasoningMessage({ provider: 'codex', text, timestamp });
      if (p.encrypted_content) return structuredCapture.reasoningMessage({ provider: 'codex', encrypted: true, timestamp });
      return null;
    }
    case 'function_call': {
      const name = String(p.name || '').trim();
      let parsedArgs = null;
      if (typeof p.arguments === 'string') { try { parsedArgs = JSON.parse(p.arguments); } catch {} }
      else if (p.arguments && typeof p.arguments === 'object') parsedArgs = p.arguments;
      const patch = structuredCapture.detectApplyPatch({
        name,
        argumentsJson: p.arguments,
        command: parsedArgs && parsedArgs.command,
      });
      if (patch) {
        return structuredCapture.patchMessage({
          provider: 'codex', patchText: patch.patchText, files: patch.files, callId: p.call_id, timestamp,
        });
      }
      if ((name === 'shell' || name === 'container.exec' || name === 'exec_command')
          && parsedArgs && parsedArgs.command) {
        return structuredCapture.shellMessage({ provider: 'codex', command: parsedArgs.command, callId: p.call_id, timestamp });
      }
      return structuredCapture.toolCallMessage({ provider: 'codex', tool: name || 'tool', callId: p.call_id, args: p.arguments, timestamp });
    }
    case 'function_call_output':
    case 'custom_tool_call_output': {
      let outputText = typeof p.output === 'string' ? p.output
        : (p.output != null ? JSON.stringify(p.output) : '');
      let exitCode;
      let durationMs;
      if (outputText.startsWith('{')) {
        // Shell outputs arrive as a JSON envelope: {"output", "metadata": {"exit_code", "duration_seconds"}}
        try {
          const parsed = JSON.parse(outputText);
          if (parsed && typeof parsed === 'object') {
            if (typeof parsed.output === 'string') outputText = parsed.output;
            const md = parsed.metadata || {};
            if (Number.isFinite(Number(md.exit_code))) exitCode = Number(md.exit_code);
            if (Number.isFinite(Number(md.duration_seconds))) durationMs = Math.round(Number(md.duration_seconds) * 1000);
          }
        } catch {}
      }
      return structuredCapture.toolResultMessage({
        provider: 'codex', callId: p.call_id, output: outputText, exitCode, durationMs, timestamp,
      });
    }
    case 'local_shell_call': {
      const action = p.action || {};
      const command = Array.isArray(action.command) ? action.command : [];
      const patch = structuredCapture.detectApplyPatch({ command });
      if (patch) {
        return structuredCapture.patchMessage({
          provider: 'codex', patchText: patch.patchText, files: patch.files, callId: p.call_id, timestamp,
        });
      }
      return structuredCapture.shellMessage({ provider: 'codex', command, callId: p.call_id, timestamp });
    }
    case 'custom_tool_call': {
      const name = String(p.name || '').trim();
      const patch = structuredCapture.detectApplyPatch({ name, input: p.input });
      if (patch) {
        return structuredCapture.patchMessage({
          provider: 'codex', patchText: patch.patchText, files: patch.files, callId: p.call_id, timestamp,
        });
      }
      return structuredCapture.toolCallMessage({ provider: 'codex', tool: name || 'tool', callId: p.call_id, args: p.input, timestamp });
    }
    case 'web_search_call': {
      const action = p.action || {};
      const query = String(action.query || p.query || '').trim();
      return structuredCapture.webSearchMessage({ provider: 'codex', query: query || 'web search', timestamp });
    }
    default:
      return null;
  }
}

function codexMessageFromEntry(entry) {
  if (!entry || typeof entry !== 'object') return null;

  if (entry.type === 'event_msg' && entry.payload?.type === 'user_message') {
    const text = cleanCodexUserText(entry.payload.message);
    return text ? { role: 'user', text, timestamp: entry.timestamp || '' } : null;
  }

  if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'user') {
    const text = cleanCodexUserText(codexInputText(entry.payload.content));
    return text ? { role: 'user', text, timestamp: entry.timestamp || '' } : null;
  }

  if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'assistant') {
    const text = codexAssistantText(entry.payload.content);
    return text ? { role: 'assistant', text, timestamp: entry.timestamp || '' } : null;
  }

  return null;
}

function codexSessionMetaFromEntry(entry) {
  if (!entry || entry.type !== 'session_meta') return null;
  const payload = entry.payload || {};
  const lineage = codexSessionLineageFromPayload(payload);
  return {
    id: payload.id || '',
    cwd: payload.cwd || '',
    model: payload.model || payload.model_id || '',
    git_branch: payload.git_branch || '',
    timestamp: payload.timestamp || entry.timestamp || '',
    source: lineage.source,
    thread_source: lineage.thread_source,
    parent_agent_session_id: lineage.parent_agent_session_id,
    parent_thread_id: lineage.parent_thread_id,
    agent_nickname: lineage.agent_nickname,
    agent_role: lineage.agent_role,
  };
}

function appendCodexEntryMessage(entry, messages, seenUsers) {
  if (!entry || typeof entry !== 'object') return false;
  const msg = codexMessageFromEntry(entry);
  if (!msg) {
    // Structured capture: items the text parser drops (reasoning, tool/shell
    // calls + outputs, patches, web search, compaction). role:'system' rows —
    // the user echo deduper does not apply.
    if (!structuredCapture.structuredCaptureEnabled()) return false;
    const structured = codexStructuredMessageFromEntry(entry);
    if (!structured) return false;
    for (const m of Array.isArray(structured) ? structured : [structured]) messages.push(m);
    return true;
  }
  if (msg.role === 'user') {
    const deduper = normalizeCodexUserDeduper(seenUsers);
    if (!deduper.remember(msg.text, msg.timestamp)) return false;
  }
  messages.push(msg);
  return true;
}

function parseCodexJsonlRecord(line, messages, state) {
  if (!line || !line.trim()) return { valid: true, messageAdded: false };
  let entry;
  try { entry = JSON.parse(line); } catch { return { valid: false, messageAdded: false }; }
  if (!entry || typeof entry !== 'object') return { valid: false, messageAdded: false };
  if (!state.sessionMeta) {
    const meta = codexSessionMetaFromEntry(entry);
    if (meta?.id) state.sessionMeta = meta;
  }
  return { valid: true, messageAdded: appendCodexEntryMessage(entry, messages, state.seenUsers) };
}

function parseCodexJsonlLine(line, messages, state) {
  return parseCodexJsonlRecord(line, messages, state).messageAdded;
}

function parseCodexJsonlIntoMessages(content, messages, options = {}) {
  const raw = String(content || '');
  const state = {
    seenUsers: normalizeCodexUserDeduper(options.codexUserDeduper || options.seenUsers),
    sessionMeta: null,
  };
  const before = messages.length;
  let linesRead = 0;
  let completeBytesRead = 0;

  let start = 0;
  while (start < raw.length) {
    const end = raw.indexOf('\n', start);
    if (end === -1) {
      const line = raw.slice(start);
      if (line) {
        linesRead++;
        const parsed = parseCodexJsonlRecord(line, messages, state);
        if (parsed.valid) completeBytesRead += Buffer.byteLength(line);
      }
      break;
    }
    const line = raw.slice(start, end);
    completeBytesRead += Buffer.byteLength(line) + 1;
    if (line) {
      linesRead++;
      parseCodexJsonlRecord(line, messages, state);
    }
    start = end + 1;
  }

  return {
    bytesRead: Buffer.byteLength(raw),
    completeBytesRead,
    linesRead,
    messagesAdded: messages.length - before,
    sessionMeta: state.sessionMeta,
  };
}

function parseCodexJsonlFileIntoMessages(filePath, messages, options = {}) {
  const readPath = _codexReadablePath(filePath);
  if (!readPath) {
    return { bytesRead: 0, completeBytesRead: 0, linesRead: 0, messagesAdded: 0, sessionMeta: null, failed: 1, compressed: true };
  }
  const state = {
    seenUsers: normalizeCodexUserDeduper(options.codexUserDeduper || options.seenUsers),
    sessionMeta: null,
  };
  const before = messages.length;
  const chunkSize = options.chunkSize || 1024 * 1024;
  const buf = Buffer.alloc(chunkSize);
  const decoder = new StringDecoder('utf8');
  let bytesReadTotal = 0;
  let completeBytesRead = 0;
  let linesRead = 0;
  let partial = '';
  let fd = null;

  try {
    fd = fs.openSync(readPath, 'r');
    while (true) {
      const bytesRead = fs.readSync(fd, buf, 0, buf.length, null);
      if (bytesRead <= 0) break;
      bytesReadTotal += bytesRead;
      const text = partial + decoder.write(buf.subarray(0, bytesRead));
      let start = 0;
      while (true) {
        const end = text.indexOf('\n', start);
        if (end === -1) break;
        const line = text.slice(start, end);
        completeBytesRead += Buffer.byteLength(line) + 1;
        if (line) {
          linesRead++;
          parseCodexJsonlRecord(line, messages, state);
        }
        start = end + 1;
      }
      partial = text.slice(start);
    }
    const tail = decoder.end();
    if (tail) partial += tail;
    if (partial && partial.trim()) {
      linesRead++;
      const parsed = parseCodexJsonlRecord(partial, messages, state);
      if (parsed.valid) completeBytesRead += Buffer.byteLength(partial);
    }
  } finally {
    try { if (fd != null) fs.closeSync(fd); } catch {}
  }

  return {
    bytesRead: bytesReadTotal,
    completeBytesRead,
    linesRead,
    messagesAdded: messages.length - before,
    sessionMeta: state.sessionMeta,
  };
}

async function parseCodexJsonlFileIntoMessagesAsync(filePath, messages, options = {}) {
  const state = {
    seenUsers: normalizeCodexUserDeduper(options.codexUserDeduper || options.seenUsers),
    sessionMeta: null,
  };
  const before = messages.length;
  const chunkSize = options.chunkSize || 1024 * 1024;
  const yieldAfterMs = Math.max(5, Number(options.yieldAfterMs || 25));
  const buf = Buffer.alloc(chunkSize);
  const decoder = new StringDecoder('utf8');
  let bytesReadTotal = 0;
  let completeBytesRead = 0;
  let linesRead = 0;
  let partial = '';
  let lastYieldAt = Date.now();
  let fh = null;

  async function maybeYield() {
    if (Date.now() - lastYieldAt < yieldAfterMs) return;
    await new Promise((resolve) => setImmediate(resolve));
    lastYieldAt = Date.now();
  }

  const readPath = _codexReadablePath(filePath);
  if (!readPath) {
    return { bytesRead: 0, completeBytesRead: 0, linesRead: 0, messagesAdded: 0, sessionMeta: null, failed: 1, compressed: true };
  }
  try {
    fh = await fs.promises.open(readPath, 'r');
    let position = 0;
    while (true) {
      const { bytesRead } = await fh.read(buf, 0, buf.length, position);
      if (bytesRead <= 0) break;
      position += bytesRead;
      bytesReadTotal += bytesRead;
      const text = partial + decoder.write(buf.subarray(0, bytesRead));
      let start = 0;
      while (true) {
        const end = text.indexOf('\n', start);
        if (end === -1) break;
        const line = text.slice(start, end);
        completeBytesRead += Buffer.byteLength(line) + 1;
        if (line) {
          linesRead++;
          parseCodexJsonlRecord(line, messages, state);
        }
        start = end + 1;
      }
      partial = text.slice(start);
      await maybeYield();
    }
    const tail = decoder.end();
    if (tail) partial += tail;
    if (partial && partial.trim()) {
      linesRead++;
      const parsed = parseCodexJsonlRecord(partial, messages, state);
      if (parsed.valid) completeBytesRead += Buffer.byteLength(partial);
    }
  } finally {
    try { if (fh) await fh.close(); } catch {}
  }

  return {
    bytesRead: bytesReadTotal,
    completeBytesRead,
    linesRead,
    messagesAdded: messages.length - before,
    sessionMeta: state.sessionMeta,
  };
}

function parseCodexJsonlTailFileIntoMessages(filePath, messages, options = {}) {
  const maxBytes = Math.max(1, Number(options.maxBytes || 10 * 1024 * 1024));
  // A compressed archive can't be range-read — tail the materialized copy.
  const readPath = _codexReadablePath(filePath);
  if (!readPath) {
    return { bytesRead: 0, linesRead: 0, messagesAdded: 0, fileSize: 0, partial: false, failed: 1, compressed: true };
  }
  let stat;
  try { stat = fs.statSync(readPath); } catch {
    return { bytesRead: 0, linesRead: 0, messagesAdded: 0, fileSize: 0, partial: false, failed: 1 };
  }
  const size = Number(stat.size || 0);
  if (size <= 0) {
    return { bytesRead: 0, linesRead: 0, messagesAdded: 0, fileSize: size, partial: false };
  }

  const readSize = Math.min(maxBytes, size);
  const startOffset = Math.max(0, size - readSize);
  let fd = null;
  try {
    fd = fs.openSync(readPath, 'r');
    const buf = Buffer.alloc(readSize);
    const bytesRead = fs.readSync(fd, buf, 0, readSize, startOffset);
    let content = buf.toString('utf8', 0, bytesRead);
    if (startOffset > 0) {
      const nlIdx = content.indexOf('\n');
      content = nlIdx >= 0 ? content.slice(nlIdx + 1) : '';
    }
    const before = messages.length;
    const parsed = parseCodexJsonlIntoMessages(content, messages, { codexUserDeduper: options.codexUserDeduper || options.seenUsers });
    return {
      ...parsed,
      bytesRead,
      messagesAdded: messages.length - before,
      fileSize: size,
      partial: startOffset > 0,
    };
  } catch {
    return { bytesRead: 0, linesRead: 0, messagesAdded: 0, fileSize: size, partial: startOffset > 0, failed: 1 };
  } finally {
    try { if (fd != null) fs.closeSync(fd); } catch {}
  }
}

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

function codexSearchSnippet(text, terms) {
  const raw = String(text || '').replace(/\s+/g, ' ').trim();
  const normalized = normalizeCodexSearchValue(raw);
  let idx = -1;
  for (const term of terms || []) {
    idx = normalized.indexOf(term);
    if (idx >= 0) break;
  }
  if (idx < 0) return raw.slice(0, 180);
  const start = Math.max(0, idx - 60);
  const end = Math.min(raw.length, idx + 140);
  return (start > 0 ? '...' : '') + raw.slice(start, end).trim() + (end < raw.length ? '...' : '');
}

function codexSearchMatchForMessage(msg, terms, order) {
  const text = msg?.text || '';
  const normalized = normalizeCodexSearchValue(text);
  if (!terms.every(term => normalized.includes(term))) return null;

  const rawLower = String(text).toLowerCase();
  let score = msg.role === 'user' ? 20 : 0;
  for (const term of terms) {
    if (rawLower.includes(`$${term}`) || rawLower.includes(`/${term}`)) score += 50;
    const wordRe = new RegExp(`(^|[^a-z0-9_-])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^a-z0-9_-]|$)`, 'i');
    if (wordRe.test(text)) score += 10;
  }

  return {
    role: msg.role,
    timestamp: msg.timestamp || '',
    snippet: codexSearchSnippet(text, terms),
    _rankScore: score,
    _order: order,
  };
}

function searchCodexRolloutFileMessages(filePath, terms, row) {
  const state = { codexUserDeduper: createCodexUserDeduper(), sessionMeta: null };
  const matches = [];
  const readPath = _codexReadablePath(filePath);
  if (!readPath) return matches;
  const chunkSize = 1024 * 1024;
  const buf = Buffer.alloc(chunkSize);
  const decoder = new StringDecoder('utf8');
  let partial = '';
  let fd = null;
  let order = 0;

  try {
    fd = fs.openSync(readPath, 'r');
    while (true) {
      const bytesRead = fs.readSync(fd, buf, 0, buf.length, null);
      if (bytesRead <= 0) break;
      const text = partial + decoder.write(buf.subarray(0, bytesRead));
      let start = 0;
      while (true) {
        const end = text.indexOf('\n', start);
        if (end === -1) break;
        const line = text.slice(start, end);
        if (line) {
          let entry;
          try { entry = JSON.parse(line); } catch { entry = null; }
          if (entry) {
            if (!state.sessionMeta) {
              const meta = codexSessionMetaFromEntry(entry);
              if (meta?.id) state.sessionMeta = meta;
            }
            const msg = codexMessageFromEntry(entry);
            if (msg) {
              if (msg.role === 'user') {
                if (!state.codexUserDeduper.remember(msg.text, msg.timestamp)) {
                  start = end + 1;
                  continue;
                }
              }
              const match = codexSearchMatchForMessage(msg, terms, order++);
              if (match) matches.push(match);
            }
          }
        }
        start = end + 1;
      }
      partial = text.slice(start);
    }
    const tail = decoder.end();
    if (tail) partial += tail;
    if (partial && partial.trim()) {
      let entry;
      try { entry = JSON.parse(partial); } catch { entry = null; }
      if (entry) {
        if (!state.sessionMeta) {
          const meta = codexSessionMetaFromEntry(entry);
          if (meta?.id) state.sessionMeta = meta;
        }
        const msg = codexMessageFromEntry(entry);
        if (msg) {
          let shouldConsider = true;
          if (msg.role === 'user') {
            if (!state.codexUserDeduper.remember(msg.text, msg.timestamp)) {
              shouldConsider = false;
            }
          }
          const match = shouldConsider ? codexSearchMatchForMessage(msg, terms, order++) : null;
          if (match) matches.push(match);
        }
      }
    }
  } finally {
    try { if (fd != null) fs.closeSync(fd); } catch {}
  }

  if (matches.length === 0) return null;
  const meta = state.sessionMeta || {};
  const id = row?.id || meta.id || codexRolloutIdFromPath(filePath);
  if (!id) return null;
  let stat = null;
  try { stat = fs.statSync(filePath); } catch {}
  const updatedAt = stat ? Math.floor(stat.mtimeMs / 1000) : (row?.updated_at || 0);
  return {
    id,
    title: row?.title || row?.first_user_message || '',
    first_user_message: row?.first_user_message || '',
    model: row?.model || meta.model || '',
    cwd: row?.cwd || meta.cwd || '',
    git_branch: row?.git_branch || meta.git_branch || '',
    created_at: row?.created_at || Math.floor(parseSessionStartMs(meta.timestamp) / 1000) || 0,
    updated_at: updatedAt,
    _jsonlPath: filePath,
    _fileSize: stat?.size || 0,
    _match: matches.sort((a, b) => (b._rankScore || 0) - (a._rankScore || 0) || (b._order || 0) - (a._order || 0))[0],
    _matchCount: matches.length,
  };
}

function searchCodexRolloutMessages(query, {
  homeDir = process.env.HOME,
  maxFiles = 100,
  limit = 50,
} = {}) {
  const terms = normalizeCodexSearchValue(query).split(/\s+/).filter(t => t.length >= 2);
  if (terms.length === 0) return [];
  const rows = listCodexSessionsFromRollouts(homeDir, maxFiles);
  const hits = [];
  for (const row of rows) {
    if (hits.length >= limit) break;
    const filePath = row._jsonlPath;
    if (!filePath) continue;
    const hit = searchCodexRolloutFileMessages(filePath, terms, row);
    if (hit) hits.push(hit);
  }
  return hits.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0)).slice(0, limit);
}

function mergeCodexRecentThreadRow(existing, incoming) {
  if (!existing) return incoming;
  const providerResumeIds = new Set([
    ...(existing.providerResumeIds || []),
    ...(incoming.providerResumeIds || []),
  ].filter(Boolean));
  const preferIncoming = (incoming.updated_at || 0) > (existing.updated_at || 0)
    || (!existing.first_user_message && incoming.first_user_message)
    || (incoming._originalThreadId === incoming.id && existing._originalThreadId !== existing.id);
  const base = preferIncoming ? incoming : existing;
  const other = preferIncoming ? existing : incoming;
  return {
    ...base,
    title: base.title || other.title || '',
    first_user_message: base.first_user_message || other.first_user_message || '',
    model: base.model || other.model || '',
    cwd: base.cwd || other.cwd || '',
    git_branch: base.git_branch || other.git_branch || '',
    created_at: base.created_at || other.created_at || 0,
    updated_at: Math.max(base.updated_at || 0, other.updated_at || 0),
    providerResumeId: base.providerResumeId || other.providerResumeId || '',
    providerResumeIds: [...providerResumeIds],
    thread_source: base.thread_source || other.thread_source || '',
    parent_agent_session_id: base.parent_agent_session_id || base.parent_thread_id || other.parent_agent_session_id || other.parent_thread_id || '',
    parent_thread_id: base.parent_thread_id || base.parent_agent_session_id || other.parent_thread_id || other.parent_agent_session_id || '',
    agent_nickname: base.agent_nickname || other.agent_nickname || '',
    agent_role: base.agent_role || other.agent_role || '',
    _jsonlPath: base._jsonlPath || other._jsonlPath || '',
    _fileSize: base._fileSize || other._fileSize || 0,
    _modifiedAt: base._modifiedAt || other._modifiedAt || '',
  };
}

function normalizeCodexRecentThreadRows(rows, getRolloutInfo = () => ({})) {
  const byCanonicalId = new Map();
  for (const row of Array.isArray(rows) ? rows : []) {
    const originalThreadId = String(row?.id || '').trim();
    if (!originalThreadId) continue;
    const rolloutInfo = getRolloutInfo(row) || {};
    const rolloutPath = rolloutInfo.rolloutPath || row.rollout_path || row._jsonlPath || '';
    const canonicalId = codexRolloutIdFromPath(rolloutPath) || originalThreadId;
    const providerResumeId = originalThreadId !== canonicalId ? originalThreadId : (row.providerResumeId || '');
    const providerResumeIds = [];
    if (providerResumeId) providerResumeIds.push(providerResumeId);
    for (const id of row.providerResumeIds || []) {
      const clean = String(id || '').trim();
      if (clean && clean !== canonicalId && !providerResumeIds.includes(clean)) providerResumeIds.push(clean);
    }
    const normalized = {
      ...row,
      id: canonicalId,
      providerResumeId,
      providerResumeIds,
      thread_source: row.thread_source || '',
      parent_agent_session_id: row.parent_agent_session_id || row.parent_thread_id || '',
      parent_thread_id: row.parent_thread_id || row.parent_agent_session_id || '',
      agent_nickname: row.agent_nickname || '',
      agent_role: row.agent_role || '',
      _originalThreadId: originalThreadId,
      _jsonlPath: rolloutPath,
      _fileSize: rolloutInfo.fileSize || row._fileSize || 0,
      _modifiedAt: rolloutInfo.modifiedAt || row._modifiedAt || '',
    };
    byCanonicalId.set(canonicalId, mergeCodexRecentThreadRow(byCanonicalId.get(canonicalId), normalized));
  }
  return [...byCanonicalId.values()].sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
}

function commandLooksLikeCodex(value) {
  return /(^|\/)codex(\s|$)/i.test(String(value || ''));
}

function findCodexThreadForCtmSession({
  task,
  row,
  liveSession,
  cwd,
  createdAtMs,
  homeDir = process.env.HOME,
  lookbackSec = DEFAULT_CODEX_LOOKBACK_SEC,
  maxDelaySec = DEFAULT_CODEX_MAX_DELAY_SEC,
  allowAmbiguous = false,
} = {}) {
  const explicitId = row?.provider_resume_id || row?.agent_session_id || liveSession?._providerResumeId || liveSession?._claudeSessionId || '';
  if (explicitId) {
    const direct = getCodexThreadById(explicitId, homeDir);
    if (direct) return direct;
    const files = findCodexSessionFiles(explicitId, null, homeDir);
    if (files.length > 0) {
      const rollout = _readCodexRolloutMetadata(files[0]);
      if (rollout) {
        const hydrated = _codexRolloutRowWithFileInfo(rollout, files[0], { _source: 'codex_rollout' });
        return {
          ...hydrated,
          id: explicitId,
          providerResumeId: explicitId !== hydrated.id ? explicitId : '',
          _rolloutSessionId: hydrated.id || '',
        };
      }
      return {
        id: explicitId,
        title: row?.title || task?.label || '',
        first_user_message: row?.first_message || '',
        model: row?.model || liveSession?.model_id || '',
        cwd: cwd || row?.cwd || task?.cwd || liveSession?.cwd || '',
        git_branch: row?.git_branch || liveSession?.branch || '',
        created_at: parseSessionStartMs(createdAtMs || row?.created_at || task?.created_at || liveSession?.createdAt),
        updated_at: parseSessionStartMs(row?.modified_at || task?.created_at || liveSession?.createdAt),
      };
    }
  }

  const provider = String(row?.provider || task?.provider || liveSession?._providerId || '');
  const isCodex = provider === 'codex' || commandLooksLikeCodex(task?.cmd) || commandLooksLikeCodex(liveSession?.cmd);
  if (!isCodex) return null;

  const lookupCwd = cwd || task?.cwd || row?.cwd || row?.project_path || liveSession?.cwd || '';
  const lookupCreatedAt = createdAtMs || task?.created_at || row?.created_at || liveSession?._ctmOriginalCreatedAt || liveSession?.createdAt;
  if (!lookupCwd || lookupCreatedAt == null) return null;

  return findCodexThreadForSession({
    cwd: lookupCwd,
    createdAtMs: lookupCreatedAt,
    homeDir,
    lookbackSec,
    maxDelaySec,
    allowAmbiguous,
  });
}

// Scan ~/.codex/sessions/**/*.jsonl and return session metadata rows sorted by mtime DESC.
// Rollout JSONL is the authoritative recent-session source. state_5.sqlite can
// be asked for resume aliases explicitly, but stale aliases must not decide
// which transcript/title CTM displays.
function listCodexSessionsFromRollouts(homeDir = process.env.HOME, limit = 50, options = {}) {
  const useStateRedirects = !!options.useStateRedirects;
  const root = path.join(homeDir, '.codex', 'sessions');
  if (!fs.existsSync(root)) return [];
  const resultsByPath = new Map();
  const redirectedResumeIdsByPath = new Map();
  const addRedirect = (targetPath, resumeId) => {
    const key = path.resolve(targetPath);
    const ids = redirectedResumeIdsByPath.get(key) || [];
    if (resumeId && !ids.includes(resumeId)) ids.push(resumeId);
    redirectedResumeIdsByPath.set(key, ids);
  };
  const addRow = (filePath, row) => {
    const key = path.resolve(filePath);
    const existing = resultsByPath.get(key);
    if (existing) {
      const ids = new Set([...(existing.providerResumeIds || []), ...(row.providerResumeIds || [])]);
      resultsByPath.set(key, { ...existing, ...row, providerResumeIds: [...ids] });
    } else {
      resultsByPath.set(key, row);
    }
  };
  const stack = [root];
  while (stack.length > 0) {
    const dir = stack.pop();
    let entries = [];
    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) { stack.push(fullPath); continue; }
      if (!entry.isFile() || !isCodexRolloutCandidateName(entry.name)) continue;
      let row = _readCodexRolloutMetadata(fullPath);
      if (!row) continue;
      row = _codexRolloutRowWithFileInfo(row, fullPath);
      if (useStateRedirects) {
        const stateRolloutPath = findCodexStateRolloutPath(row.id, homeDir);
        if (stateRolloutPath && path.resolve(stateRolloutPath) !== path.resolve(fullPath)) {
          addRedirect(stateRolloutPath, row.id);
          continue;
        }
      }
      addRow(fullPath, row);
    }
  }
  if (useStateRedirects) {
    for (const [targetPath, resumeIds] of redirectedResumeIdsByPath.entries()) {
      let row = resultsByPath.get(targetPath);
      if (!row && fs.existsSync(targetPath)) {
        row = _readCodexRolloutMetadata(targetPath);
        if (row) row = _codexRolloutRowWithFileInfo(row, targetPath);
      }
      if (!row) continue;
      const ids = new Set([...(row.providerResumeIds || []), ...resumeIds].filter(id => id && id !== row.id));
      row.providerResumeIds = [...ids];
      row.providerResumeId = row.providerResumeId || row.providerResumeIds[0] || '';
      addRow(targetPath, row);
    }
  }
  const results = [...resultsByPath.values()];
  results.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0));
  return results.slice(0, limit);
}

module.exports = {
  cleanCodexUserText,
  codexInputText,
  codexMessageFromEntry,
  codexStructuredMessageFromEntry,
  isCodexRolloutCandidateName,
  codexRolloutFileInfo,
  codexRolloutFileMatchesThread,
  codexRolloutIdFromPath,
  codexUserKey,
  createCodexUserDeduper,
  checkCodexStateIntegrity,
  extractResumeTarget,
  findCodexSessionFiles,
  findCodexSessionFilesByFilename,
  findCodexStateRolloutPath,
  findCodexThreadFromRolloutsForSession,
  findCodexThreadForCtmSession,
  findCodexThreadForSession,
  getCodexStateThreadById,
  getCodexThreadResumeCwdInfo,
  getCodexThreadResumeCwd,
  getCodexThreadById,
  getResumeSpec,
  checkCodexStateDbHealth,
  classifyCodexUserText,
  inspectCodexStateThreadRollout,
  isCodexProviderControlUserText,
  isCodexSyntheticUserText,
  listCodexSessionsFromRollouts,
  normalizeCodexRecentThreadRows,
  readCodexRolloutMetadata,
  repairCodexStateThreadRollout,
  searchCodexRolloutMessages,
  parseSessionStartMs,
  parseCodexJsonlIntoMessages,
  parseCodexJsonlFileIntoMessages,
  parseCodexJsonlFileIntoMessagesAsync,
  parseCodexJsonlTailFileIntoMessages,
};
