← Files Codex Process JobsARCHIVED FILE

scripts/session.mjs

5.65 KB · Sep 30, 2026 · 23:13 UTC

↓ Download file

import fs from "node:fs";
import path from "node:path";

import { resolveCodexHome } from "./state.mjs";

const MAX_SESSION_META_BYTES = 256 * 1024;
const MAX_SUBAGENT_PARENT_DEPTH = 16;

export function sanitizeThreadId(value) {
  const normalized = String(value ?? "").trim();
  if (!/^[A-Za-z0-9_-]{8,160}$/.test(normalized)) {
    throw new Error("No valid owning Codex thread id was provided.");
  }
  return normalized;
}

// Codex hook payloads can retain the user-visible parent as `session_id`
// while a spawned subagent executes with its own CODEX_THREAD_ID. Prefer the
// runtime identity so hook decisions and launch boundaries apply to the agent
// that is actually using the tool.
export function resolveHookThreadId(input, env = process.env) {
  let payloadThreadId = null;
  for (const candidate of [input?.thread_id, input?.session_id]) {
    if (candidate == null || String(candidate).trim() === "") continue;
    try {
      payloadThreadId = sanitizeThreadId(candidate);
      break;
    } catch {}
  }

  try {
    const runtimeThreadId = sanitizeThreadId(env.CODEX_THREAD_ID);
    if (!payloadThreadId || runtimeThreadId === payloadThreadId) return runtimeThreadId;
    const rollout = resolveOwnerRolloutFile(runtimeThreadId, env);
    const metadata = rollout ? readSessionMeta(rollout) : null;
    const recordedId = metadata?.id ?? null;
    if (recordedId != null && sanitizeThreadId(recordedId) === runtimeThreadId) {
      return runtimeThreadId;
    }
  } catch {}

  if (payloadThreadId) return payloadThreadId;
  try {
    return sanitizeThreadId(env.CODEX_THREAD_ID);
  } catch {
    return null;
  }
}

export function resolveOwnerRolloutFile(threadId, env = process.env) {
  const safeThreadId = sanitizeThreadId(threadId);
  const sessionsRoot = path.join(resolveCodexHome(env), "sessions");
  const suffix = `${safeThreadId}.jsonl`;
  const stack = [sessionsRoot];
  const matches = [];
  while (stack.length > 0) {
    const directory = stack.pop();
    let entries;
    try {
      entries = fs.readdirSync(directory, { withFileTypes: true });
    } catch (error) {
      if (error?.code === "ENOENT" || error?.code === "EACCES") continue;
      throw error;
    }
    for (const entry of entries) {
      const candidate = path.join(directory, entry.name);
      if (entry.isDirectory()) stack.push(candidate);
      else if (entry.isFile() && entry.name.endsWith(suffix)) {
        matches.push({ file: candidate, modifiedMs: fs.statSync(candidate).mtimeMs });
      }
    }
  }
  matches.sort((left, right) => right.modifiedMs - left.modifiedMs);
  return matches[0]?.file ?? null;
}

export function readSessionMeta(file) {
  const stat = fs.statSync(file);
  const length = Math.min(stat.size, MAX_SESSION_META_BYTES);
  if (length === 0) return null;
  const buffer = Buffer.alloc(length);
  const descriptor = fs.openSync(file, "r");
  try {
    fs.readSync(descriptor, buffer, 0, length, 0);
  } finally {
    fs.closeSync(descriptor);
  }
  const lines = buffer.toString("utf8").split("\n");
  if (stat.size > length) lines.pop();
  for (const line of lines) {
    if (!line.trim()) continue;
    try {
      const event = JSON.parse(line);
      if (event?.type === "session_meta" && event.payload && typeof event.payload === "object") {
        return event.payload;
      }
    } catch {}
  }
  return null;
}

function spawnedSubagentParentThreadId(metadata, current) {
  if (!metadata || typeof metadata !== "object") return null;

  // The rollout file name is not enough to establish ownership. Require the
  // metadata identity to match the current thread before using any parent.
  const recordedId = metadata.id ?? metadata.thread_id ?? metadata.session_id ?? null;
  if (recordedId == null) return null;
  try {
    if (sanitizeThreadId(recordedId) !== current) return null;
  } catch {
    return null;
  }

  const sourceSubagent = metadata.source?.subagent;
  const nestedDeclaration = sourceSubagent?.thread_spawn;
  const explicitlySpawned = metadata.thread_source === "subagent"
    || (nestedDeclaration && typeof nestedDeclaration === "object");
  if (!explicitlySpawned) return null;

  const candidates = [];
  for (const candidate of [nestedDeclaration?.parent_thread_id, metadata.parent_thread_id]) {
    if (candidate == null) continue;
    try {
      candidates.push(sanitizeThreadId(candidate));
    } catch {
      return null;
    }
  }
  if (candidates.length === 0 || new Set(candidates).size !== 1) return null;
  const parent = candidates[0];
  return parent === current ? null : parent;
}

export function isSpawnedSubagentThread(threadId, env = process.env) {
  try {
    const current = sanitizeThreadId(threadId);
    const rollout = resolveOwnerRolloutFile(current, env);
    if (!rollout) return false;
    return spawnedSubagentParentThreadId(readSessionMeta(rollout), current) != null;
  } catch {
    return false;
  }
}

// A spawned subagent can launch a valid detached job, but it has no durable UI
// client after its turn drains. Route completion to the highest user-visible
// ancestor while preserving the actual launch thread separately for hook turn
// boundaries.
export function resolveNotificationOwnerThreadId(threadId, env = process.env) {
  let current = sanitizeThreadId(threadId);
  const origin = current;
  const visited = new Set();
  for (let depth = 0; depth < MAX_SUBAGENT_PARENT_DEPTH; depth += 1) {
    if (visited.has(current)) return origin;
    visited.add(current);
    const rollout = resolveOwnerRolloutFile(current, env);
    if (!rollout) return current;
    const parent = spawnedSubagentParentThreadId(readSessionMeta(rollout), current);
    if (!parent) return current;
    if (visited.has(parent)) return origin;
    current = parent;
  }
  return origin;
}

SHA-256: 95c074be9c8987a6c4378211225e8ec7baaa89860d8011b32e9e34d45006323f