// Codex CLI JSONL log monitor
// Polls ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl for state changes
// Zero external dependencies (node built-ins + local Codex helpers only)
//
// Replay protection is two layers — change one, consider the other:
//   1. Line-level: _processLine skips entries whose `timestamp` field is
//      older than monitor start. Only helps lines that carry a timestamp.
//   2. File-level: _pollFile sets tracked.backfilling when attaching to a
//      file whose mtime predates monitor start. _processLine then suppresses
//      historical emits until the first read drains, then
//      _emitBackfillSnapshot may synthesize ONE current sustained state
//      (thinking / working). Works for any line shape,
//      covers what layer 1 can't.
// The two overlap but don't duplicate each other — collapsing them takes a
// refactor, not a tweak.

const fs = require("fs");
const path = require("path");
const os = require("os");
const CodexSubagentClassifier = require("./codex-subagent-classifier");
const { readCodexThreadName } = require("../hooks/codex-session-index");
const {
  clampAssistantOutputText,
  extractAssistantTextFromRecord,
} = require("../hooks/codex-assistant-output");

const MAX_TRACKED_FILES = 50;
const MAX_RETIRED_TRACKED_FILES = 100;
const MAX_PARTIAL_BYTES = 65536;
const RECENT_DAY_DIR_CACHE_MS = 60 * 60 * 1000; // 1 hour
// A rollout file is considered "active" if written within this window. Used by
// both the untracked-file pickup gate in _poll and the _getActiveDayDirs scan
// so slow Codex desktop sessions (3–5 min write cadence) aren't dropped by one
// path only to be rescued by the other.
const ACTIVE_SESSION_WINDOW_MS = 5 * 60 * 1000;
// Grace window around monitor start. A file with content whose last write
// predates this window is treated as pre-existing history on attach — we
// replay it silently (backfill) instead of emitting stale transitions. A
// file written within the grace window is a live session and emits normally.
const BACKFILL_GRACE_MS = 5 * 1000;
const BACKFILL_SNAPSHOT_STATES = new Set(["thinking", "working"]);

function finiteNonnegativeNumber(value) {
  const n = Number(value);
  return Number.isFinite(n) && n >= 0 ? n : null;
}

function positiveNumber(value) {
  const n = Number(value);
  return Number.isFinite(n) && n > 0 ? n : null;
}

function extractCodexContextUsage(payload) {
  if (!payload || typeof payload !== "object") return null;
  const info = payload.info && typeof payload.info === "object" ? payload.info : null;
  const lastUsage = info && info.last_token_usage && typeof info.last_token_usage === "object"
    ? info.last_token_usage
    : null;
  const used = finiteNonnegativeNumber(
    (lastUsage && lastUsage.total_tokens)
    ?? payload.total_tokens
    ?? payload.tokens_used
    ?? payload.input_tokens
    ?? payload.context_tokens
  );
  if (used === null) return null;

  const limit = positiveNumber(
    (info && info.model_context_window)
    ?? payload.model_context_window
    ?? payload.context_window
    ?? payload.limit
    ?? payload.max_tokens
  );
  const out = { used, source: "codex" };
  if (limit !== null) {
    out.limit = limit;
    out.percent = Math.max(0, Math.min(100, Math.round((used / limit) * 100)));
  }
  return out;
}

class CodexLogMonitor {
  /**
   * @param {object} agentConfig - codex.js config (logConfig + logEventMap)
   * @param {function} onStateChange - (sessionId, state, event, extra) => void
   * @param {object} options
   */
  constructor(agentConfig, onStateChange, options = {}) {
    this._config = agentConfig;
    this._onStateChange = onStateChange;
    this._classifier = options.classifier || new CodexSubagentClassifier();
    this._interval = null;
    // Map<filePath, { offset, sessionId, cwd, lastEventTime, lastState, partial }>
    this._tracked = new Map();
    this._retiredTracked = new Map();
    this._baseDir = this._resolveBaseDir();
    this._codexDir = options.codexDir || null;
    this._recentDayDirsCache = [];
    this._recentDayDirsCacheAt = 0;
    this._recentDayDirsDateKey = "";
    this._activeDayDirsCache = null;
    this._activeDayDirsCacheAt = 0;
    this._startedAtMs = Date.now();
  }

  _resolveBaseDir() {
    const dir = this._config.logConfig.sessionDir;
    if (dir.startsWith("~")) {
      return path.join(os.homedir(), dir.slice(1));
    }
    return dir;
  }

  start() {
    if (this._interval) return;
    this._startedAtMs = Date.now();
    // Initial scan
    this._poll();
    this._interval = setInterval(
      () => this._poll(),
      this._config.logConfig.pollIntervalMs || 1500
    );
  }

  stop() {
    if (this._interval) {
      clearInterval(this._interval);
      this._interval = null;
    }
    this._tracked.clear();
    this._retiredTracked.clear();
  }

  _poll() {
    const dirs = this._getSessionDirs();
    for (const dir of dirs) {
      let files;
      try {
        files = fs.readdirSync(dir);
      } catch {
        continue; // directory doesn't exist yet
      }
      const now = Date.now();
      for (const file of files) {
        if (!file.startsWith("rollout-") || !file.endsWith(".jsonl")) continue;
        const filePath = path.join(dir, file);
        // Skip files we're not already tracking if they haven't been written recently
        if (!this._tracked.has(filePath)) {
          try {
            const mtime = fs.statSync(filePath).mtimeMs;
            if (now - mtime > ACTIVE_SESSION_WINDOW_MS) continue; // completed session, skip
          } catch { continue; }
        }
        this._pollFile(filePath, file);
      }
    }
    this._pruneTrackedFilesIfNeeded();
  }

  _getSessionDirs() {
    const dirs = [];
    const seen = new Set();
    const addDir = (dir) => {
      if (!dir || seen.has(dir)) return;
      seen.add(dir);
      dirs.push(dir);
    };
    const now = new Date();
    for (let daysAgo = 0; daysAgo <= 2; daysAgo++) {
      const d = new Date(now);
      d.setDate(d.getDate() - daysAgo);
      const yyyy = d.getFullYear();
      const mm = String(d.getMonth() + 1).padStart(2, "0");
      const dd = String(d.getDate()).padStart(2, "0");
      addDir(path.join(this._baseDir, String(yyyy), mm, dd));
    }
    // Fallback: include most recent existing day dirs to handle
    // clock/timezone drift and `codex resume` of older sessions
    for (const dir of this._getCachedRecentExistingDayDirs(7)) addDir(dir);
    // Also include any day dir that has a recently-modified rollout file.
    // Covers Codex desktop app's long-lived conversations where new writes
    // keep landing in the ORIGINAL day dir (which can be weeks/months old).
    for (const dir of this._getActiveDayDirs()) addDir(dir);
    return dirs;
  }

  // Scan baseDir for any day dir containing a rollout-*.jsonl whose mtime
  // is within `withinMs`. Returns the set of such day dirs.
  // Cached for 5s to keep polling cheap.
  _getActiveDayDirs(withinMs = ACTIVE_SESSION_WINDOW_MS) {
    const now = Date.now();
    if (this._activeDayDirsCache && now - this._activeDayDirsCacheAt < 5000) {
      return this._activeDayDirsCache;
    }
    const out = new Set();
    let years;
    try {
      years = fs.readdirSync(this._baseDir, { withFileTypes: true })
        .filter((d) => d.isDirectory() && /^\d{4}$/.test(d.name))
        .map((d) => d.name);
    } catch {
      this._activeDayDirsCache = [];
      this._activeDayDirsCacheAt = now;
      return [];
    }
    for (const y of years) {
      const yPath = path.join(this._baseDir, y);
      let months;
      try {
        months = fs.readdirSync(yPath, { withFileTypes: true })
          .filter((d) => d.isDirectory() && /^\d{2}$/.test(d.name))
          .map((d) => d.name);
      } catch { continue; }
      for (const m of months) {
        const mPath = path.join(yPath, m);
        let days;
        try {
          days = fs.readdirSync(mPath, { withFileTypes: true })
            .filter((d) => d.isDirectory() && /^\d{2}$/.test(d.name))
            .map((d) => d.name);
        } catch { continue; }
        for (const day of days) {
          const dPath = path.join(mPath, day);
          let files;
          try {
            files = fs.readdirSync(dPath);
          } catch { continue; }
          for (const file of files) {
            if (!file.startsWith("rollout-") || !file.endsWith(".jsonl")) continue;
            try {
              const mtime = fs.statSync(path.join(dPath, file)).mtimeMs;
              if (now - mtime < withinMs) {
                out.add(dPath);
                break;
              }
            } catch {}
          }
        }
      }
    }
    this._activeDayDirsCache = Array.from(out);
    this._activeDayDirsCacheAt = now;
    return this._activeDayDirsCache;
  }

  _getCachedRecentExistingDayDirs(limit = 7) {
    const now = Date.now();
    const dateKey = this._getLocalDateKey();
    const cacheStale = now - this._recentDayDirsCacheAt > RECENT_DAY_DIR_CACHE_MS;
    const dayChanged = dateKey !== this._recentDayDirsDateKey;
    if (!this._recentDayDirsCache.length || cacheStale || dayChanged) {
      this._recentDayDirsCache = this._getRecentExistingDayDirs(limit);
      this._recentDayDirsCacheAt = now;
      this._recentDayDirsDateKey = dateKey;
    }
    return this._recentDayDirsCache.slice(0, limit);
  }

  _getLocalDateKey() {
    const now = new Date();
    const yyyy = now.getFullYear();
    const mm = String(now.getMonth() + 1).padStart(2, "0");
    const dd = String(now.getDate()).padStart(2, "0");
    return `${yyyy}-${mm}-${dd}`;
  }

  _getRecentExistingDayDirs(limit = 7) {
    const out = [];
    let years;
    try {
      years = fs.readdirSync(this._baseDir, { withFileTypes: true })
        .filter((d) => d.isDirectory() && /^\d{4}$/.test(d.name))
        .map((d) => d.name)
        .sort((a, b) => b.localeCompare(a));
    } catch {
      return out;
    }
    for (const y of years) {
      const yPath = path.join(this._baseDir, y);
      let months;
      try {
        months = fs.readdirSync(yPath, { withFileTypes: true })
          .filter((d) => d.isDirectory() && /^\d{2}$/.test(d.name))
          .map((d) => d.name)
          .sort((a, b) => b.localeCompare(a));
      } catch { continue; }
      for (const m of months) {
        const mPath = path.join(yPath, m);
        let days;
        try {
          days = fs.readdirSync(mPath, { withFileTypes: true })
            .filter((d) => d.isDirectory() && /^\d{2}$/.test(d.name))
            .map((d) => d.name)
            .sort((a, b) => b.localeCompare(a));
        } catch { continue; }
        for (const d of days) {
          out.push(path.join(mPath, d));
          if (out.length >= limit) return out;
        }
      }
    }
    return out;
  }

  _pollFile(filePath, fileName) {
    let stat;
    try {
      stat = fs.statSync(filePath);
    } catch {
      return;
    }

    let tracked = this._tracked.get(filePath);
    if (!tracked) {
      // New file — extract session ID from filename
      // Format: rollout-YYYY-MM-DDTHH-MM-SS-<uuid>.jsonl
      const sessionId = this._extractSessionId(fileName);
      if (!sessionId) return;
      // Cap tracked files to prevent unbounded Map growth
      if (this._tracked.size >= MAX_TRACKED_FILES) {
        this._pruneTrackedFilesIfNeeded();
        if (this._tracked.size >= MAX_TRACKED_FILES) return;
      }
      const retired = this._retiredTracked.get(filePath) || null;
      const resumeOffset = retired && stat.size >= retired.offset ? retired.offset : 0;
      if (retired) this._retiredTracked.delete(filePath);
      tracked = {
        offset: resumeOffset,
        sessionId: "codex:" + sessionId,
        filePath,
        cwd: retired ? retired.cwd : "",
        sessionTitle: retired ? retired.sessionTitle : null,
        codexOriginator: retired ? retired.codexOriginator : null,
        codexSource: retired ? retired.codexSource : null,
        lastEventTime: Date.now(),
        lastState: retired ? retired.lastState : null,
        lastStateEvent: retired ? retired.lastStateEvent : null,
        hasEmittedState: retired ? retired.hasEmittedState === true : false,
        partial: "",
        hadToolUse: retired ? retired.hadToolUse === true : false,
        isSubagent: retired ? retired.isSubagent === true : false,
        agentPid: retired ? retired.agentPid : null,
        assistantLastOutput: retired ? retired.assistantLastOutput || null : null,
        assistantLastOutputTruncated: retired ? retired.assistantLastOutputTruncated === true : false,
        contextUsage: retired ? retired.contextUsage || null : null,
        // Backfill mode: only a file whose last write predates monitor
        // start (by more than BACKFILL_GRACE_MS) is treated as stale
        // history — we replay it silently to advance offset + pick up
        // cwd/sessionTitle without emitting old transitions. Files written
        // inside the grace window are live sessions and emit normally.
        // Empty files have nothing to replay.
        backfilling:
          !retired &&
          stat.size > 0 &&
          stat.mtimeMs < this._startedAtMs - BACKFILL_GRACE_MS,
      };
      this._tracked.set(filePath, tracked);
    }

    // No new data
    if (stat.size <= tracked.offset) return;

    // Read incremental bytes
    let buf;
    try {
      const fd = fs.openSync(filePath, "r");
      const readLen = stat.size - tracked.offset;
      buf = Buffer.alloc(readLen);
      fs.readSync(fd, buf, 0, readLen, tracked.offset);
      fs.closeSync(fd);
    } catch {
      return;
    }
    tracked.offset = stat.size;

    // Split into lines, handle partial last line
    const text = tracked.partial + buf.toString("utf8");
    const lines = text.split("\n");
    // Last element might be incomplete — save for next poll.
    // Cap at 64KB: lines larger than this (e.g. huge tool output) are discarded —
    // both halves will fail JSON.parse so one state update is silently lost, which
    // is harmless for the pet's display state.
    const remainder = lines.pop() || "";
    tracked.partial = remainder.length > MAX_PARTIAL_BYTES ? "" : remainder;

    for (const line of lines) {
      if (!line.trim()) continue;
      this._processLine(line, tracked);
    }

    // First pass drained the historical bytes we picked up on attach;
    // subsequent writes to this file are live and must emit normally.
    if (tracked.backfilling) {
      this._emitBackfillSnapshot(tracked);
      tracked.backfilling = false;
    }
  }

  _processLine(line, tracked) {
    let obj;
    try {
      obj = JSON.parse(line);
    } catch {
      return; // corrupted line, skip
    }

    const type = obj.type;
    const payload = obj.payload;
    const subtype =
      payload && typeof payload === "object" ? payload.type || "" : "";

    // Build lookup key
    const key = subtype ? type + ":" + subtype : type;

    // Metadata is needed for future live writes even when the session_meta
    // record itself predates monitor start.
    if (type === "session_meta") {
      this._applySessionMeta(payload, tracked);
    }

    // Skip historical events that predate monitor start — prevents replay
    // storms on app restart from driving stale state transitions.
    if (obj && typeof obj.timestamp === "string") {
      const ts = Date.parse(obj.timestamp);
      if (!tracked.backfilling && Number.isFinite(ts) && ts < this._startedAtMs - 1500) return;
    }

    const assistantText = extractAssistantTextFromRecord(obj);
    if (assistantText) {
      const assistantOutput = clampAssistantOutputText(assistantText);
      tracked.assistantLastOutput = assistantOutput ? assistantOutput.text : null;
      tracked.assistantLastOutputTruncated = !!(assistantOutput && assistantOutput.truncated);
    }

    if (key === "event_msg:token_count") {
      const contextUsage = extractCodexContextUsage(payload);
      if (contextUsage) {
        tracked.contextUsage = contextUsage;
        if (!tracked.backfilling) {
          this._emitStateChange(tracked, tracked.lastState || "idle", key);
        }
      }
      return;
    }

    // Extract Codex-authored session summary (turn_context.summary).
    // Updates tracked.sessionTitle in place; gets picked up by the next
    // _onStateChange call. Intentionally no metaOnly side-channel —
    // accepts brief staleness until the next state emit.
    const extractedTitle = this._extractSessionTitle(obj);
    if (extractedTitle && extractedTitle !== tracked.sessionTitle) {
      tracked.sessionTitle = extractedTitle;
    }
    const threadName = readCodexThreadName(tracked.sessionId, { codexDir: this._codexDir });
    if (threadName && threadName !== tracked.sessionTitle) {
      tracked.sessionTitle = threadName;
    }

    // Look up state mapping
    const map = this._config.logEventMap;
    const state = map[key];
    if (state === undefined) return; // unmapped event, skip
    if (state === null) return; // explicitly ignored
    tracked.lastStateEvent = key;

    // Track tool use per turn — reset on task_started, set on function_call
    if (key === "event_msg:task_started") {
      tracked.hadToolUse = false;
      tracked.assistantLastOutput = null;
      tracked.assistantLastOutputTruncated = false;
    }
    if (key === "response_item:function_call") {
      tracked.hadToolUse = true;
    }

    // Turn-end: happy if tools were used or the turn produced assistant text;
    // metadata-only completions stay idle to avoid noisy fallback animation.
    if (state === "codex-turn-end") {
      const resolved = this._isTrackedSubagent(tracked)
        ? "idle"
        : (tracked.hadToolUse || !!tracked.assistantLastOutput ? "attention" : "idle");
      tracked.hadToolUse = false;
      tracked.lastState = resolved;
      if (tracked.backfilling) return;
      this._emitStateChange(tracked, resolved, key, this._assistantOutputExtra(tracked));
      return;
    }

    // Backfill gate: first-pass replay of a file's historical content skips
    // every callback, but it still updates
    // internal state so attach can synthesize the current visible state once.
    // Independent of the timestamp-based replay guard, which only helps lines
    // that carry a timestamp field.
    if (tracked.backfilling) {
      tracked.lastState = state;
      return;
    }

    // Avoid spamming same state
    if (state === tracked.lastState && state === "working") return;
    tracked.lastState = state;
    this._emitStateChange(tracked, state, key);
  }

  _applySessionMeta(payload, tracked) {
    if (!payload || typeof payload !== "object") return;
    tracked.cwd = payload.cwd || "";
    tracked.codexOriginator = typeof payload.originator === "string" && payload.originator.trim()
      ? payload.originator.trim()
      : tracked.codexOriginator;
    tracked.codexSource = typeof payload.source === "string" && payload.source.trim()
      ? payload.source.trim()
      : tracked.codexSource;
    const role = this._classifier.registerSession(tracked.sessionId, { sessionMeta: payload });
    if (role === "subagent") tracked.isSubagent = true;
    else if (role === "root") tracked.isSubagent = false;
  }

  // Codex-authored session summary, extracted from turn_context.summary.
  // Filters "none" / "auto" placeholder values that Codex writes when
  // the model hasn't produced a real summary yet.
  _extractSessionTitle(obj) {
    if (!obj || typeof obj !== "object") return null;
    const payload = obj.payload && typeof obj.payload === "object" ? obj.payload : null;
    if (!payload) return null;
    if (obj.type === "turn_context" && typeof payload.summary === "string") {
      const summary = payload.summary.trim();
      if (summary && summary !== "none" && summary !== "auto") return summary;
    }
    return null;
  }

  // Extract UUID from rollout filename
  // rollout-2026-03-25T15-10-51-019d23d4-f1a9-7633-b9c7-758327137228.jsonl
  _extractSessionId(fileName) {
    // UUID v7 is the last 5 segments of the filename (before .jsonl)
    const base = fileName.replace(".jsonl", "");
    const parts = base.split("-");
    // UUID: last 5 parts (8-4-4-4-12 hex)
    if (parts.length < 10) return null;
    return parts.slice(-5).join("-");
  }

  _resolveTrackedAgentPid(tracked) {
    if (tracked.agentPid && this._isProcessAlive(tracked.agentPid)) {
      return tracked.agentPid;
    }
    const pid = this._findCodexWriterPid(tracked.filePath);
    tracked.agentPid = pid || null;
    return tracked.agentPid;
  }

  _isProcessAlive(pid) {
    try {
      process.kill(pid, 0);
      return true;
    } catch (err) {
      return err && err.code === "EPERM";
    }
  }

  // Linux-only: find codex process that has the rollout file open via /proc
  _findCodexWriterPid(filePath) {
    if (process.platform !== "linux" || !filePath) return null;
    let procEntries;
    try {
      procEntries = fs.readdirSync("/proc", { withFileTypes: true });
    } catch {
      return null;
    }
    for (const ent of procEntries) {
      if (!ent.isDirectory() || !/^\d+$/.test(ent.name)) continue;
      const pid = Number(ent.name);
      if (!Number.isFinite(pid) || pid <= 1) continue;
      // Fast prefilter: skip non-codex processes
      try {
        const cmd = fs.readFileSync(`/proc/${pid}/cmdline`, "utf8");
        if (!cmd.includes("codex")) continue;
      } catch { continue; }
      let fds;
      try {
        fds = fs.readdirSync(`/proc/${pid}/fd`);
      } catch { continue; }
      for (const fd of fds) {
        try {
          const target = fs.readlinkSync(`/proc/${pid}/fd/${fd}`);
          if (target === filePath) return pid;
        } catch {}
      }
    }
    return null;
  }

  _pruneTrackedFilesIfNeeded() {
    if (this._tracked.size < MAX_TRACKED_FILES) return;
    const byAge = (a, b) => (a[1].lastEventTime || 0) - (b[1].lastEventTime || 0);
    const neverEmitted = [...this._tracked.entries()]
      .filter(([, tracked]) => tracked && !tracked.hasEmittedState)
      .sort(byAge);
    const emitted = [...this._tracked.entries()]
      .filter(([, tracked]) => tracked && tracked.hasEmittedState)
      .sort(byAge);
    for (const [filePath, tracked] of [...neverEmitted, ...emitted]) {
      if (this._tracked.size < MAX_TRACKED_FILES) break;
      this._retireTrackedFile(filePath, tracked);
    }
  }

  _retireTrackedFile(filePath, tracked) {
    this._tracked.delete(filePath);
    if (!filePath || !tracked) return;
    this._retiredTracked.delete(filePath);
    this._retiredTracked.set(filePath, {
      offset: Number.isFinite(tracked.offset) ? tracked.offset : 0,
      cwd: tracked.cwd || "",
      sessionTitle: tracked.sessionTitle || null,
      codexOriginator: tracked.codexOriginator || null,
      codexSource: tracked.codexSource || null,
      lastState: tracked.lastState || null,
      lastStateEvent: tracked.lastStateEvent || null,
      hasEmittedState: tracked.hasEmittedState === true,
      hadToolUse: tracked.hadToolUse === true,
      isSubagent: tracked.isSubagent === true,
      agentPid: tracked.agentPid || null,
      assistantLastOutput: tracked.assistantLastOutput || null,
      assistantLastOutputTruncated: tracked.assistantLastOutputTruncated === true,
      contextUsage: tracked.contextUsage || null,
    });
    while (this._retiredTracked.size > MAX_RETIRED_TRACKED_FILES) {
      const oldest = this._retiredTracked.keys().next().value;
      this._retiredTracked.delete(oldest);
    }
  }

  _emitBackfillSnapshot(tracked) {
    const snapshotState = tracked.lastState;
    if (!BACKFILL_SNAPSHOT_STATES.has(snapshotState)) {
      if (tracked.contextUsage) {
        this._emitStateChange(tracked, "idle", "event_msg:token_count");
      }
      return;
    }
    this._emitStateChange(
      tracked,
      snapshotState,
      tracked.lastStateEvent || "session_meta",
      null
    );
  }

  _assistantOutputExtra(tracked) {
    if (!tracked || typeof tracked.assistantLastOutput !== "string" || !tracked.assistantLastOutput) {
      return null;
    }
    return {
      assistantLastOutput: tracked.assistantLastOutput,
      assistantLastOutputTruncated: tracked.assistantLastOutputTruncated === true,
    };
  }

  _withTrackedContextUsage(tracked, extra = null) {
    if (!tracked || !tracked.contextUsage) return extra;
    return { ...(extra || {}), contextUsage: tracked.contextUsage };
  }

  _isTrackedSubagent(tracked) {
    if (!tracked) return false;
    const role = this._classifier && typeof this._classifier.classify === "function"
      ? this._classifier.classify(tracked.sessionId)
      : "unknown";
    if (role === "subagent") {
      tracked.isSubagent = true;
      return true;
    }
    if (role === "root") {
      tracked.isSubagent = false;
      return false;
    }
    return tracked.isSubagent === true;
  }

  _emitStateChange(tracked, state, event, extra = null) {
    tracked.lastState = state;
    tracked.lastEventTime = Date.now();
    tracked.hasEmittedState = true;
    const agentPid = this._resolveTrackedAgentPid(tracked);
    this._onStateChange(tracked.sessionId, state, event, {
      cwd: tracked.cwd,
      sourcePid: extra && Object.prototype.hasOwnProperty.call(extra, "sourcePid")
        ? extra.sourcePid
        : agentPid,
      agentPid: extra && Object.prototype.hasOwnProperty.call(extra, "agentPid")
        ? extra.agentPid
        : agentPid,
      sessionTitle: tracked.sessionTitle,
      codexOriginator: tracked.codexOriginator || null,
      codexSource: tracked.codexSource || null,
      ...this._withTrackedContextUsage(tracked, extra),
      headless: this._isTrackedSubagent(tracked)
        ? true
        : (extra && Object.prototype.hasOwnProperty.call(extra, "headless") ? extra.headless : undefined),
    });
  }
}

module.exports = CodexLogMonitor;
