import { createHash, randomUUID } from "crypto";
import { EventEmitter } from "events";
import {
  chmodSync,
  copyFileSync,
  existsSync,
  mkdirSync,
  readdirSync,
  readFileSync,
  rmSync,
  statSync,
} from "fs";
import { dirname, isAbsolute, join, relative, resolve } from "path";
import { fileURLToPath } from "url";
import { ClaudeCodeAdapter } from "./adapters/claude-code-adapter.mjs";
import { CodexAppServerAdapter } from "./adapters/codex-app-server-adapter.mjs";
import { DebugFixtureProviderAdapter } from "./adapters/debug-fixture-provider-adapter.mjs";
import { readConfig } from "./config.mjs";
import {
  createProviderUnavailableError,
  getConfiguredProviderCommand,
  isConfiguredCommandAvailable,
  requireProvider,
} from "./provider-registry.mjs";
import {
  deleteSessionRecord,
  saveSessionRecord,
  updateSessionRecord,
} from "./session-history.mjs";
import {
  buildSessionNameConflictMessage,
  buildUniqueDefaultSessionName,
  normalizeSessionName,
} from "./session-name.mjs";
import { getSessionNameCounterStore } from "./session-name-counter-store.mjs";
import { normalizeAssetReadablePaths } from "./workspace-policy.mjs";
import {
  blockRouteCHost2IntakeProviderDelivery,
  claimRouteCHost2IntakeForProviderDelivery,
  getRouteCHost2IntakeProof,
  markRouteCHost2IntakeAgentTurnStarted,
  recordRouteCProviderDeliveryFailure,
} from "./matrix-event-correlator.mjs";

const OYSTERUN_REAL_CODEX_DELIVERY_MARKER =
  "oysterun_real_codex_host_provider_delivery_no_pass";
const OYSTERUN_REAL_CLAUDE_DELIVERY_MARKER =
  "oysterun_real_claude_host_provider_delivery_no_pass";
const OYSTERUN_REAL_CODEX_SEMANTIC_CONTRACT =
  "real_codex_e2e_semantic_contract";
const OYSTERUN_REAL_CLAUDE_SEMANTIC_CONTRACT =
  "real_claude_e2e_semantic_contract_no_pass";
const PRODUCT_SKILL_COPY_CONTRACT =
  "p54_product_skill_source_of_truth_split_v1";
const PRODUCT_SKILL_NAMES = Object.freeze([
  "oysterun-host-startup",
  "oysterun-mail",
  "oysterun-session-ops",
  "oysterun-website-authoring",
]);
const PRODUCT_SKILL_NAME_SET = new Set(PRODUCT_SKILL_NAMES);
const __dirname = dirname(fileURLToPath(import.meta.url));
const DEFAULT_PRODUCT_SKILL_ROOTS = Object.freeze({
  sourceRoot: resolve(__dirname, "..", "skills"),
  codexMirrorRoot: resolve(__dirname, "..", ".codex", "skills"),
  hostAssetRoot: resolve(__dirname, "assets", "product-skills"),
});

class ProductSkillRequirementError extends Error {
  constructor(message, { statusCode = 409, reason = "product_skill_error" } = {}) {
    super(message);
    this.name = "ProductSkillRequirementError";
    this.code = "PRODUCT_SKILL_REQUIREMENT_ERROR";
    this.statusCode = statusCode;
    this.reason = reason;
  }
}

function normalizeProductSkillRoots(value = {}) {
  return {
    sourceRoot: resolve(value.sourceRoot || DEFAULT_PRODUCT_SKILL_ROOTS.sourceRoot),
    codexMirrorRoot: resolve(
      value.codexMirrorRoot || DEFAULT_PRODUCT_SKILL_ROOTS.codexMirrorRoot
    ),
    hostAssetRoot: resolve(
      value.hostAssetRoot || DEFAULT_PRODUCT_SKILL_ROOTS.hostAssetRoot
    ),
  };
}

function productSkillError(message, options = {}) {
  return new ProductSkillRequirementError(message, options);
}

function normalizeRequiredProductSkills(value) {
  if (value === undefined) return null;
  if (!Array.isArray(value)) {
    throw productSkillError("required_product_skills must be an array", {
      statusCode: 400,
      reason: "invalid_required_product_skills",
    });
  }
  const seen = new Set();
  const skills = [];
  for (const rawSkill of value) {
    const skillName = typeof rawSkill === "string" ? rawSkill.trim() : "";
    if (!skillName) {
      throw productSkillError(
        "required_product_skills entries must be non-empty strings",
        {
          statusCode: 400,
          reason: "invalid_required_product_skills",
        }
      );
    }
    if (!PRODUCT_SKILL_NAME_SET.has(skillName)) {
      throw productSkillError(`unknown required product skill: ${skillName}`, {
        statusCode: 400,
        reason: "unknown_required_product_skill",
      });
    }
    if (seen.has(skillName)) {
      throw productSkillError(`duplicate required product skill: ${skillName}`, {
        statusCode: 400,
        reason: "duplicate_required_product_skill",
      });
    }
    seen.add(skillName);
    skills.push(skillName);
  }
  return skills;
}

function sha256File(path) {
  return createHash("sha256").update(readFileSync(path)).digest("hex");
}

function sortedDirEntries(path) {
  return readdirSync(path, { withFileTypes: true }).sort((a, b) =>
    a.name.localeCompare(b.name)
  );
}

function buildProductSkillFileManifest(skillRoot) {
  const files = [];
  const stack = [skillRoot];
  while (stack.length) {
    const current = stack.pop();
    for (const entry of sortedDirEntries(current)) {
      const entryPath = join(current, entry.name);
      if (entry.isSymbolicLink()) {
        throw productSkillError(
          `product skill trees must not contain symlinks: ${entryPath}`,
          { statusCode: 409, reason: "product_skill_symlink" }
        );
      }
      if (entry.isDirectory()) {
        stack.push(entryPath);
        continue;
      }
      if (!entry.isFile()) {
        throw productSkillError(
          `product skill trees must contain only files/directories: ${entryPath}`,
          { statusCode: 409, reason: "invalid_product_skill_entry" }
        );
      }
      const stats = statSync(entryPath);
      files.push({
        path: relative(skillRoot, entryPath),
        size: stats.size,
        sha256: sha256File(entryPath),
      });
    }
  }
  return files.sort((a, b) => a.path.localeCompare(b.path));
}

function buildProductSkillManifest(root, skillName) {
  const skillRoot = join(root, skillName);
  if (!existsSync(skillRoot)) {
    return { skillName, root: skillRoot, exists: false, files: [] };
  }
  if (!statSync(skillRoot).isDirectory()) {
    throw productSkillError(`product skill path is not a directory: ${skillRoot}`, {
      statusCode: 409,
      reason: "invalid_product_skill_root",
    });
  }
  return {
    skillName,
    root: skillRoot,
    exists: true,
    files: buildProductSkillFileManifest(skillRoot),
  };
}

function productSkillManifestSignature(manifest) {
  return JSON.stringify(manifest.files);
}

function assertManifestMatches(source, mirror, label) {
  if (!source.exists) {
    throw productSkillError(`missing product skill source: ${source.root}`, {
      statusCode: 409,
      reason: "missing_product_skill_source",
    });
  }
  if (!mirror.exists) {
    throw productSkillError(`missing ${label} product skill asset: ${mirror.root}`, {
      statusCode: 409,
      reason: "missing_product_skill_asset",
    });
  }
  if (productSkillManifestSignature(source) !== productSkillManifestSignature(mirror)) {
    throw productSkillError(
      `stale product skill mirror for ${source.skillName}: ${label}`,
      { statusCode: 409, reason: "product_skill_mirror_drift" }
    );
  }
}

function assertProductSkillMirrorsCurrent(productSkillRoots) {
  for (const skillName of PRODUCT_SKILL_NAMES) {
    const source = buildProductSkillManifest(productSkillRoots.sourceRoot, skillName);
    assertManifestMatches(
      source,
      buildProductSkillManifest(productSkillRoots.codexMirrorRoot, skillName),
      ".codex mirror"
    );
    assertManifestMatches(
      source,
      buildProductSkillManifest(productSkillRoots.hostAssetRoot, skillName),
      "Host packaged"
    );
  }
}

function assertPathInside(parentPath, childPath, label) {
  const relativePath = relative(parentPath, childPath);
  if (
    relativePath === "" ||
    relativePath.startsWith("..") ||
    isAbsolute(relativePath)
  ) {
    throw productSkillError(`${label} must stay inside the agent folder`, {
      statusCode: 400,
      reason: "product_skill_target_escape",
    });
  }
}

function copyDirectoryContents(sourceDir, targetDir) {
  mkdirSync(targetDir, { recursive: true });
  for (const entry of sortedDirEntries(sourceDir)) {
    const sourcePath = join(sourceDir, entry.name);
    const targetPath = join(targetDir, entry.name);
    if (entry.isSymbolicLink()) {
      throw productSkillError(`refusing to copy product skill symlink: ${sourcePath}`, {
        statusCode: 409,
        reason: "product_skill_symlink",
      });
    }
    if (entry.isDirectory()) {
      copyDirectoryContents(sourcePath, targetPath);
      continue;
    }
    if (!entry.isFile()) {
      throw productSkillError(
        `refusing to copy non-file product skill entry: ${sourcePath}`,
        { statusCode: 409, reason: "invalid_product_skill_entry" }
      );
    }
    const stats = statSync(sourcePath);
    copyFileSync(sourcePath, targetPath);
    chmodSync(targetPath, stats.mode & 0o777);
  }
}

function copyRequiredProductSkillsToAgent({
  cwd,
  requiredProductSkills,
  productSkillRoots,
}) {
  const skills = normalizeRequiredProductSkills(requiredProductSkills);
  if (skills === null) {
    return {
      provided: false,
      contract: null,
      skills: [],
      copied: [],
    };
  }
  const agentRoot = resolve(cwd);
  if (!existsSync(agentRoot) || !statSync(agentRoot).isDirectory()) {
    throw productSkillError(`agent folder does not exist: ${agentRoot}`, {
      statusCode: 400,
      reason: "missing_agent_folder",
    });
  }
  assertProductSkillMirrorsCurrent(productSkillRoots);
  const targetRoot = resolve(agentRoot, ".claude", "skills");
  assertPathInside(agentRoot, targetRoot, "product skill target root");
  const copied = [];
  for (const skillName of skills) {
    const sourceDir = join(productSkillRoots.hostAssetRoot, skillName);
    const targetDir = resolve(targetRoot, skillName);
    assertPathInside(agentRoot, targetDir, "product skill target");
    rmSync(targetDir, { recursive: true, force: true });
    mkdirSync(dirname(targetDir), { recursive: true });
    copyDirectoryContents(sourceDir, targetDir);
    copied.push({
      skill: skillName,
      source: sourceDir,
      target: targetDir,
      files: buildProductSkillFileManifest(targetDir),
    });
  }
  return {
    provided: true,
    contract: PRODUCT_SKILL_COPY_CONTRACT,
    skills,
    targetRoot,
    copied,
  };
}

function normalizeRuntimeEnv(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
  const env = {};
  for (const [key, rawValue] of Object.entries(value)) {
    if (typeof key !== "string" || !key.trim()) continue;
    if (rawValue === undefined || rawValue === null) continue;
    env[key] = String(rawValue);
  }
  return env;
}

function normalizeRedactionValues(value) {
  if (!Array.isArray(value)) return [];
  return value
    .map((entry) => (typeof entry === "string" ? entry : String(entry || "")))
    .filter((entry) => entry.length > 0);
}

function redactRuntimeCapabilityText(value, tokens) {
  let redacted = String(value);
  for (const token of tokens) {
    redacted = redacted.split(token).join("[redacted]");
  }
  return redacted;
}

function redactRuntimeCapabilityPayload(value, tokens, seen = new WeakSet()) {
  if (!tokens.length) return value;
  if (typeof value === "string") {
    return redactRuntimeCapabilityText(value, tokens);
  }
  if (!value || typeof value !== "object") return value;
  if (seen.has(value)) return value;
  seen.add(value);
  if (Array.isArray(value)) {
    return value.map((entry) =>
      redactRuntimeCapabilityPayload(entry, tokens, seen)
    );
  }
  const next = {};
  for (const [key, entry] of Object.entries(value)) {
    next[key] = redactRuntimeCapabilityPayload(entry, tokens, seen);
  }
  return next;
}

function routeCRealProviderDeliveryMarker(providerId) {
  if (providerId === "codex") return OYSTERUN_REAL_CODEX_DELIVERY_MARKER;
  if (providerId === "claude") return OYSTERUN_REAL_CLAUDE_DELIVERY_MARKER;
  if (providerId === "debug-fixture")
    return "oysterun_debug_fixture_host_provider_delivery_no_pass";
  return null;
}

function routeCRealProviderSemanticContract(providerId) {
  if (providerId === "codex") return OYSTERUN_REAL_CODEX_SEMANTIC_CONTRACT;
  if (providerId === "claude") return OYSTERUN_REAL_CLAUDE_SEMANTIC_CONTRACT;
  if (providerId === "debug-fixture")
    return "debug_fixture_semantic_contract";
  return null;
}

function normalizeProviderMetadataId(value) {
  return typeof value === "string" && value.trim() ? value.trim() : null;
}

/**
 * Manages multiple agent sessions through a pluggable interface adapter.
 * The current default adapter is Claude Code.
 *
 * Adapted from CCSessions/session-manager.mjs for Oysterun host service.
 * Key addition: agent-level session mapping (agent_id → session).
 */
export class SessionManager extends EventEmitter {
  constructor(options = {}) {
    super();
    const getCurrentConfig =
      typeof options.getCurrentConfig === "function"
        ? options.getCurrentConfig
        : readConfig;
    const defaultAdapter =
      options.adapter ||
      new ClaudeCodeAdapter({
        getConfiguredCommand: () =>
          getConfiguredProviderCommand(getCurrentConfig(), "claude"),
      });
    const codexAdapter =
      options.codexAdapter ||
      new CodexAppServerAdapter({
        getConfiguredCommand: () =>
          getConfiguredProviderCommand(getCurrentConfig(), "codex"),
      });
    const debugFixtureAdapter =
      options.debugFixtureAdapter || new DebugFixtureProviderAdapter();
    this.adapters = new Map([
      [defaultAdapter.providerId, defaultAdapter],
      [codexAdapter.providerId, codexAdapter],
      [debugFixtureAdapter.providerId, debugFixtureAdapter],
    ]);
    this.getCurrentConfig = getCurrentConfig;
    this.productSkillRoots = normalizeProductSkillRoots(options.productSkillRoots);
    /** @type {Map<string, import("./adapters/claude-code-adapter.mjs").ClaudeCodeSession>} sessionId → Session */
    this.sessions = new Map();
    /** @type {Map<string, Set<string>>} agentId → sessionIds */
    this.agentSessions = new Map();
  }

  initializeDeliveryState(session) {
    session._deliveryState = session._ready === true ? "ready" : "starting";
    session._outbox = [];
    session._nextOutboxSequence = 1;
    session._activeOutboxMessageId = null;
    session._activeOutboxProgressSeen = false;
  }

  getEffectiveProviderResumeId(session) {
    if (!session) return null;
    return session.providerResumeId || session.resumeSessionId || null;
  }

  getOutboxMessage(session, messageId) {
    return (
      (session._outbox || []).find((entry) => entry.id === messageId) || null
    );
  }

  getActiveOutboxMessage(session) {
    if (!session._activeOutboxMessageId) return null;
    return this.getOutboxMessage(session, session._activeOutboxMessageId);
  }

  getNextQueuedOutboxMessage(session) {
    return (
      (session._outbox || []).find((entry) => entry.state === "queued") || null
    );
  }

  isRetryableOutboxState(state) {
    return state === "ambiguous" || state === "stalled" || state === "failed";
  }

  isSkippableOutboxState(state) {
    return state === "ambiguous" || state === "stalled";
  }

  canonicalDeliveryStateForMessageState(state) {
    switch (state) {
      case "queued":
        return "queued";
      case "running":
        return "running";
      case "completed":
        return "committed";
      case "failed":
      case "stalled":
      case "ambiguous":
        return "failed";
      default:
        return "queued";
    }
  }

  routeCStableHash(value) {
    return createHash("sha256")
      .update(String(value || ""))
      .digest("hex");
  }

  routeCHostGeneratedProviderTurnId(session, message, delivery) {
    const hash = this.routeCStableHash(
      JSON.stringify({
        host_session_id: delivery?.host_session_id || session?.id || null,
        matrix_room_id: delivery?.matrix_room_id || null,
        source_user_event_id: delivery?.source_user_event_id || null,
        outbox_message_id: message?.id || null,
        provider_id: delivery?.provider_id || session?.provider || null,
      })
    ).slice(0, 32);
    return `routec_host_provider_turn_${hash}`;
  }

  resolveOutboxProviderTurnId(
    session,
    message,
    delivery,
    { eventProviderTurnId = null, eventProviderTurnIdKind = null } = {}
  ) {
    if (!delivery || !message) {
      return { providerTurnId: null, providerTurnIdKind: null };
    }
    const normalizedEventProviderTurnId =
      typeof eventProviderTurnId === "string" && eventProviderTurnId.trim()
        ? eventProviderTurnId.trim()
        : null;
    const normalizedEventProviderTurnIdKind =
      typeof eventProviderTurnIdKind === "string" &&
      eventProviderTurnIdKind.trim()
        ? eventProviderTurnIdKind.trim()
        : null;
    if (normalizedEventProviderTurnId) {
      delivery.provider_turn_id = normalizedEventProviderTurnId;
      delivery.provider_turn_id_kind =
        normalizedEventProviderTurnIdKind || "provider_reported_turn_id";
      message.providerTurnId = normalizedEventProviderTurnId;
      message.providerTurnIdKind = delivery.provider_turn_id_kind;
      return {
        providerTurnId: delivery.provider_turn_id,
        providerTurnIdKind: delivery.provider_turn_id_kind,
      };
    }
    if (delivery.provider_turn_id) {
      message.providerTurnId = delivery.provider_turn_id;
      message.providerTurnIdKind =
        delivery.provider_turn_id_kind ||
        message.providerTurnIdKind ||
        "provider_reported_turn_id";
      delivery.provider_turn_id_kind = message.providerTurnIdKind;
      return {
        providerTurnId: delivery.provider_turn_id,
        providerTurnIdKind: delivery.provider_turn_id_kind,
      };
    }
    if (message.providerTurnId) {
      delivery.provider_turn_id = message.providerTurnId;
      delivery.provider_turn_id_kind =
        message.providerTurnIdKind || "provider_reported_turn_id";
      return {
        providerTurnId: delivery.provider_turn_id,
        providerTurnIdKind: delivery.provider_turn_id_kind,
      };
    }
    const generatedProviderTurnId = this.routeCHostGeneratedProviderTurnId(
      session,
      message,
      delivery
    );
    delivery.provider_turn_id = generatedProviderTurnId;
    delivery.provider_turn_id_kind = "host_generated_provider_turn_id";
    message.providerTurnId = generatedProviderTurnId;
    message.providerTurnIdKind = delivery.provider_turn_id_kind;
    return {
      providerTurnId: delivery.provider_turn_id,
      providerTurnIdKind: delivery.provider_turn_id_kind,
    };
  }

  routeCProviderRuntimeSemanticTypeForOutboxIndex(event) {
    if (!event || event.delta === true) return null;
    const explicit =
      typeof event.semantic_type === "string" && event.semantic_type.trim()
        ? event.semantic_type.trim()
        : typeof event.semanticType === "string" && event.semanticType.trim()
        ? event.semanticType.trim()
        : null;
    const semanticType =
      explicit ||
      (event.type === "message.assistant"
        ? "message.assistant"
        : event.type === "message.thinking"
        ? "thinking.reasoning"
        : event.type === "tool.call"
        ? "tool.call"
        : event.type === "tool.output" || event.type === "stderr"
        ? "tool.output"
        : event.type === "tool.result"
        ? event.is_error === true
          ? "tool.failure"
          : "tool.result"
        : null);
    if (
      semanticType === "message.assistant" ||
      semanticType === "thinking.reasoning" ||
      String(semanticType || "").startsWith("tool.")
    ) {
      return semanticType;
    }
    return null;
  }

  normalizeRouteCMatrixDelivery(delivery) {
    if (!delivery || typeof delivery !== "object") return null;
    const hostSessionId =
      typeof delivery.host_session_id === "string"
        ? delivery.host_session_id.trim()
        : "";
    const matrixRoomId =
      typeof delivery.matrix_room_id === "string"
        ? delivery.matrix_room_id.trim()
        : "";
    const sourceUserEventId =
      typeof delivery.source_user_event_id === "string"
        ? delivery.source_user_event_id.trim()
        : "";
    if (!hostSessionId || !matrixRoomId || !sourceUserEventId) {
      throw new Error(
        "Route C Matrix delivery requires host_session_id, matrix_room_id, and source_user_event_id"
      );
    }
    const providerId =
      typeof delivery.provider_id === "string"
        ? delivery.provider_id.trim()
        : null;
    return {
      marker:
        typeof delivery.marker === "string" && delivery.marker.trim()
          ? delivery.marker.trim()
          : null,
      host_session_id: hostSessionId,
      matrix_room_id: matrixRoomId,
      matrix_user_id:
        typeof delivery.matrix_user_id === "string"
          ? delivery.matrix_user_id.trim()
          : null,
      matrix_txn_id:
        typeof delivery.matrix_txn_id === "string"
          ? delivery.matrix_txn_id.trim()
          : null,
      source_user_event_id: sourceUserEventId,
      source_user_event_id_hash:
        typeof delivery.source_user_event_id_hash === "string" &&
        delivery.source_user_event_id_hash.trim()
          ? delivery.source_user_event_id_hash.trim()
          : this.routeCStableHash(sourceUserEventId),
      source_user_event_id_hash_kind: "raw_event_id_sha256",
      target_user_event_id:
        typeof delivery.target_user_event_id === "string" &&
        delivery.target_user_event_id.trim()
          ? delivery.target_user_event_id.trim()
          : null,
      provider_id: providerId,
      provider_delivery_claim_id:
        typeof delivery.provider_delivery_claim_id === "string"
          ? delivery.provider_delivery_claim_id.trim()
          : null,
      provider_delivery_claimed: delivery.provider_delivery_claimed === true,
      provider_delivery_permitted:
        delivery.provider_delivery_permitted === true,
      provider_delivery_attempted:
        delivery.provider_delivery_attempted === true,
      provider_delivery_blocked_reason:
        typeof delivery.provider_delivery_blocked_reason === "string" &&
        delivery.provider_delivery_blocked_reason.trim()
          ? delivery.provider_delivery_blocked_reason.trim()
          : null,
      provider_delivery_state:
        typeof delivery.provider_delivery_state === "string" &&
        delivery.provider_delivery_state.trim()
          ? delivery.provider_delivery_state.trim()
          : null,
      host_provider_delivery_path:
        typeof delivery.host_provider_delivery_path === "string" &&
        delivery.host_provider_delivery_path.trim()
          ? delivery.host_provider_delivery_path.trim()
          : null,
      provider_turn_id:
        typeof delivery.provider_turn_id === "string"
          ? delivery.provider_turn_id.trim()
          : null,
      provider_turn_id_kind:
        typeof delivery.provider_turn_id_kind === "string"
          ? delivery.provider_turn_id_kind.trim()
          : null,
      provider_runtime_event_index:
        Number.isSafeInteger(Number(delivery.provider_runtime_event_index)) &&
        Number(delivery.provider_runtime_event_index) > 0
          ? Number(delivery.provider_runtime_event_index)
          : null,
      diagnostic_host_delivery_claim_id:
        typeof delivery.diagnostic_host_delivery_claim_id === "string" &&
        delivery.diagnostic_host_delivery_claim_id.trim()
          ? delivery.diagnostic_host_delivery_claim_id.trim()
          : null,
      diagnostic_host_delivery_claim_id_kind:
        typeof delivery.diagnostic_host_delivery_claim_id_kind === "string" &&
        delivery.diagnostic_host_delivery_claim_id_kind.trim()
          ? delivery.diagnostic_host_delivery_claim_id_kind.trim()
          : null,
      semantic_type:
        typeof delivery.semantic_type === "string" &&
        delivery.semantic_type.trim()
          ? delivery.semantic_type.trim()
          : "message.assistant",
      same_event_both_canceled_and_started:
        delivery.same_event_both_canceled_and_started === true,
      semantic_contract:
        typeof delivery.semantic_contract === "string" &&
        delivery.semantic_contract.trim()
          ? delivery.semantic_contract.trim()
          : null,
      real_codex_host_delivery: delivery.real_codex_host_delivery === true,
      real_claude_host_delivery: delivery.real_claude_host_delivery === true,
      real_provider_host_delivery:
        delivery.real_provider_host_delivery === true,
      direct_host_send_used: false,
      direct_matrix_harness_write_used: false,
      storage_seeded: false,
      real_codex_e2e_claimed: false,
      full_provider_parity_claimed: false,
      readiness_claimed: false,
      foundation_pass_claimed: false,
    };
  }

  buildDeliverySummary(session) {
    const queuedCount = (session._outbox || []).filter(
      (entry) => entry.state === "queued"
    ).length;
    const active = this.getActiveOutboxMessage(session);
    return {
      state: session._deliveryState || "starting",
      queued_count: queuedCount,
      active_message_id: active?.id || null,
      active_message_state: active?.state || null,
    };
  }

  getDeliverySummary(sessionId) {
    const session = this.requireSession(sessionId);
    return this.buildDeliverySummary(session);
  }

  getOutboxSnapshot(sessionId) {
    const session = this.requireSession(sessionId);
    return (session._outbox || []).map((message) => ({
      id: message.id,
      message_id: message.id,
      sequence: message.sequence,
      user_id: message.userId,
      nickname: message.nickname,
      text: message.rawText,
      state: message.state,
      delivery_state: this.canonicalDeliveryStateForMessageState(message.state),
      message_type: "message",
      text_entities: [],
      media: [],
      created_at: message.createdAt,
      completed_at: message.completedAt || null,
      canceled_at: message.canceledAt || null,
      can_cancel: message.state === "queued",
      can_retry: this.isRetryableOutboxState(message.state),
      can_skip: this.isSkippableOutboxState(message.state),
      ambiguity_reason: message.ambiguityReason || null,
      error: message.error || null,
      routec_matrix_delivery: message.routeCMatrixDelivery || null,
    }));
  }

  emitOutboxMessageEvent(session, message) {
    this.emit(
      "runtimeEvent",
      session.agentId,
      this.decorateEvent(session, {
        type: "outbox.message",
        provider: session.provider,
        message_id: message.id,
        sequence: message.sequence,
        user_id: message.userId,
        nickname: message.nickname,
        text: message.rawText,
        state: message.state,
        delivery_state: this.canonicalDeliveryStateForMessageState(
          message.state
        ),
        message_type: "message",
        text_entities: [],
        media: [],
        created_at: message.createdAt,
        can_cancel: message.state === "queued",
        can_retry: this.isRetryableOutboxState(message.state),
        can_skip: this.isSkippableOutboxState(message.state),
        ambiguity_reason: message.ambiguityReason || null,
        error: message.error || null,
        routec_matrix_delivery: message.routeCMatrixDelivery || null,
      })
    );
  }

  emitDeliveryStateNotice(session) {
    this.emit(
      "runtimeEvent",
      session.agentId,
      this.decorateEvent(session, {
        type: "session.notice",
        provider: session.provider,
        subtype: "delivery.state",
        payload: this.buildDeliverySummary(session),
      })
    );
  }

  providerControlKindFromPendingRequest(pendingRequest) {
    const subtype =
      typeof pendingRequest?.subtype === "string"
        ? pendingRequest.subtype.trim()
        : "";
    if (!subtype) {
      throw new Error("Provider control request is missing subtype");
    }
    if (subtype === "command" || subtype === "command_execution")
      return "command";
    if (subtype === "patch" || subtype === "file_change") return "patch";
    if (subtype === "permissions" || subtype === "permission")
      return "permissions";
    if (subtype === "user_input") return "user_input";
    if (subtype === "mcp_elicitation") return "mcp_elicitation";
    if (subtype === "exit_plan") return "exit_plan";
    if (subtype === "can_use_tool") {
      const toolName =
        typeof pendingRequest?.payload?.tool_name === "string"
          ? pendingRequest.payload.tool_name.trim()
          : "";
      if (toolName === "Bash") return "command";
      if (
        toolName === "Edit" ||
        toolName === "Write" ||
        toolName === "MultiEdit"
      )
        return "patch";
      if (toolName === "ExitPlanMode") return "exit_plan";
      return "permissions";
    }
    throw new Error(`Unsupported provider control subtype: ${subtype}`);
  }

  providerControlAllowedActions(controlKind) {
    switch (controlKind) {
      case "command":
      case "patch":
      case "permissions":
      case "exit_plan":
        return ["accept", "reject"];
      case "user_input":
      case "mcp_elicitation":
        return ["submit", "reject"];
      default:
        throw new Error(
          `Unsupported provider control kind for actions: ${controlKind}`
        );
    }
  }

  emitProviderControlOutcomeEvent(
    session,
    pendingRequest,
    responsePayload = {}
  ) {
    if (!session?.id || !pendingRequest?.request_id) {
      throw new Error(
        "Provider control outcome requires session and pending request id"
      );
    }
    const controlKind =
      this.providerControlKindFromPendingRequest(pendingRequest);
    const outcome = responsePayload.allow === false ? "rejected" : "accepted";
    const controlOutcomeId = `routec_control_outcome_${this.routeCStableHash(
      JSON.stringify({
        host_session_id: session.id,
        control_request_id: pendingRequest.request_id,
        outcome,
        provider: session.provider || session.adapterId || null,
      })
    ).slice(0, 32)}`;
    const event = {
      type: "control.outcome",
      provider: session.provider || session.adapterId || "claude",
      request_id: pendingRequest.request_id,
      control_request_id: pendingRequest.request_id,
      control_outcome_id: controlOutcomeId,
      control_kind: controlKind,
      control_family: "provider_request",
      control_origin: "user",
      actor: "oysterun-ui",
      outcome,
      control_outcome: outcome,
      target_id: pendingRequest.request_id,
      target_turn_id: session._activeOutboxMessageId || null,
      target_session_id: session.id,
      allowed_actions: this.providerControlAllowedActions(controlKind),
      sensitive: pendingRequest.sensitive === true,
      semantic_body:
        outcome === "accepted"
          ? `Provider control ${controlKind} accepted.`
          : `Provider control ${controlKind} rejected.`,
      source_label: "host_provider_control_response",
      durable: true,
      replay_policy: "always",
    };
    this.handleOutboxRuntimeEvent(session, event);
    this.emit(
      "runtimeEvent",
      session.agentId,
      this.decorateEvent(session, event)
    );
  }

  emitOutboxCancelControlEvent(
    session,
    message,
    { semanticType, outcome = null } = {}
  ) {
    if (!session?.id || !message?.id) {
      throw new Error(
        "Outbox cancel control event requires session and message"
      );
    }
    const delivery = message.routeCMatrixDelivery || null;
    if (!delivery) return;
    const controlRequestId = `routec_control_cancel_${this.routeCStableHash(
      JSON.stringify({
        host_session_id: session.id,
        message_id: message.id,
        created_at: message.createdAt,
      })
    ).slice(0, 32)}`;
    const targetEventId = delivery.source_user_event_id || null;
    const baseEvent = {
      type: semanticType,
      provider: session.provider || session.adapterId || "claude",
      request_id: controlRequestId,
      control_request_id: controlRequestId,
      control_kind: "cancel",
      control_family: "session_control",
      control_origin: "user",
      actor: "oysterun-ui",
      target_id: message.id,
      target_user_event_id: targetEventId,
      target_event_id: targetEventId,
      target_event_id_kind: targetEventId ? "server" : null,
      target_session_id: session.id,
      target_turn_id: message.providerTurnId || null,
      allowed_actions: ["cancel"],
      source_label: "host_outbox_cancel_control",
      durable: true,
      routec_matrix_delivery: {
        ...delivery,
        outbox_message_id: message.id,
        target_user_event_id: targetEventId,
      },
    };
    if (semanticType === "control.request") {
      this.emit(
        "runtimeEvent",
        session.agentId,
        this.decorateEvent(session, {
          ...baseEvent,
          semantic_body: `Cancel requested for queued message ${message.id}.`,
          replay_policy: "latest_state_only",
        })
      );
      return;
    }
    const controlOutcomeId = `routec_control_outcome_${this.routeCStableHash(
      JSON.stringify({
        host_session_id: session.id,
        control_request_id: controlRequestId,
        outcome,
        target_id: message.id,
      })
    ).slice(0, 32)}`;
    this.emit(
      "runtimeEvent",
      session.agentId,
      this.decorateEvent(session, {
        ...baseEvent,
        semantic_body: `Cancel ${outcome} for queued message ${message.id}.`,
        control_outcome_id: controlOutcomeId,
        control_outcome: outcome,
        outcome,
        replay_policy: "always",
      })
    );
  }

  parseSlashCommand(text) {
    if (typeof text !== "string") return null;
    const trimmed = text.trim();
    if (!trimmed.startsWith("/")) return null;
    const commandName = trimmed.slice(1).split(/\s+/)[0]?.toLowerCase() || "";
    if (!commandName) return null;
    return {
      rawText: trimmed,
      commandName,
    };
  }

  enqueueOutboxMessage(
    session,
    userId,
    nickname,
    text,
    {
      messageId = null,
      routeCMatrixDelivery = null,
      providerTextOverride = null,
    } = {}
  ) {
    const normalizedMessageId =
      typeof messageId === "string" ? messageId.trim() : "";
    const normalizedProviderTextOverride =
      typeof providerTextOverride === "string" ? providerTextOverride : null;
    if (normalizedMessageId) {
      const existing = this.getOutboxMessage(session, normalizedMessageId);
      if (existing) {
        if (
          existing.userId !== userId ||
          existing.nickname !== nickname ||
          existing.rawText !== text
        ) {
          throw new Error(
            `Outbox message ${normalizedMessageId} conflicts with an existing payload`
          );
        }
        if (routeCMatrixDelivery) {
          existing.routeCMatrixDelivery =
            existing.routeCMatrixDelivery ||
            this.normalizeRouteCMatrixDelivery(routeCMatrixDelivery);
        }
        if (
          normalizedProviderTextOverride !== null &&
          existing.providerText !== normalizedProviderTextOverride
        ) {
          throw new Error(
            `Outbox message ${normalizedMessageId} conflicts with an existing provider prompt`
          );
        }
        return existing;
      }
    }
    const slashCommand = this.parseSlashCommand(text);
    const message = {
      id: normalizedMessageId || randomUUID(),
      sequence: session._nextOutboxSequence++,
      userId,
      nickname,
      rawText: text,
      isSlashCommand: Boolean(slashCommand),
      slashCommandName: slashCommand?.commandName || null,
      attributedText: `[${nickname}]: ${text}`,
      providerText:
        normalizedProviderTextOverride !== null
          ? normalizedProviderTextOverride
          : slashCommand
          ? slashCommand.rawText
          : `[${nickname}]: ${text}`,
      state: "queued",
      createdAt: new Date().toISOString(),
      completedAt: null,
      canceledAt: null,
      error: null,
      ambiguityReason: null,
      routeCMatrixDelivery:
        this.normalizeRouteCMatrixDelivery(routeCMatrixDelivery),
      providerRuntimeEventIndex: 0,
      providerTurnId: null,
      providerTurnIdKind: null,
    };
    session._outbox.push(message);
    this.emitOutboxMessageEvent(session, message);
    this.emitDeliveryStateNotice(session);
    this.drainOutbox(session);
    return message;
  }

  cancelOutboxMessage(sessionId, messageId) {
    const session = this.requireSession(sessionId);
    const message = this.getOutboxMessage(session, messageId);
    if (!message) {
      throw new Error(`Outbox message ${messageId} not found`);
    }
    if (
      message.id === session._activeOutboxMessageId ||
      message.state !== "queued"
    ) {
      throw new Error(`Outbox message ${messageId} is not cancelable`);
    }
    this.emitOutboxCancelControlEvent(session, message, {
      semanticType: "control.request",
    });
    message.state = "canceled";
    message.canceledAt = new Date().toISOString();
    this.emitOutboxMessageEvent(session, message);
    this.emitOutboxCancelControlEvent(session, message, {
      semanticType: "control.outcome",
      outcome: "accepted",
    });
    this.emitDeliveryStateNotice(session);
    return message;
  }

  retryOutboxMessage(sessionId, messageId) {
    const session = this.requireSession(sessionId);
    const message = this.getOutboxMessage(session, messageId);
    if (!message) {
      throw new Error(`Outbox message ${messageId} not found`);
    }
    if (
      message.id !== session._activeOutboxMessageId ||
      !this.isRetryableOutboxState(message.state)
    ) {
      throw new Error(`Outbox message ${messageId} is not retryable`);
    }
    session._activeOutboxMessageId = null;
    session._activeOutboxProgressSeen = false;
    message.state = "queued";
    message.error = null;
    message.ambiguityReason = null;
    message.completedAt = null;
    this.emitOutboxMessageEvent(session, message);
    this.emitDeliveryStateNotice(session);
    this.drainOutbox(session);
    return message;
  }

  skipOutboxMessage(sessionId, messageId) {
    const session = this.requireSession(sessionId);
    const message = this.getOutboxMessage(session, messageId);
    if (!message) {
      throw new Error(`Outbox message ${messageId} not found`);
    }
    if (
      message.id !== session._activeOutboxMessageId ||
      !this.isSkippableOutboxState(message.state)
    ) {
      throw new Error(`Outbox message ${messageId} is not skippable`);
    }
    session._activeOutboxMessageId = null;
    session._activeOutboxProgressSeen = false;
    message.state = "canceled";
    message.canceledAt = new Date().toISOString();
    this.emitOutboxMessageEvent(session, message);
    this.emitDeliveryStateNotice(session);
    this.drainOutbox(session);
    return message;
  }

  canDrainOutbox(session) {
    const providerId = session.provider || session.adapterId || "claude";
    const allowFreshClaudeStartupDispatch =
      providerId === "claude" &&
      session._deliveryState === "starting" &&
      !session.providerResumeId;
    const allowClaudeInterruptRespawnDispatch =
      providerId === "claude" &&
      session._deliveryState === "restarting" &&
      session.alive === true;
    return (
      session.alive === true &&
      (session._deliveryState === "ready" ||
        allowFreshClaudeStartupDispatch ||
        allowClaudeInterruptRespawnDispatch) &&
      !session._activeOutboxMessageId
    );
  }

  claimRouteCMatrixDeliveryForDispatch(session, message) {
    const delivery = message.routeCMatrixDelivery || null;
    if (!delivery) {
      return {
        claimed: true,
        status: "no_routec_matrix_delivery",
        proof: null,
      };
    }
    const providerId =
      delivery.provider_id || session.provider || session.adapterId || null;
    const claim = claimRouteCHost2IntakeForProviderDelivery({
      hostSessionId: delivery.host_session_id,
      matrixRoomId: delivery.matrix_room_id,
      serverEventId: delivery.source_user_event_id,
      providerId,
    });
    delivery.host2_intake_proof = claim.proof || null;
    delivery.provider_id = providerId;
    delivery.provider_delivery_state = claim.status;
    delivery.provider_delivery_claimed = claim.claimed === true;
    delivery.provider_delivery_permitted = claim.claimed === true;
    delivery.provider_delivery_attempted = claim.claimed === true;
    delivery.provider_delivery_claim_id =
      claim.claimed === true ? claim.claim_id : null;
    delivery.provider_delivery_blocked_reason =
      claim.claimed === true ? null : claim.status;
    if (claim.claimed === true) {
      this.resolveOutboxProviderTurnId(session, message, delivery);
    }
    return claim;
  }

  drainOutbox(session) {
    if (!this.canDrainOutbox(session)) return;
    const message = this.getNextQueuedOutboxMessage(session);
    if (!message) return;

    const adapter = this.getAdapter(
      session.provider || session.adapterId || "claude"
    );
    message.state = "dispatching";
    message.error = null;
    message.ambiguityReason = null;
    message.providerRuntimeEventIndex = 0;
    session._activeOutboxMessageId = message.id;
    session._activeOutboxProgressSeen = false;
    this.emitOutboxMessageEvent(session, message);
    this.emitDeliveryStateNotice(session);

    try {
      const routeCClaim = this.claimRouteCMatrixDeliveryForDispatch(
        session,
        message
      );
      if (!routeCClaim.claimed) {
        session._activeOutboxMessageId = null;
        session._activeOutboxProgressSeen = false;
        if (
          routeCClaim.status ===
          "host2_queued_canceled_before_provider_delivery"
        ) {
          message.state = "canceled";
          message.canceledAt = new Date().toISOString();
          message.error = null;
        } else {
          message.state = "failed";
          message.error =
            routeCClaim.status ||
            "Route C Matrix provider delivery claim failed";
        }
        this.emitOutboxMessageEvent(session, message);
        this.emitDeliveryStateNotice(session);
        this.drainOutbox(session);
        return;
      }
      adapter.sendMessage(session, {
        userId: message.userId,
        nickname: message.nickname,
        text: message.providerText,
        rawText: message.rawText,
        isSlashCommand: message.isSlashCommand,
        slashCommandName: message.slashCommandName,
      });
      this.emit(
        "runtimeEvent",
        session.agentId,
        this.decorateEvent(session, {
          type: "message.user",
          provider: null,
          user_id: message.userId,
          nickname: message.nickname,
          text: message.rawText,
          message_id: message.id,
          created_at: message.createdAt,
          message_type: "message",
          text_entities: [],
          media: [],
          delivery_state: this.canonicalDeliveryStateForMessageState(
            message.state
          ),
        })
      );
    } catch (err) {
      session._activeOutboxMessageId = null;
      session._activeOutboxProgressSeen = false;
      message.state = "queued";
      message.error = err.message || String(err);
      this.emitOutboxMessageEvent(session, message);
      this.emit(
        "runtimeEvent",
        session.agentId,
        this.decorateEvent(session, {
          type: "runtime.error",
          provider: session.provider,
          error: message.error,
        })
      );
      this.emitDeliveryStateNotice(session);
    }
  }

  noteOutboxProgress(session, event) {
    const activeMessage = this.getActiveOutboxMessage(session);
    if (!activeMessage) return;

    if (event.type === "session.notice" && event.subtype === "turn.started") {
      activeMessage.state = "running";
      if (activeMessage.routeCMatrixDelivery) {
        const delivery = activeMessage.routeCMatrixDelivery;
        const eventProviderTurnId =
          typeof event.payload?.turn?.id === "string" &&
          event.payload.turn.id.trim()
            ? event.payload.turn.id.trim()
            : null;
        const eventProviderTurnIdKind = eventProviderTurnId
          ? "provider_reported_turn_id"
          : null;
        const { providerTurnId, providerTurnIdKind } =
          this.resolveOutboxProviderTurnId(session, activeMessage, delivery, {
            eventProviderTurnId,
            eventProviderTurnIdKind,
          });
        const providerId = delivery.provider_id || session.provider || null;
        const hostDeliveryClaimId =
          delivery.provider_delivery_claim_id || activeMessage.id;
        if (providerTurnId) {
          delivery.host2_intake_proof = markRouteCHost2IntakeAgentTurnStarted({
            hostSessionId: delivery.host_session_id,
            matrixRoomId: delivery.matrix_room_id,
            serverEventId: delivery.source_user_event_id,
            providerId,
            providerTurnId,
            providerTurnIdKind,
          });
        } else {
          delivery.diagnostic_host_delivery_claim_id = hostDeliveryClaimId;
          delivery.diagnostic_host_delivery_claim_id_kind =
            "host_delivery_claim_id";
          delivery.provider_turn_id = null;
          delivery.provider_turn_id_kind = null;
          delivery.host2_intake_start_blocked_reason =
            "provider_reported_turn_id_missing";
        }
      }
      session._activeOutboxProgressSeen = true;
      this.emitOutboxMessageEvent(session, activeMessage);
      this.emitDeliveryStateNotice(session);
      return;
    }

    if (event.type === "turn.completed" && activeMessage.routeCMatrixDelivery) {
      const delivery = activeMessage.routeCMatrixDelivery;
      const providerId = delivery.provider_id || session.provider || null;
      const hostDeliveryClaimId =
        delivery.provider_delivery_claim_id || activeMessage.id;
      const eventProviderTurnId =
        typeof event.provider_turn_id === "string" &&
        event.provider_turn_id.trim()
          ? event.provider_turn_id.trim()
          : typeof event.turn_id === "string" && event.turn_id.trim()
          ? event.turn_id.trim()
          : typeof event.turnId === "string" && event.turnId.trim()
          ? event.turnId.trim()
          : null;
      const providerTurnId =
        this.resolveOutboxProviderTurnId(session, activeMessage, delivery, {
          eventProviderTurnId,
          eventProviderTurnIdKind: event.provider_turn_id_kind,
        }).providerTurnId;
      if (!providerTurnId) {
        delivery.diagnostic_host_delivery_claim_id = hostDeliveryClaimId;
        delivery.diagnostic_host_delivery_claim_id_kind =
          "host_delivery_claim_id";
      }
      event.routec_matrix_delivery = {
        ...delivery,
        outbox_message_id: activeMessage.id,
        provider_id: providerId,
        provider_turn_id: providerTurnId,
        provider_turn_id_kind: providerTurnId
          ? delivery.provider_turn_id_kind
          : null,
        provider_runtime_event_index: null,
        semantic_type: "session_lifecycle",
      };
      if (providerTurnId) {
        event.provider_turn_id = event.provider_turn_id || providerTurnId;
        event.provider_turn_id_kind =
          event.provider_turn_id_kind || delivery.provider_turn_id_kind;
      }
      return;
    }

    if (
      event.type === "message.assistant" ||
      event.type === "message.thinking" ||
      event.type === "tool.call" ||
      event.type === "tool.output" ||
      event.type === "tool.result" ||
      event.type === "control.request" ||
      event.type === "control.outcome" ||
      event.type === "runtime.error" ||
      event.type === "session.lifecycle" ||
      event.type === "session.ready" ||
      event.type === "session.exit" ||
      event.type === "routec.semantic.absence" ||
      event.type === "stderr" ||
      (typeof event.semantic_type === "string" && event.semantic_type.trim())
    ) {
      if (activeMessage.state !== "running") {
        activeMessage.state = "running";
        this.emitOutboxMessageEvent(session, activeMessage);
        this.emitDeliveryStateNotice(session);
      }
      if (activeMessage.routeCMatrixDelivery) {
        const delivery = activeMessage.routeCMatrixDelivery;
        const providerId = delivery.provider_id || session.provider || null;
        const providerRuntimeSemanticType =
          this.routeCProviderRuntimeSemanticTypeForOutboxIndex(event);
        const providerRuntimeEventIndex = providerRuntimeSemanticType
          ? (activeMessage.providerRuntimeEventIndex || 0) + 1
          : null;
        if (providerRuntimeEventIndex !== null) {
          activeMessage.providerRuntimeEventIndex = providerRuntimeEventIndex;
        }
        const hostDeliveryClaimId =
          delivery.provider_delivery_claim_id || activeMessage.id;
        const eventProviderTurnId =
          typeof event.provider_turn_id === "string" &&
          event.provider_turn_id.trim()
            ? event.provider_turn_id.trim()
            : typeof event.turn_id === "string" && event.turn_id.trim()
            ? event.turn_id.trim()
            : typeof event.turnId === "string" && event.turnId.trim()
            ? event.turnId.trim()
            : null;
        const providerTurnId =
          this.resolveOutboxProviderTurnId(session, activeMessage, delivery, {
            eventProviderTurnId,
            eventProviderTurnIdKind: event.provider_turn_id_kind,
          }).providerTurnId;
        if (!providerTurnId) {
          delivery.diagnostic_host_delivery_claim_id = hostDeliveryClaimId;
          delivery.diagnostic_host_delivery_claim_id_kind =
            "host_delivery_claim_id";
        }
        event.routec_matrix_delivery = {
          ...delivery,
          outbox_message_id: activeMessage.id,
          provider_id: providerId,
          provider_turn_id: providerTurnId,
          provider_turn_id_kind: providerTurnId
            ? delivery.provider_turn_id_kind
            : null,
          provider_runtime_event_index: providerRuntimeEventIndex,
          semantic_type:
            typeof event.semantic_type === "string" &&
            event.semantic_type.trim()
              ? event.semantic_type.trim()
              : delivery.semantic_type,
        };
        if (providerTurnId) {
          event.provider_turn_id = event.provider_turn_id || providerTurnId;
          event.provider_turn_id_kind =
            event.provider_turn_id_kind || delivery.provider_turn_id_kind;
        }
        if (providerRuntimeEventIndex !== null) {
          event.provider_runtime_event_index =
            event.provider_runtime_event_index || providerRuntimeEventIndex;
        }
      }
      session._activeOutboxProgressSeen = true;
    }
  }

  setActiveOutboxMessageState(session, nextState, extra = {}) {
    const activeMessage = this.getActiveOutboxMessage(session);
    if (!activeMessage) return;
    activeMessage.state = nextState;
    activeMessage.error = extra.error || null;
    activeMessage.ambiguityReason = extra.ambiguityReason || null;
    if (
      nextState === "completed" ||
      nextState === "failed" ||
      nextState === "interrupted"
    ) {
      activeMessage.completedAt = new Date().toISOString();
    }
    if (nextState === "canceled") {
      activeMessage.canceledAt = new Date().toISOString();
    }
    const keepBlockingActive =
      nextState === "ambiguous" || nextState === "stalled";
    if (!keepBlockingActive) {
      session._activeOutboxMessageId = null;
      session._activeOutboxProgressSeen = false;
    }
    this.emitOutboxMessageEvent(session, activeMessage);
    this.emitDeliveryStateNotice(session);
    if (!keepBlockingActive) {
      this.drainOutbox(session);
    }
  }

  handleOutboxRuntimeEvent(session, event) {
    if (event.type === "session.ready") {
      session._deliveryState = "ready";
      this.emitDeliveryStateNotice(session);
      this.drainOutbox(session);
      return;
    }

    if (
      event.type === "session.notice" &&
      event.subtype === "interrupt.requested"
    ) {
      session._deliveryState = "interrupting";
      this.emitDeliveryStateNotice(session);
      return;
    }

    if (
      event.type === "session.notice" &&
      event.subtype === "interrupt.resuming"
    ) {
      session._deliveryState = "restarting";
      this.emitDeliveryStateNotice(session);
      return;
    }

    if (
      event.type === "session.notice" &&
      event.subtype === "interrupt.respawned"
    ) {
      this.emitDeliveryStateNotice(session);
      this.drainOutbox(session);
      return;
    }

    this.noteOutboxProgress(session, event);

    if (event.type === "turn.completed") {
      if (event.status === "interrupted") {
        this.setActiveOutboxMessageState(
          session,
          session._activeOutboxProgressSeen ? "interrupted" : "canceled"
        );
      } else if (event.status === "failed") {
        this.setActiveOutboxMessageState(session, "failed", {
          error: event.error || "Turn failed",
        });
      } else {
        this.setActiveOutboxMessageState(session, "completed");
      }
      if (
        (session._deliveryState === "interrupting" ||
          session._deliveryState === "restarting") &&
        session.resuming !== true
      ) {
        session._deliveryState = "ready";
        this.emitDeliveryStateNotice(session);
      }
    }
  }

  handleOutboxRuntimeError(session, err) {
    const activeMessage = this.getActiveOutboxMessage(session);
    if (!activeMessage) return;
    const nextState = session._activeOutboxProgressSeen
      ? "ambiguous"
      : "failed";
    this.setActiveOutboxMessageState(session, nextState, {
      error: err.message || String(err),
      ambiguityReason: session._activeOutboxProgressSeen
        ? "provider_error_after_progress"
        : null,
    });
  }

  shouldSuppressExitDerivedRuntimeError(session, err) {
    const message = err?.message || String(err || "");
    return (
      session?.provider === "codex" &&
      session?.alive !== true &&
      /exited before response \(code=0, signal=none\)/.test(message)
    );
  }

  handleOutboxExit(session) {
    const activeMessage = this.getActiveOutboxMessage(session);
    if (!activeMessage) return;
    this.setActiveOutboxMessageState(session, "ambiguous", {
      ambiguityReason: "session_exited_before_turn_completed",
    });
  }

  syncSessionRuntimeState(session, event) {
    const nextProviderResumeId = normalizeProviderMetadataId(
      event.provider_resume_id
    );
    if (nextProviderResumeId) {
      session.providerResumeId = nextProviderResumeId;
    }
    const nextProviderThreadId = normalizeProviderMetadataId(
      event.provider_thread_id || event.thread_id
    );
    if (nextProviderThreadId) {
      session.threadId = nextProviderThreadId;
      if (
        !session.providerResumeId &&
        (session.provider || session.adapterId) === "codex"
      ) {
        session.providerResumeId = nextProviderThreadId;
      }
    }

    if (event.type === "session.ready") {
      session._ready = true;
      if (typeof event.model === "string" && event.model.trim()) {
        session.model = event.model.trim();
      }
      if (
        typeof event.permissionMode === "string" &&
        event.permissionMode.trim()
      ) {
        session.permissionMode = event.permissionMode.trim();
      }
      if (typeof event.cwd === "string" && event.cwd.trim()) {
        session.cwd = event.cwd.trim();
      }
      if (Number.isInteger(event.toolsCount) && event.toolsCount >= 0) {
        session.toolsCount = event.toolsCount;
      }
      return;
    }

    if (
      event.type === "session.notice" &&
      event.subtype === "permission_mode.updated" &&
      typeof event.payload?.permissionMode === "string" &&
      event.payload.permissionMode.trim()
    ) {
      session.permissionMode = event.payload.permissionMode.trim();
    }
  }

  getAdapter(providerId = "claude") {
    const provider = requireProvider(providerId, {
      config: this.getCurrentConfig(),
    });
    const adapter = this.adapters.get(provider.id) || null;
    if (!adapter) {
      throw new Error(
        `Provider "${provider.id}" is configured but runtime is not implemented yet`
      );
    }
    return adapter;
  }

  assertConfiguredProviderCommandAvailable(providerId, adapter) {
    if (!adapter || typeof adapter.getConfiguredCommand !== "function") {
      return;
    }
    const configuredCommand = adapter.getConfiguredCommand();
    if (!configuredCommand) {
      return;
    }
    if (isConfiguredCommandAvailable(configuredCommand)) {
      return;
    }
    throw createProviderUnavailableError(providerId, {
      command: configuredCommand,
    });
  }

  /**
   * Start a new interactive session for an agent.
   * @param {object} opts
   * @param {string} opts.agentId - Agent identifier
   * @param {string} opts.cwd - Working directory for the session
   * @param {string} [opts.sessionId] - Reuse a specific session ID (for resume)
   * @param {string} [opts.model] - Model override (e.g. "sonnet", "opus")
   * @param {boolean} [opts.resume] - Resume an existing session by sessionId
   * @param {string} [opts.permissionMode] - Permission mode (e.g. "default")
   * @param {boolean} [opts.chromeEnabled]
   * @param {boolean} [opts.allowDangerouslySkipPermissions]
   * @returns {Session}
   */
  start({
    agentId,
    cwd,
    provider = "claude",
    sessionId,
    sessionName,
    parentSessionId = null,
    resumeSessionId,
    fork = false,
    model,
    reasoningEffort,
    reasoningEffortSource,
    resume = false,
    permissionMode,
    approvalPolicy,
    sandboxMode,
    dangerousMode,
    chromeEnabled,
    allowDangerouslySkipPermissions,
    searchEnabled,
    imageInputEnabled,
    native,
    workspacePolicy,
    assetReadablePaths,
    runtimeCapabilities,
    runtimeCapabilityEnv,
    runtimeCapabilityGrant,
    runtimeCapabilityRedactionValues,
    requiredProductSkills,
  }) {
    const id = sessionId || randomUUID();
    if (this.sessions.has(id)) {
      throw new Error(`Session ${id} already exists`);
    }
    const resolvedSessionName =
      normalizeSessionName(sessionName) ||
      buildUniqueDefaultSessionName(
        agentId,
        (candidate) => this.hasRunningSessionName(candidate),
        () => getSessionNameCounterStore().nextAgentSessionCounter(agentId)
      );
    if (this.hasRunningSessionName(resolvedSessionName)) {
      throw new Error(buildSessionNameConflictMessage(resolvedSessionName));
    }
    const adapter = this.getAdapter(provider);
    this.assertConfiguredProviderCommandAvailable(provider, adapter);
    const productSkillCopy = copyRequiredProductSkillsToAgent({
      cwd,
      requiredProductSkills,
      productSkillRoots: this.productSkillRoots,
    });
    const session = adapter.startSession({
      sessionId: id,
      cwd,
      agentId,
      resumeSessionId:
        resume === true ? resumeSessionId || sessionId : undefined,
      fork,
      model,
      reasoningEffort,
      reasoningEffortSource,
      permissionMode,
      approvalPolicy,
      sandboxMode,
      dangerousMode,
      chromeEnabled,
      allowDangerouslySkipPermissions,
      searchEnabled,
      imageInputEnabled,
      native,
      workspacePolicy,
      assetReadablePaths,
      runtimeEnv: normalizeRuntimeEnv(runtimeCapabilityEnv),
    });
    session.adapterId = adapter.providerId;
    session.workspacePolicy = workspacePolicy ?? null;
    session.assetReadablePaths = normalizeAssetReadablePaths(
      assetReadablePaths || session.assetReadablePaths || []
    );
    session.runtimeCapabilities =
      runtimeCapabilities && typeof runtimeCapabilities === "object"
        ? JSON.parse(JSON.stringify(runtimeCapabilities))
        : {};
    session.runtimeCapabilityEnv = normalizeRuntimeEnv(runtimeCapabilityEnv);
    session.runtimeCapabilityGrant =
      runtimeCapabilityGrant && typeof runtimeCapabilityGrant === "object"
        ? JSON.parse(JSON.stringify(runtimeCapabilityGrant))
        : null;
    session.runtimeCapabilityRedactionValues = normalizeRedactionValues(
      runtimeCapabilityRedactionValues
    );
    session.requiredProductSkills = productSkillCopy.skills;
    session.productSkillCopyContract = productSkillCopy.contract;
    session.productSkillCopyResult = productSkillCopy.provided
      ? {
          contract: productSkillCopy.contract,
          skills: productSkillCopy.skills,
          copied: productSkillCopy.copied.map((entry) => ({
            skill: entry.skill,
            file_count: entry.files.length,
          })),
        }
      : null;
    session.chromeEnabled = chromeEnabled === true;
    session.allowDangerouslySkipPermissions =
      allowDangerouslySkipPermissions === true;
    session.searchEnabled = searchEnabled === true;
    session.imageInputEnabled = imageInputEnabled === true;
    session.native = native ||
      session.native || { args: [], configOverrides: {}, profile: null };
    session.provider = session.provider || adapter.providerId;
    if (!session.provider) {
      throw new Error("Session provider is missing after adapter start");
    }
    session.transport =
      session.transport || (session.provider === "codex" ? "app-server" : null);
    session.resumeSessionId =
      resume === true
        ? resumeSessionId || session.resumeSessionId || sessionId || null
        : session.resumeSessionId || null;
    session.capabilities = session.capabilities || adapter.capabilities || {};
    session.model = session.model ?? model ?? null;
    session.reasoningEffort =
      session.reasoningEffort ?? reasoningEffort ?? null;
    session.reasoningEffortSource =
      session.reasoningEffortSource ?? reasoningEffortSource ?? null;
    session.permissionMode = session.permissionMode ?? permissionMode ?? null;
    session.toolsCount = Number.isInteger(session.toolsCount)
      ? session.toolsCount
      : null;
    // Claude accepts stdin before system.init arrives. Mark the session ready
    // immediately so chat opens and /login can be sent through the normal path.
    if (
      session.provider === "claude" &&
      session.alive === true &&
      session._ready !== true
    ) {
      session._ready = true;
    }
    session.approvalPolicy = session.approvalPolicy ?? approvalPolicy ?? null;
    session.sandboxMode = session.sandboxMode ?? sandboxMode ?? null;
    session.dangerousMode = dangerousMode === true;
    session.sessionName = resolvedSessionName;
    session.historyCreatedAt = new Date().toISOString();
    session.historyRecordId = session.id;
    session.parentSessionId = parentSessionId || null;
    session.bookkeepingWarnings = [];

    const recordBookkeepingWarning = (subtype, err) => {
      const warning = `${subtype}: ${err.message}`;
      session.bookkeepingWarnings.push(warning);
      console.error(`[session-manager] ${agentId} ${warning}`);
      this.emit(
        "runtimeEvent",
        agentId,
        this.decorateEvent(session, {
          type: "session.notice",
          provider: session.provider,
          subtype,
          error: err.message,
        })
      );
    };

    const buildHistoryRecord = (lastActiveAt = session.historyCreatedAt) => ({
      session_id: session.id,
      session_name: session.sessionName,
      parent_session_id: session.parentSessionId,
      agent_id: agentId,
      agent_folder: cwd,
      runtime: session.provider,
      model: session.model,
      provider_resume_id: this.getEffectiveProviderResumeId(session),
      provider_thread_id: session.threadId || null,
      provider_transport: session.transport || null,
      created_at: session.historyCreatedAt,
      last_active_at: lastActiveAt,
    });

    const buildHistoryMetadataSnapshot = () =>
      JSON.stringify({
        runtime: session.provider,
        model: session.model,
        provider_resume_id: this.getEffectiveProviderResumeId(session),
        provider_thread_id: session.threadId || null,
        provider_transport: session.transport || null,
      });

    session.historyMetadataSnapshot = null;
    session.lastActiveAt = session.historyCreatedAt || new Date().toISOString();
    this.initializeDeliveryState(session);

    // Relay events with agentId context
    session.on("event", (event) => {
      this.handleOutboxRuntimeEvent(session, event);
      this.syncSessionRuntimeState(session, event);
      if (
        event.type === "message.user" ||
        event.type === "message.assistant" ||
        event.type === "turn.completed"
      ) {
        session.lastActiveAt = new Date().toISOString();
      }
      if (
        event.type === "session.ready" &&
        session.id !== session.historyRecordId
      ) {
        try {
          saveSessionRecord(buildHistoryRecord());
          deleteSessionRecord(session.historyRecordId);
          session.historyRecordId = session.id;
        } catch (err) {
          recordBookkeepingWarning("history.ready_sync_failed", err);
        }
      }
      const nextHistoryMetadata = buildHistoryMetadataSnapshot();
      if (
        event.type === "session.ready" ||
        event.type === "turn.completed" ||
        event.type === "session.exit" ||
        nextHistoryMetadata !== session.historyMetadataSnapshot
      ) {
        try {
          updateSessionRecord(
            session.historyRecordId || session.id,
            buildHistoryRecord(new Date().toISOString())
          );
          session.historyMetadataSnapshot = nextHistoryMetadata;
        } catch (err) {
          recordBookkeepingWarning("history.runtime_sync_failed", err);
        }
      }
      this.emit("runtimeEvent", agentId, this.decorateEvent(session, event));
    });
    session.on("error", (err) => {
      if (this.shouldSuppressExitDerivedRuntimeError(session, err)) {
        return;
      }
      this.handleOutboxRuntimeError(session, err);
      this.emit(
        "runtimeEvent",
        agentId,
        this.decorateEvent(session, {
          type: "runtime.error",
          provider: session.provider,
          error: err.message || String(err),
        })
      );
    });
    session.on("exit", (code) => {
      this.handleOutboxExit(session);
      this.sessions.delete(id);
      this.removeAgentSession(agentId, id);
      try {
        updateSessionRecord(
          session.historyRecordId || session.id,
          buildHistoryRecord(new Date().toISOString())
        );
      } catch (err) {
        this.emit(
          "runtimeEvent",
          agentId,
          this.decorateEvent(session, {
            type: "session.notice",
            provider: session.provider,
            subtype: "history.update_failed",
            error: err.message,
          })
        );
      }
      this.emit(
        "runtimeEvent",
        agentId,
        this.decorateEvent(session, {
          type: "session.exit",
          provider: session.provider,
          code,
        })
      );
    });

    this.sessions.set(id, session);
    this.addAgentSession(agentId, id);
    try {
      saveSessionRecord(buildHistoryRecord());
      session.historyMetadataSnapshot = buildHistoryMetadataSnapshot();
    } catch (err) {
      recordBookkeepingWarning("history.start_write_failed", err);
    }
    this.emit(
      "runtimeEvent",
      agentId,
      this.decorateEvent(session, {
        type: "session.started",
        provider: session.provider,
      })
    );

    return session;
  }

  addAgentSession(agentId, sessionId) {
    if (!this.agentSessions.has(agentId)) {
      this.agentSessions.set(agentId, new Set());
    }
    this.agentSessions.get(agentId).add(sessionId);
  }

  removeAgentSession(agentId, sessionId) {
    const sessionIds = this.agentSessions.get(agentId);
    if (!sessionIds) return;
    sessionIds.delete(sessionId);
    if (sessionIds.size === 0) {
      this.agentSessions.delete(agentId);
    }
  }

  getAgentSessionId(agentId) {
    const sessionIds = this.agentSessions.get(agentId);
    if (!sessionIds || sessionIds.size === 0) return null;
    let latestSessionId = null;
    for (const candidateId of sessionIds) {
      if (this.sessions.has(candidateId)) {
        latestSessionId = candidateId;
      }
    }
    return latestSessionId;
  }

  requireSession(sessionId) {
    if (!sessionId) throw new Error("sessionId required");
    const session = this.sessions.get(sessionId);
    if (!session) throw new Error(`Session ${sessionId} not found`);
    return session;
  }

  /**
   * Send a user message to an agent's session.
   * Includes user attribution for shared chat.
   */
  sendToAgent(agentId, userId, nickname, text) {
    const session = this.getAgentSession(agentId);
    if (!session) throw new Error(`No active session for agent ${agentId}`);
    this.sendToSession(session.id, userId, nickname, text);
  }

  sendToSession(sessionId, userId, nickname, text, opts = {}) {
    const session = this.requireSession(sessionId);
    return this.enqueueOutboxMessage(session, userId, nickname, text, opts);
  }

  deliverRouteCMatrixUserEventToProvider({
    sessionId,
    matrixUserId = null,
    matrixRoomId,
    serverEventId,
    txnId = null,
    text,
    providerText = null,
    nickname = "Host Owner",
  }) {
    const session = this.requireSession(sessionId);
    const providerId = session.provider || session.adapterId || "unknown";
    const realProviderDeliveryMarker =
      routeCRealProviderDeliveryMarker(providerId);
    if (!realProviderDeliveryMarker) {
      const proof = blockRouteCHost2IntakeProviderDelivery({
        hostSessionId: session.id,
        matrixRoomId,
        serverEventId,
        providerId,
        reason:
          "provider_delivery_unsupported_provider_for_routec_host_delivery",
      });
      return {
        status: "provider_delivery_blocked_unsupported_provider",
        delivered: false,
        proof,
      };
    }
    const delivery = {
      marker: realProviderDeliveryMarker,
      host_session_id: session.id,
      matrix_room_id: matrixRoomId,
      matrix_user_id: matrixUserId,
      matrix_txn_id: txnId,
      source_user_event_id: serverEventId,
      source_user_event_id_hash: this.routeCStableHash(serverEventId),
      provider_id: providerId,
      provider_delivery_claim_id: null,
      provider_delivery_claimed: false,
      provider_delivery_permitted: false,
      provider_delivery_attempted: false,
      provider_delivery_blocked_reason: null,
      provider_delivery_state: "provider_delivery_queued",
      host_provider_delivery_path: `host_session_outbox_${providerId}_adapter_real_provider`,
      semantic_type: "message.assistant",
      semantic_contract: routeCRealProviderSemanticContract(providerId),
      real_codex_host_delivery: providerId === "codex",
      real_claude_host_delivery: providerId === "claude",
      real_provider_host_delivery:
        providerId === "codex" || providerId === "claude",
      debug_fixture_host_delivery: providerId === "debug-fixture",
      direct_matrix_harness_write_used: false,
      direct_host_send_used: false,
      real_codex_acceptance_claimed: false,
      real_codex_e2e_claimed: false,
      full_provider_parity_claimed: false,
    };
    const deterministicMessageId = `routec_host2_${this.routeCStableHash(
      `${session.id}:${matrixRoomId}:${serverEventId}`
    ).slice(0, 32)}`;
    try {
      const queuedMessage = this.sendToSession(
        session.id,
        matrixUserId || "routec-matrix-user",
        nickname,
        text,
        {
          messageId: deterministicMessageId,
          routeCMatrixDelivery: delivery,
          providerTextOverride: providerText,
        }
      );
      return {
        status:
          providerId === "codex"
            ? "real_codex_provider_delivery_queued"
            : providerId === "claude"
            ? "real_claude_provider_delivery_queued"
            : "provider_delivery_queued",
        delivered: true,
        provider_delivery_claimed:
          queuedMessage.routeCMatrixDelivery?.provider_delivery_claimed ===
          true,
        provider_delivery_permitted:
          queuedMessage.routeCMatrixDelivery?.provider_delivery_permitted ===
          true,
        provider_delivery_attempted:
          queuedMessage.routeCMatrixDelivery?.provider_delivery_attempted ===
          true,
        provider_delivery_blocked_reason:
          queuedMessage.routeCMatrixDelivery
            ?.provider_delivery_blocked_reason || null,
        provider_id: providerId,
        message_id: queuedMessage.id,
        proof:
          queuedMessage.routeCMatrixDelivery?.host2_intake_proof ||
          getRouteCHost2IntakeProof({
            hostSessionId: session.id,
            matrixRoomId,
            serverEventId,
          }),
      };
    } catch (err) {
      const proof = recordRouteCProviderDeliveryFailure({
        hostSessionId: session.id,
        matrixRoomId,
        serverEventId,
        providerId,
        error: err.message || String(err),
      });
      throw new Error(
        `Route C provider delivery failed for Matrix event ${serverEventId}: ${
          err.message || err
        }`,
        {
          cause: err,
        }
      );
    }
  }

  /**
   * Get newest live session for an agent.
   */
  getAgentSession(agentId) {
    const sessionId = this.getAgentSessionId(agentId);
    if (!sessionId) return null;
    return this.sessions.get(sessionId) || null;
  }

  getSession(sessionId) {
    if (!sessionId) return null;
    return this.sessions.get(sessionId) || null;
  }

  updateRuntimeCapabilities(sessionId, runtimeCapabilities = {}) {
    const session = this.requireSession(sessionId);
    if (
      !runtimeCapabilities ||
      typeof runtimeCapabilities !== "object" ||
      Array.isArray(runtimeCapabilities)
    ) {
      throw new Error("runtimeCapabilities must be an object");
    }
    session.runtimeCapabilities = JSON.parse(
      JSON.stringify(runtimeCapabilities)
    );
    this.emit(
      "runtimeEvent",
      session.agentId,
      this.decorateEvent(session, {
        type: "session.notice",
        provider: session.provider || "claude",
        subtype: "runtime.capabilities.updated",
        payload: {
          session_id: session.id,
          current_session_only: true,
          config_mutated: false,
          runtime_capabilities: session.runtimeCapabilities,
        },
      })
    );
    return session.runtimeCapabilities;
  }

  renameSession(sessionId, sessionName) {
    const session = this.requireSession(sessionId);
    const resolvedSessionName = normalizeSessionName(sessionName);
    if (!resolvedSessionName) {
      throw new Error("session_name required");
    }
    const currentSessionName = normalizeSessionName(session.sessionName);
    if (currentSessionName === resolvedSessionName) {
      return { session, changed: false };
    }
    if (
      this.hasRunningSessionName(resolvedSessionName, {
        excludeSessionId: sessionId,
      })
    ) {
      throw new Error(buildSessionNameConflictMessage(resolvedSessionName));
    }

    updateSessionRecord(session.historyRecordId || session.id, {
      session_name: resolvedSessionName,
      last_active_at: new Date().toISOString(),
    });
    session.sessionName = resolvedSessionName;
    this.emit(
      "runtimeEvent",
      session.agentId,
      this.decorateEvent(session, {
        type: "session.notice",
        subtype: "session_name.updated",
        session_name: resolvedSessionName,
        payload: {
          sessionName: resolvedSessionName,
        },
      })
    );
    return { session, changed: true };
  }

  registerSessionAssetReadablePaths(sessionId, assetReadablePaths = []) {
    const session = this.requireSession(sessionId);
    session.assetReadablePaths = normalizeAssetReadablePaths([
      ...(Array.isArray(session.assetReadablePaths)
        ? session.assetReadablePaths
        : []),
      ...assetReadablePaths,
    ]);
    return session.assetReadablePaths;
  }

  getAgentSessions(agentId) {
    const sessionIds = this.agentSessions.get(agentId);
    if (!sessionIds || sessionIds.size === 0) return [];
    return [...sessionIds]
      .map((sessionId) => this.sessions.get(sessionId) || null)
      .filter(Boolean);
  }

  interruptAgent(agentId) {
    const session = this.getAgentSession(agentId);
    if (!session) throw new Error(`No active session for agent ${agentId}`);
    this.interruptSession(session.id);
  }

  interruptSession(sessionId) {
    const session = this.requireSession(sessionId);
    const adapter = this.getAdapter(
      session.provider || session.adapterId || "claude"
    );
    if (typeof adapter.interruptSession !== "function") {
      throw new Error(
        `Provider "${
          session.provider || session.adapterId || "claude"
        }" does not support interrupt`
      );
    }
    session._deliveryState = "interrupting";
    this.emitDeliveryStateNotice(session);
    adapter.interruptSession(session);
  }

  waitForSessionExit(session, timeoutMs = 8000) {
    return new Promise((resolve, reject) => {
      if (!session.alive) {
        resolve();
        return;
      }

      const timer = setTimeout(() => {
        cleanup();
        reject(
          new Error(`Timed out waiting for session ${session.id} to exit`)
        );
      }, timeoutMs);

      const onExit = () => {
        cleanup();
        resolve();
      };

      const cleanup = () => {
        clearTimeout(timer);
        session.off("exit", onExit);
      };

      session.on("exit", onExit);
    });
  }

  /**
   * Gracefully stop an agent's session.
   */
  async stopAgent(agentId) {
    const session = this.getAgentSession(agentId);
    if (!session) throw new Error(`No active session for agent ${agentId}`);
    await this.stopSession(session.id);
  }

  async stopSession(sessionId) {
    const session = this.requireSession(sessionId);
    const agentId = session.agentId;
    const adapter = this.getAdapter(
      session.provider || session.adapterId || "claude"
    );
    adapter.stopSession(session);
    try {
      await this.waitForSessionExit(session);
    } catch (err) {
      this.emit(
        "runtimeEvent",
        agentId,
        this.decorateEvent(session, {
          type: "session.notice",
          provider: session.provider,
          subtype: "stop.timeout",
          error: err.message,
        })
      );
      adapter.killSession(session);
      await this.waitForSessionExit(session, 5000);
    }
  }

  /**
   * Force kill an agent's session.
   */
  async killAgent(agentId) {
    const session = this.getAgentSession(agentId);
    if (!session) throw new Error(`No active session for agent ${agentId}`);
    await this.killSession(session.id);
  }

  async killSession(sessionId) {
    const session = this.requireSession(sessionId);
    const adapter = this.getAdapter(
      session.provider || session.adapterId || "claude"
    );
    adapter.killSession(session);
    await this.waitForSessionExit(session, 5000);
  }

  /**
   * Restart an agent's session with adapter-level resume support.
   * @param {string} agentId
   * @param {object} [overrides] - Optional overrides for model, permissionMode, chromeEnabled, allowDangerouslySkipPermissions, workspacePolicy
   */
  async restartAgent(agentId, overrides = {}) {
    const session = this.getAgentSession(agentId);
    if (!session) throw new Error(`No active session for agent ${agentId}`);
    return this.restartSession(session.id, overrides);
  }

  async restartSession(sessionId, overrides = {}) {
    const session = this.requireSession(sessionId);

    const cwd = session.cwd;
    const agentId = session.agentId;
    const currentProvider = session.provider || session.adapterId || "claude";
    const nextProvider = overrides.provider || currentProvider;
    const providerResumeId =
      nextProvider === currentProvider
        ? this.getEffectiveProviderResumeId(session) || session.id
        : undefined;
    const nextSessionId = session.id;
    const workspacePolicy =
      overrides.workspacePolicy ?? session.workspacePolicy;
    const assetReadablePaths =
      overrides.assetReadablePaths ?? session.assetReadablePaths ?? [];

    // Stop the session gracefully
    const adapter = this.getAdapter(
      session.provider || session.adapterId || "claude"
    );
    if (
      typeof adapter.supportsResume === "function" &&
      adapter.supportsResume() !== true
    ) {
      throw new Error(
        `Provider "${
          session.provider || session.adapterId || "claude"
        }" does not support restart/resume yet`
      );
    }
    adapter.stopSession(session);

    // Wait for exit
    await new Promise((resolve) => {
      if (!session.alive) return resolve();
      session.once("exit", resolve);
    });

    // Start new session with --resume (and optional overrides)
    return this.start({
      agentId,
      cwd,
      provider: nextProvider,
      sessionId: nextSessionId,
      sessionName: overrides.sessionName ?? session.sessionName,
      resumeSessionId: providerResumeId,
      resume: providerResumeId !== undefined,
      model: overrides.model,
      reasoningEffort: overrides.reasoningEffort ?? session.reasoningEffort,
      reasoningEffortSource:
        overrides.reasoningEffortSource ?? session.reasoningEffortSource,
      permissionMode: overrides.permissionMode,
      approvalPolicy: overrides.approvalPolicy,
      sandboxMode: overrides.sandboxMode,
      dangerousMode: overrides.dangerousMode,
      chromeEnabled: overrides.chromeEnabled ?? session.chromeEnabled,
      allowDangerouslySkipPermissions:
        overrides.allowDangerouslySkipPermissions ??
        session.allowDangerouslySkipPermissions,
      searchEnabled: overrides.searchEnabled ?? session.searchEnabled,
      imageInputEnabled:
        overrides.imageInputEnabled ?? session.imageInputEnabled,
      native: overrides.native ?? session.native,
      workspacePolicy,
      assetReadablePaths,
      runtimeCapabilities:
        overrides.runtimeCapabilities ?? session.runtimeCapabilities,
      runtimeCapabilityEnv:
        overrides.runtimeCapabilityEnv ?? session.runtimeCapabilityEnv,
      runtimeCapabilityGrant:
        overrides.runtimeCapabilityGrant ?? session.runtimeCapabilityGrant,
      runtimeCapabilityRedactionValues:
        overrides.runtimeCapabilityRedactionValues ??
        session.runtimeCapabilityRedactionValues,
    });
  }

  /**
   * Stop all sessions.
   */
  stopAll() {
    for (const [, session] of this.sessions) {
      const adapter = this.getAdapter(
        session.provider || session.adapterId || "claude"
      );
      adapter.stopSession(session);
    }
  }

  /**
   * List active sessions with agent mapping.
   */
  list() {
    return [...this.sessions.entries()].map(([id, s]) => ({
      sessionId: id,
      sessionName: s.sessionName || null,
      agentId: s.agentId,
      provider: s.provider || "claude",
      cwd: s.cwd,
      model: s.model ?? null,
      reasoningEffort: s.reasoningEffort ?? null,
      permissionMode: s.permissionMode ?? null,
      approvalPolicy: s.approvalPolicy ?? null,
      sandboxMode: s.sandboxMode ?? null,
      dangerousMode: s.dangerousMode === true,
      alive: s.alive,
      ready: s._ready,
      capabilities: s.capabilities || {},
      runtimeCapabilities: s.runtimeCapabilities || {},
      runtimeCapabilityGrant: s.runtimeCapabilityGrant || null,
      chromeEnabled: s.chromeEnabled === true,
      allowDangerouslySkipPermissions:
        s.allowDangerouslySkipPermissions === true,
      searchEnabled: s.searchEnabled === true,
      imageInputEnabled: s.imageInputEnabled === true,
      native: s.native || {
        args: [],
        configOverrides: {},
        commands: [],
        profile: null,
      },
      workspacePolicy: s.workspacePolicy,
      providerResumeId: this.getEffectiveProviderResumeId(s),
      providerThreadId: s.threadId || null,
      transport: s.transport || null,
      toolsCount: Number.isInteger(s.toolsCount) ? s.toolsCount : null,
      startedAt: s.historyCreatedAt || null,
      lastActiveAt: s.lastActiveAt || s.historyCreatedAt || null,
    }));
  }

  hasRunningSessionName(sessionName, opts = {}) {
    const normalized = normalizeSessionName(sessionName);
    if (!normalized) return false;
    const excludeSessionId = opts.excludeSessionId || null;
    for (const [sessionId, session] of this.sessions.entries()) {
      if (excludeSessionId && sessionId === excludeSessionId) continue;
      if (normalizeSessionName(session.sessionName) === normalized) {
        return true;
      }
    }
    return false;
  }

  decorateEvent(session, event) {
    const decorated = {
      agentId: session.agentId,
      sessionId: session.id,
      provider: session.provider || "claude",
      reasoning_effort: session.reasoningEffort ?? null,
      created_at: event.created_at || new Date().toISOString(),
      ...event,
    };
    return redactRuntimeCapabilityPayload(
      decorated,
      normalizeRedactionValues(session.runtimeCapabilityRedactionValues)
    );
  }
}
