#!/usr/bin/env node
import http from 'node:http'
import os from 'node:os'
import path from 'node:path'
import fs from 'node:fs/promises'
import fssync from 'node:fs'
import { spawn } from 'node:child_process'
import { fileURLToPath } from 'node:url'

const HOST = process.env.AI_BRIDGE_HOST || '0.0.0.0'
const PORT = Number(process.env.AI_BRIDGE_PORT || 8787)
const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex')
const SESSION_INDEX = process.env.AI_BRIDGE_SESSION_INDEX || path.join(CODEX_HOME, 'session_index.jsonl')
const SESSIONS_DIR = process.env.AI_BRIDGE_SESSIONS_DIR || path.join(CODEX_HOME, 'sessions')
const CODEX_BIN = process.env.AI_BRIDGE_CODEX_BIN || 'codex'
const DEFAULT_CODEX_ARGS = '["exec","--json","--skip-git-repo-check","-c","model_reasoning_summary=detailed","-c","model_supports_reasoning_summaries=true","-c","use_experimental_reasoning_summary=true","-c","hide_agent_reasoning=false","-c","show_raw_agent_reasoning=false","-"]'
const CODEX_ARGS = parseArgs(process.env.AI_BRIDGE_CODEX_ARGS || DEFAULT_CODEX_ARGS)
const CODEX_RESUME_ARGS = parseArgs(process.env.AI_BRIDGE_CODEX_RESUME_ARGS || '')
const CODEX_CWD = process.env.AI_BRIDGE_CODEX_CWD || process.cwd()
const MESSAGE_TIMEOUT_MS = Number(process.env.AI_BRIDGE_MESSAGE_TIMEOUT_MS || 12 * 60 * 60 * 1000)
const RESUME_TIMEOUT_MS = Number(process.env.AI_BRIDGE_RESUME_TIMEOUT_MS || MESSAGE_TIMEOUT_MS)
const MAX_BODY_BYTES = Number(process.env.AI_BRIDGE_MAX_BODY_BYTES || 1024 * 1024)
const MAX_STDIO_BYTES = Number(process.env.AI_BRIDGE_MAX_STDIO_BYTES || 2 * 1024 * 1024)
const MAX_CONVERSATIONS = Number(process.env.AI_BRIDGE_MAX_CONVERSATIONS || 100)
const MAX_SESSION_FILES = Number(process.env.AI_BRIDGE_MAX_SESSION_FILES || 5000)
const MAX_SESSION_FILE_BYTES = Number(process.env.AI_BRIDGE_MAX_SESSION_FILE_BYTES || 64 * 1024 * 1024)
const MAX_MESSAGES_PER_CONVERSATION = Number(process.env.AI_BRIDGE_MAX_MESSAGES_PER_CONVERSATION || 300)
const MAX_MESSAGE_CHARS = Number(process.env.AI_BRIDGE_MAX_MESSAGE_CHARS || 12000)
const MAX_WORKSPACES = Number(process.env.AI_BRIDGE_MAX_WORKSPACES || 120)
const REASONING_SUMMARY_DELTA_MIN_CHARS = Number(process.env.AI_BRIDGE_REASONING_SUMMARY_DELTA_MIN_CHARS || 80)
const REASONING_SUMMARY_DELTA_MAX_WAIT_MS = Number(process.env.AI_BRIDGE_REASONING_SUMMARY_DELTA_MAX_WAIT_MS || 1800)
const CODEX_STOP_GRACE_MS = Number(process.env.AI_BRIDGE_CODEX_STOP_GRACE_MS || 2500)
const STALE_RUNNING_SESSION_MS = Number(process.env.AI_BRIDGE_STALE_RUNNING_SESSION_MS || 10 * 60 * 1000)
const FINAL_MESSAGE_PHASES = new Set(['final', 'final_answer', 'answer'])
const BRIDGE_FILE = fileURLToPath(import.meta.url)
const BRIDGE_DIR = path.dirname(BRIDGE_FILE)
const NDC_AGENT_CODEX_HOME = path.resolve(process.env.AI_BRIDGE_NDC_AGENT_CODEX_HOME || path.join(BRIDGE_DIR, 'codex-home-ndc-agent-core'))
const RUN_CODEX_HOME_ROOT = path.resolve(process.env.AI_BRIDGE_RUN_CODEX_HOME_ROOT || path.join(BRIDGE_DIR, 'codex-home-runs'))
const DEFAULT_NDC_AGENT_MCP_API_BASE = 'http://127.0.0.1:3001/api/ndc-agent-mcp'
const HUB_URL = String(process.env.AI_BRIDGE_HUB_URL || '').trim()
const HUB_URLS = parseHubUrls(process.env.AI_BRIDGE_HUB_URLS || '', HUB_URL)
const PAIRING_CODE = String(process.env.AI_BRIDGE_PAIRING_CODE || '').trim()
const MACHINE_NAME = String(process.env.AI_BRIDGE_MACHINE_NAME || os.hostname()).trim()
const HUB_RECONNECT_MS = Number(process.env.AI_BRIDGE_HUB_RECONNECT_MS || 5000)
const HUB_CONNECT_TIMEOUT_MS = Number(process.env.AI_BRIDGE_HUB_CONNECT_TIMEOUT_MS || 10000)
const BRIDGE_AGENT_VERSION = String(process.env.AI_BRIDGE_AGENT_VERSION || '0.0.0').trim()
const BRIDGE_PROTOCOL_VERSION = String(process.env.AI_BRIDGE_PROTOCOL_VERSION || 'ai-workspace-bridge.v1').trim()
const BRIDGE_CAPABILITIES = parseBridgeCapabilities(process.env.AI_BRIDGE_CAPABILITIES || [
  'hub-outbound',
  'local-health',
  'codex-exec',
  'codex-resume',
  'codex-stop',
  'dynamic-run-profile',
  'dynamic-mcp',
  'bundled-skills',
].join(','))
const hubState = {
  mode: Boolean(HUB_URLS.length && PAIRING_CODE),
  connected: false,
  url: '',
  pairingCode: PAIRING_CODE,
  candidates: HUB_URLS,
  lastConnectedAt: '',
  lastDisconnectedAt: '',
  lastError: '',
}
const ACTIVE_MIRROR_ENABLED = process.env.AI_BRIDGE_ACTIVE_MIRROR !== '0'
const ACTIVE_MIRROR_DIR = resolveWorkspacePath(process.env.AI_BRIDGE_ACTIVE_MIRROR_DIR || path.join('.nodedc', 'ai-workspace'))
const ACTIVE_MIRROR_CURRENT_FILE = path.join(ACTIVE_MIRROR_DIR, 'current.md')
const ACTIVE_MIRROR_EVENTS_FILE = path.join(ACTIVE_MIRROR_DIR, 'events.jsonl')
const ACTIVE_MIRROR_MAX_EVENTS = Number(process.env.AI_BRIDGE_ACTIVE_MIRROR_MAX_EVENTS || 80)
const ACTIVE_MIRROR_OPEN_IDE = process.env.AI_BRIDGE_ACTIVE_MIRROR_OPEN_IDE !== '0'
const ACTIVE_MIRROR_IDE_BIN = String(process.env.AI_BRIDGE_ACTIVE_MIRROR_IDE_BIN || 'code').trim()
const NDC_AGENT_MCP_SERVER = resolveWorkspacePath(defaultNdcAgentMcpServerPath())
const NDC_AGENT_MCP_REF_PATH = process.env.AI_BRIDGE_NDC_AGENT_MCP_REF_PATH ||
  path.resolve(path.dirname(NDC_AGENT_MCP_SERVER), '..', '..', '..', 'tools', 'NDCMCP')
const activeCodexRunsByRequest = new Map()
const activeCodexRunsByThread = new Map()

function defaultNdcAgentMcpServerPath() {
  const explicit = String(process.env.AI_BRIDGE_NDC_AGENT_MCP_SERVER || '').trim()
  if (explicit) return explicit
  const installed = path.join(BRIDGE_DIR, 'ndcAgentMcpServer.mjs')
  if (fssync.existsSync(installed)) return installed
  const cwdLeaf = path.basename(String(CODEX_CWD || '').replace(/[\\/]+$/, ''))
  if (cwdLeaf === 'nodedc-source') return path.join('server', 'aiWorkspace', 'ndcAgentMcpServer.mjs')
  return path.join('nodedc-source', 'server', 'aiWorkspace', 'ndcAgentMcpServer.mjs')
}

function resolveWorkspacePath(value) {
  const text = String(value || '').trim()
  if (!text) return CODEX_CWD
  return path.isAbsolute(text) ? text : path.resolve(CODEX_CWD, text)
}

async function resolveExistingDirectory(value, fallback = CODEX_CWD) {
  const fallbackPath = resolveWorkspacePath(fallback)
  const candidate = resolveWorkspacePath(value || fallback)
  try {
    const stat = await fs.stat(candidate)
    if (stat.isDirectory()) return candidate
  } catch {}
  return fallbackPath
}

function workspaceTitle(value) {
  const normalized = String(value || '').replace(/[\\/]+$/, '')
  return path.basename(normalized) || normalized || 'Workspace'
}

function workspaceId(value) {
  return String(value || '').trim()
}

function parseHubUrls(raw, primary = '') {
  const values = []
  const push = (value) => {
    const text = String(value || '').trim()
    if (!text) return
    try {
      const url = new URL(text)
      if (url.protocol !== 'ws:' && url.protocol !== 'wss:') return
      url.username = ''
      url.password = ''
      const clean = url.toString().replace(/\/+$/, '')
      if (!values.includes(clean)) values.push(clean)
    } catch {}
  }
  push(primary)
  const text = String(raw || '').trim()
  if (!text) return values
  try {
    const parsed = JSON.parse(text)
    if (Array.isArray(parsed)) {
      parsed.forEach(push)
      return values
    }
  } catch {}
  text.split(/[\n,;]+/).forEach(push)
  return values
}

function parseBridgeCapabilities(raw) {
  const values = Array.isArray(raw)
    ? raw
    : String(raw || '').split(/[\n,;]+/)
  const out = []
  for (const value of values) {
    const text = String(value || '').trim().replace(/[^a-zA-Z0-9._:-]+/g, '-').replace(/^-+|-+$/g, '')
    if (!text || out.includes(text)) continue
    out.push(text.slice(0, 80))
  }
  return out
}

function nowMs() {
  return Date.now()
}

function elapsedLabel(startedAt) {
  const elapsed = Math.max(0, nowMs() - Number(startedAt || nowMs()))
  if (elapsed < 1000) return `${elapsed}ms`
  return `${(elapsed / 1000).toFixed(1)}s`
}

function elapsedFromIso(value) {
  const startedAt = Date.parse(String(value || ''))
  if (!Number.isFinite(startedAt)) return ''
  return elapsedLabel(startedAt)
}

function cleanRunKey(value, limit = 160) {
  return String(value || '').trim().slice(0, limit)
}

function terminateProcessTree(child, signal = 'SIGTERM') {
  if (!child) return
  if (process.platform === 'win32' && child.pid) {
    try {
      const args = ['/PID', String(child.pid), '/T']
      if (signal === 'SIGKILL') args.push('/F')
      const killer = spawn('taskkill', args, {
        windowsHide: true,
        stdio: 'ignore',
        shell: false,
      })
      killer.unref?.()
      return
    } catch {}
  }
  try {
    child.kill(signal)
  } catch {}
}

function registerActiveCodexRun(control = {}, child, onEvent = () => {}) {
  const requestId = cleanRunKey(control.requestId, 120)
  const threadId = cleanRunKey(control.threadId, 160)
  if (!requestId && !threadId) return null
  const run = {
    requestId,
    threadId,
    child,
    onEvent,
    closed: false,
    stopRequested: false,
    stopTimer: null,
    stop() {
      if (run.closed) return false
      if (run.stopRequested) return true
      run.stopRequested = true
      try {
        run.onEvent({ kind: 'stopped', message: 'Codex stop requested.' })
      } catch {}
      terminateProcessTree(run.child, 'SIGTERM')
      run.stopTimer = setTimeout(() => {
        if (run.closed) return
        terminateProcessTree(run.child, 'SIGKILL')
      }, CODEX_STOP_GRACE_MS)
      return true
    },
    unregister() {
      run.closed = true
      if (run.stopTimer) clearTimeout(run.stopTimer)
      if (requestId && activeCodexRunsByRequest.get(requestId) === run) activeCodexRunsByRequest.delete(requestId)
      if (threadId && activeCodexRunsByThread.get(threadId) === run) activeCodexRunsByThread.delete(threadId)
    },
  }
  if (requestId) activeCodexRunsByRequest.set(requestId, run)
  if (threadId) activeCodexRunsByThread.set(threadId, run)
  return run
}

function stopActiveCodexRun(payload = {}) {
  const requestId = cleanRunKey(payload?.requestId, 120)
  const threadId = cleanRunKey(payload?.threadId, 160)
  const run = (requestId ? activeCodexRunsByRequest.get(requestId) : null)
    || (threadId ? activeCodexRunsByThread.get(threadId) : null)
    || null
  if (!run) {
    return {
      ok: true,
      stopped: false,
      reason: 'active_codex_run_not_found',
      requestId,
      threadId,
    }
  }
  const stopped = run.stop()
  return {
    ok: true,
    stopped,
    requestId: run.requestId,
    threadId: run.threadId,
  }
}

function parseArgs(raw) {
  const text = String(raw || '').trim()
  if (!text) return []
  try {
    const parsed = JSON.parse(text)
    if (Array.isArray(parsed)) return parsed.map((item) => String(item))
  } catch {}
  return text.split(/\s+/).filter(Boolean)
}

function sendJson(res, status, payload) {
  const body = JSON.stringify(payload)
  res.writeHead(status, {
    'Content-Type': 'application/json; charset=utf-8',
    'Cache-Control': 'no-store',
    'Access-Control-Allow-Origin': process.env.AI_BRIDGE_ALLOW_ORIGIN || '*',
    'Access-Control-Allow-Headers': 'Content-Type',
    'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
  })
  res.end(body)
}

function readBody(req) {
  return new Promise((resolve, reject) => {
    let size = 0
    let body = ''
    req.setEncoding('utf8')
    req.on('data', (chunk) => {
      size += Buffer.byteLength(chunk)
      if (size > MAX_BODY_BYTES) {
        reject(new Error('body_too_large'))
        req.destroy()
        return
      }
      body += chunk
    })
    req.on('end', () => {
      if (!body.trim()) return resolve({})
      try {
        resolve(JSON.parse(body))
      } catch {
        reject(new Error('json_invalid'))
      }
    })
    req.on('error', reject)
  })
}

async function readConversations() {
  let raw = ''
  try {
    raw = await fs.readFile(SESSION_INDEX, 'utf8')
  } catch {
    return []
  }
  const conversations = raw
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => {
      try {
        const item = JSON.parse(line)
        const id = String(item.id || '').trim()
        if (!id) return null
        return {
          id,
          title: String(item.thread_name || item.title || id).trim(),
          updatedAt: item.updated_at || item.updatedAt || null,
        }
      } catch {
        return null
      }
    })
    .filter(Boolean)
    .sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')))
    .slice(0, MAX_CONVERSATIONS)

  const sessionFiles = await listSessionFiles(SESSIONS_DIR)
  await Promise.all(conversations.map(async (conversation) => {
    const sessionFile = await findSessionFile(sessionFiles, conversation)
    if (!sessionFile) {
      conversation.messages = []
      return
    }
    conversation.sessionFile = sessionFile
    const workspaceMeta = await readSessionWorkspaceMeta(sessionFile)
    if (workspaceMeta?.cwd) {
      conversation.workspacePath = workspaceMeta.cwd
      conversation.workspaceTitle = workspaceTitle(workspaceMeta.cwd)
      conversation.workspaceUpdatedAt = workspaceMeta.updatedAt || conversation.updatedAt || null
    }
    const sessionState = await readSessionState(sessionFile)
    conversation.messages = sessionState.messages
    conversation.status = sessionState.status
    conversation.running = sessionState.running
    conversation.sessionUpdatedAt = sessionState.updatedAt
  }))

  return conversations
}

function buildPrompt(payload) {
  const context = payload?.context && typeof payload.context === 'object' ? payload.context : {}
  const runProfile = payload?.runProfile && typeof payload.runProfile === 'object' ? payload.runProfile : {}
  const toolProfile = runProfile.toolProfile && typeof runProfile.toolProfile === 'object' ? runProfile.toolProfile : {}
  const assistantActions = toolProfile.assistantActions && typeof toolProfile.assistantActions === 'object'
    ? toolProfile.assistantActions
    : {}
  const assistantActionIds = Array.isArray(assistantActions.actionIds)
    ? assistantActions.actionIds.map((item) => String(item || '').trim()).filter(Boolean)
    : []
  const runProfilePolicyPrompt = String(runProfile.policyPrompt || '').trim()
  const userMessage = String(payload?.message || '').trim()
  const history = payload?.resume ? [] : normalizeHistory(payload?.history || [])
  const isNdcAgentCore = String(context.modeId || '').trim() === 'ndc-agent-core'
  const isOpsMode = String(context.modeId || '').trim() === 'ops'
  const guardInstruction = String(context.guardInstruction || payload?.guardInstruction || '').trim()
  const missingContext = Array.isArray(context.missingContext)
    ? context.missingContext.map((item) => String(item || '').trim()).filter(Boolean)
    : []
  const contextReady = context.contextReady !== false && missingContext.length === 0
  const engineContext = context.engineContext && typeof context.engineContext === 'object'
    ? context.engineContext
    : context.contexts?.engine && typeof context.contexts.engine === 'object'
      ? context.contexts.engine
      : {}
  const opsContext = context.opsContext && typeof context.opsContext === 'object'
    ? context.opsContext
    : context.contexts?.ops && typeof context.contexts.ops === 'object'
      ? context.contexts.ops
      : {}
  const ndcAgentMcpApiBaseUrl = deriveNdcAgentMcpApiBaseUrl(context)
  const lines = [
    'You are connected to NODE.DC AI Workspace through AI Workspace Bridge.',
    '',
    'Context:',
    `- active surface: ${context.surface || 'unknown'}`,
    `- source surface: ${context.sourceSurface || context.surface || 'unknown'}`,
    `- mode: ${context.modeTitle || context.modeId || 'unknown'}`,
    `- workflow: ${context.workflowTitle || context.workflowId || 'unknown'}`,
    `- agent node: ${context.agentNodeTitle || context.agentNodeId || 'unknown'}`,
    `- role: ${context.roleTitle || context.roleId || 'unknown'}`,
    `- workspace: ${context.workspacePath || payload?.workspacePath || CODEX_CWD}`,
    `- context ready: ${contextReady ? 'yes' : 'no'}`,
    ...missingContext.length ? [`- missing context: ${missingContext.join(', ')}`] : [],
    `- access mode: ${context.accessMode || 'chat'}`,
    '',
    'Cross-platform contexts:',
    `- Engine workflow: ${engineContext.workflowTitle || engineContext.workflowId || 'not selected'}`,
    `- Engine agent node: ${engineContext.agentNodeTitle || engineContext.agentNodeId || 'not selected'}`,
    `- Engine role: ${engineContext.roleTitle || engineContext.roleId || 'not selected'}`,
    `- Ops workspace: ${opsContext.opsWorkspaceTitle || opsContext.opsWorkspaceSlug || opsContext.opsWorkspaceId || 'not selected'}`,
    `- Ops project: ${opsContext.opsProjectTitle || opsContext.opsProjectIdentifier || opsContext.opsProjectId || 'not selected'}`,
    '- the active/source surface is where the user opened the assistant; it is not a write boundary by itself',
    '- choose the write target from the selected per-app contexts and the user request; cross-app work is allowed when the relevant context is selected',
    '- do not refuse Engine work only because the source surface is Ops, and do not refuse Ops work only because the mode is NDC Agent Core',
    '- use the user language for the final answer and public progress/reasoning summaries; if the user writes in Russian, write them in Russian',
    '- never mention internal n8n/N8N names, endpoints, environment variables, schemas, or runtime identifiers in public answers; call the product and workflow runtime NDC',
    '- while working, provide concise public progress/reasoning summaries when the Codex runtime exposes them',
    '- public progress/reasoning summaries should say what you are checking, reading, comparing, or deciding',
    '- keep public progress/reasoning summaries user-facing; do not include raw command lines, JSON, hidden chain-of-thought, or bridge transport details',
    ...isNdcAgentCore ? [
      '',
      'NDC Agent Core mode contract:',
      ...!contextReady ? [
        '- The NDC Agent Core context is incomplete. Chat, explanations, reading, and Ops work are still allowed when the required target for that work is selected.',
        '- Do not claim that an Engine agent node is selected when it is missing.',
        '- Do not create, edit, patch, deploy, or write Engine workflow graph changes until the missing Engine agent node is selected.',
        '- If the user asks for Engine development or graph edits, explain briefly that selecting an Engine agent node is required for that specific write action.',
      ] : [
        '- Treat the selected Engine agent node as the only writable Engine workflow target.',
        '- Do not write directly to the NDC core runtime and do not modify deployed runtime workflows by hand.',
        '- The Engine second-level workflow file is the source of truth for edits.',
        `- Engine API base for this target: ${ndcAgentMcpApiBaseUrl}`,
        `- Selected workflowId: ${context.workflowId || 'unknown'}`,
        `- Selected agentNodeId: ${context.agentNodeId || 'unknown'}`,
        '- Read the second-level graph with GET /subworkflow?workflowId=<workflowId>&nodeId=<agentNodeId>.',
        '- Apply graph edits with POST /subworkflow/patch using JSON: { workflowId, nodeId, intent, operations }.',
        '- Prefer the MCP tools exposed by the ndc_agent_core server: ndc_get_context, ndc_get_subworkflow, ndc_search_nodes, ndc_get_node_definition, ndc_apply_subworkflow_patch, ndc_validate_subworkflow, assistant_action_call.',
        '- Do not use direct NDC core runtime MCP servers, local workflow files, or shell probes for graph edits in this mode.',
        '- Use only ndc_agent_core MCP tools for the selected second-level workflow and stop after a cancellation, fetch, or policy error with the exact tool name and error.',
        '- In public answers, use only NDC labels: NDC workflow, NDC node, NDC node type, NDC nodebase, and NDC Agent Core.',
        '- Do not expose internal vendor names, raw implementation field names, backend schema routes, or internal node class names in public answers.',
        '- Supported operations: addNode, upsertNode, updateNode, removeNode, moveNode, addConnection, removeConnection.',
        '- Prefer small patches and preserve existing node ids, node names, positions, parameters, and connections unless the user asks to change them.',
        '- After saving through the Engine API, the open Engine canvas refreshes from dc.subworkflow.json; never edit local files or use shell fallbacks for graph changes in this mode.',
      ],
      '',
      'Assistant action routing contract:',
      `- Assistant action ids available in this run: ${assistantActionIds.length ? assistantActionIds.join(', ') : 'none'}.`,
      '- For Launcher/HUB/admin/access/users/invites/roles/service grants requests, use only the ndc_agent_core MCP tool assistant_action_call.',
      '- Do not use codex_apps readonly connectors, read_handoff, local files, shell search, logs, or workspace scans for Launcher/HUB live administrative data.',
      '- Use phase="execute" for read-only action calls after selecting the structured action id.',
      '- Use phase="preview" before any privileged/write action, ask for explicit confirmation, then use phase="execute" only after confirmation.',
      '- Useful read action ids when advertised: hub.access_request.list_pending, hub.invite.list_pending, hub.user.read_admin_summary.',
      '- If assistant_action_call is unavailable or the gateway returns an error, report that exact tool/error and stop; do not guess from local files.',
    ] : [],
    ...(isOpsMode || opsContext.opsWorkspaceSlug || opsContext.opsProjectId ? [
      '',
      'NODE.DC Ops mode contract:',
      '- Use Ops only inside the selected Ops workspace and project.',
      `- Selected Ops workspace slug: ${opsContext.opsWorkspaceSlug || opsContext.opsWorkspaceId || 'unknown'}`,
      `- Selected Ops project id: ${opsContext.opsProjectId || 'unknown'}`,
      `- Selected Ops project identifier: ${opsContext.opsProjectIdentifier || 'unknown'}`,
      '- Prefer the MCP tools exposed for NODE.DC Ops when creating, updating, moving, or reading tasks.',
      '- If assistant action ids include ops.card.list_recent, ops.card.create, or ops.card.add_comment, these are the canonical Ops card actions for this run.',
      '- Use assistant_action_call phase="execute" for Ops card reads after selecting ops.card.list_recent.',
      '- Use assistant_action_call phase="preview" before Ops card create/comment writes, ask for explicit confirmation, then call phase="execute" with the returned confirmation token.',
      '- Before writing Ops tasks, use the Ops MCP project/context tools when available and include a unique idempotency key for write tools.',
      '- If direct Ops MCP tools are unavailable but the Ops assistant action ids are advertised, do not refuse; route through assistant_action_call.',
      '- If neither direct Ops MCP tools nor Ops assistant action ids are available, say that the Ops context is selected but live Ops tools are unavailable.',
      '- Do not claim that an Engine workflow or Engine agent node is required for Ops task writes.',
      '- If the current request is about Ops tasks and Ops workspace/project are selected, treat that as the writable Ops target.',
      '- In public answers, use NODE.DC/Ops labels and never expose internal vendor names.',
    ] : []),
    ...guardInstruction ? [
      '',
      'System context guard:',
      guardInstruction,
    ] : [],
    ...runProfilePolicyPrompt ? [
      '',
      runProfilePolicyPrompt,
    ] : [],
    '',
    ...history.length ? [
      'Recent conversation from AI Workspace:',
      ...history.map((message) => `${message.role}: ${message.text}`),
      '',
    ] : [],
    'User message:',
    userMessage,
  ]
  return lines.join('\n')
}

async function listSessionFiles(rootDir) {
  const files = []
  async function walk(dir) {
    if (files.length >= MAX_SESSION_FILES) return
    let entries = []
    try {
      entries = await fs.readdir(dir, { withFileTypes: true })
    } catch {
      return
    }
    entries.sort((a, b) => b.name.localeCompare(a.name))
    for (const entry of entries) {
      if (files.length >= MAX_SESSION_FILES) return
      const itemPath = path.join(dir, entry.name)
      if (entry.isDirectory()) {
        await walk(itemPath)
      } else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
        files.push(itemPath)
      }
    }
  }
  await walk(rootDir)
  return files
}

async function findSessionFile(files, conversation) {
  const id = String(conversation?.id || '').trim()
  if (!id) return ''
  const updatedAt = String(conversation?.updatedAt || '').trim()
  const dateMatch = updatedAt.match(/^(\d{4})-(\d{2})-(\d{2})/)
  if (dateMatch) {
    const dateDir = path.join(SESSIONS_DIR, dateMatch[1], dateMatch[2], dateMatch[3])
    try {
      const entries = await fs.readdir(dateDir)
      const match = entries.find((name) => name.endsWith('.jsonl') && name.includes(id))
      if (match) return path.join(dateDir, match)
    } catch {}
  }
  return files.find((file) => path.basename(file).includes(id)) || ''
}

async function readTextFileOrTail(filePath) {
  const stat = await fs.stat(filePath)
  if (stat.size <= MAX_SESSION_FILE_BYTES) return fs.readFile(filePath, 'utf8')
  const handle = await fs.open(filePath, 'r')
  try {
    const size = Math.min(stat.size, MAX_SESSION_FILE_BYTES)
    const buffer = Buffer.alloc(size)
    await handle.read(buffer, 0, size, Math.max(0, stat.size - size))
    const text = buffer.toString('utf8')
    return text.slice(text.indexOf('\n') + 1)
  } finally {
    await handle.close()
  }
}

async function readTextFileHead(filePath, maxBytes = 256 * 1024) {
  const stat = await fs.stat(filePath)
  const size = Math.min(stat.size, maxBytes)
  if (size <= 0) return ''
  const handle = await fs.open(filePath, 'r')
  try {
    const buffer = Buffer.alloc(size)
    await handle.read(buffer, 0, size, 0)
    return buffer.toString('utf8')
  } finally {
    await handle.close()
  }
}

async function readSessionWorkspaceMeta(filePath) {
  let text = ''
  try {
    text = await readTextFileHead(filePath)
  } catch {
    return null
  }
  for (const line of text.split(/\r?\n/)) {
    if (!line.trim()) continue
    try {
      const item = JSON.parse(line)
      const payload = item?.payload || {}
      if (item?.type === 'session_meta' && payload?.cwd) {
        return {
          cwd: String(payload.cwd || '').trim(),
          updatedAt: payload.timestamp || item.timestamp || null,
        }
      }
      const contentText = readContentText(payload?.content)
      const match = String(contentText || '').match(/<cwd>([\s\S]*?)<\/cwd>/i)
      if (match?.[1]) {
        return {
          cwd: match[1].trim(),
          updatedAt: item.timestamp || null,
        }
      }
    } catch {}
  }
  return null
}

function timestampMs(value) {
  const parsed = Date.parse(String(value || ''))
  return Number.isFinite(parsed) ? parsed : 0
}

function messageTimestampMs(message) {
  return timestampMs(message?.createdAt)
}

function itemTimestampMs(item) {
  return timestampMs(item?.timestamp || item?.createdAt || item?.created_at || item?.payload?.createdAt || item?.payload?.created_at)
}

function isFinalAssistantMessage(message) {
  if (message?.role !== 'assistant') return false
  const phase = String(message.phase || '').trim().toLowerCase()
  return phase ? FINAL_MESSAGE_PHASES.has(phase) : true
}

async function readSessionState(filePath) {
  let text = ''
  let updatedAt = null
  try {
    const stat = await fs.stat(filePath)
    updatedAt = stat.mtime?.toISOString?.() || null
    text = await readTextFileOrTail(filePath)
  } catch {
    return { messages: [], status: 'unknown', running: false, updatedAt }
  }

  const responseMessages = []
  const eventMessages = []
  let latestUserAt = 0
  let latestAssistantFinalAt = 0
  let latestActivityAt = 0
  let latestFailureAt = 0
  for (const line of text.split(/\r?\n/)) {
    if (!line.trim()) continue
    let item = null
    try {
      item = JSON.parse(line)
    } catch {
      continue
    }
    const responseMessage = extractResponseMessage(item)
    if (responseMessage) {
      responseMessages.push(responseMessage)
      const at = messageTimestampMs(responseMessage)
      latestActivityAt = Math.max(latestActivityAt, at)
      if (responseMessage.role === 'user') latestUserAt = Math.max(latestUserAt, at)
      if (isFinalAssistantMessage(responseMessage)) latestAssistantFinalAt = Math.max(latestAssistantFinalAt, at)
      continue
    }
    const eventMessage = extractEventMessage(item)
    if (eventMessage) {
      eventMessages.push(eventMessage)
      const at = messageTimestampMs(eventMessage)
      latestActivityAt = Math.max(latestActivityAt, at)
      if (eventMessage.role === 'user') latestUserAt = Math.max(latestUserAt, at)
      if (isFinalAssistantMessage(eventMessage)) latestAssistantFinalAt = Math.max(latestAssistantFinalAt, at)
    }
    const typeText = `${item?.type || ''} ${item?.payload?.type || ''}`.toLowerCase()
    if (/error|failed|interrupted|aborted/.test(typeText)) latestFailureAt = Math.max(latestFailureAt, itemTimestampMs(item))
  }

  const latestResponseAt = responseMessages.reduce((max, message) => Math.max(max, messageTimestampMs(message)), 0)
  const liveEventMessages = responseMessages.length
    ? eventMessages.filter((message) => messageTimestampMs(message) > latestResponseAt)
    : eventMessages
  const messages = dedupeMessages([...responseMessages, ...liveEventMessages]
    .sort((a, b) => messageTimestampMs(a) - messageTimestampMs(b)))
    .slice(-MAX_MESSAGES_PER_CONVERSATION)
  const failed = latestFailureAt > latestUserAt
  const rawRunning = latestUserAt > Math.max(latestAssistantFinalAt, latestFailureAt)
  const updatedAtMs = timestampMs(updatedAt)
  const staleRunning = rawRunning && Number.isFinite(updatedAtMs) && Date.now() - updatedAtMs > STALE_RUNNING_SESSION_MS
  const running = rawRunning && !staleRunning
  const status = running ? 'running' : failed ? 'failed' : latestActivityAt ? 'completed' : 'unknown'
  return { messages, status, running, updatedAt }
}

async function readSessionMessages(filePath) {
  const state = await readSessionState(filePath)
  return state.messages
}

function extractResponseMessage(item) {
  const payload = item?.payload || {}
  if (item?.type !== 'response_item' || payload?.type !== 'message') return null
  const role = normalizeRole(payload.role)
  if (!role) return null
  const text = normalizeDisplayMessageText(role, readContentText(payload.content ?? payload.text ?? payload.message))
  if (!text || shouldSkipConversationText(text)) return null
  return {
    id: String(payload.id || item.id || `${role}-${Date.now()}-${Math.random()}`),
    role,
    text,
    phase: String(payload.phase || item.phase || '').trim(),
    createdAt: payload.created_at || payload.createdAt || item.created_at || item.createdAt || item.timestamp || null,
  }
}

function extractEventMessage(item) {
  const payload = item?.payload || {}
  if (item?.type !== 'event_msg') return null
  const type = String(payload.type || '')
  const role = type === 'user_message' ? 'user' : type === 'agent_message' ? 'assistant' : ''
  if (!role) return null
  const text = normalizeDisplayMessageText(role, payload.message || payload.text || '')
  if (!text || shouldSkipConversationText(text)) return null
  return {
    id: String(payload.id || item.id || `${role}-${Date.now()}-${Math.random()}`),
    role,
    text,
    phase: String(payload.phase || item.phase || '').trim(),
    createdAt: payload.created_at || payload.createdAt || item.created_at || item.createdAt || item.timestamp || null,
  }
}

function readContentText(content) {
  if (typeof content === 'string') return content
  if (!Array.isArray(content)) return ''
  return content
    .map((part) => {
      if (typeof part === 'string') return part
      if (typeof part?.text === 'string') return part.text
      if (typeof part?.content === 'string') return part.content
      return ''
    })
    .filter(Boolean)
    .join('\n')
}

function normalizeRole(role) {
  const value = String(role || '').trim().toLowerCase()
  if (value === 'user') return 'user'
  if (value === 'assistant') return 'assistant'
  return ''
}

function normalizeMessageText(value) {
  const text = String(value || '').replace(/\r\n/g, '\n').trim()
  if (!text) return ''
  return text.length > MAX_MESSAGE_CHARS ? `${text.slice(0, MAX_MESSAGE_CHARS).trimEnd()}\n\n[truncated]` : text
}

function normalizeDisplayMessageText(role, value) {
  const text = normalizeMessageText(value)
  if (role !== 'user') return text
  const clean = text.replace(/^user\s*\n/i, '').trim()
  if (!clean.includes('You are connected to NODE.DC AI Workspace through AI Workspace Bridge.')) return text
  const match = clean.match(/(?:^|\n)User message:\s*\n?([\s\S]*)$/i)
  return normalizeMessageText(match?.[1] || '')
}

function shouldSkipConversationText(text) {
  return /^\s*<(environment_context|permissions instructions|app-context|collaboration_mode|apps_instructions|skills_instructions|plugins_instructions|turn_aborted)\b/i.test(text)
}

function dedupeMessages(messages) {
  const result = []
  for (const message of messages) {
    const previous = result[result.length - 1]
    if (previous && previous.role === message.role && previous.text === message.text) continue
    result.push(message)
  }
  return result
}

function normalizeHistory(history) {
  if (!Array.isArray(history)) return []
  return history
    .slice(-40)
    .map((message) => ({
      role: normalizeRole(message?.role),
      text: normalizeMessageText(message?.text || message?.content || ''),
    }))
    .filter((message) => message.role && message.text)
}

function cleanSessionId(value) {
  const text = String(value || '').trim()
  if (!text || text.startsWith('ai-') || text.includes(':')) return ''
  return /^[A-Za-z0-9._-]{8,160}$/.test(text) ? text : ''
}

function buildResumeArgs(sessionId) {
  const clean = cleanSessionId(sessionId)
  if (!clean) return CODEX_ARGS
  if (CODEX_RESUME_ARGS.length) {
    return CODEX_RESUME_ARGS.map((item) => item.replace(/\{sessionId\}/g, clean))
  }
  const stdinIndex = CODEX_ARGS.lastIndexOf('-')
  const base = stdinIndex >= 0
    ? CODEX_ARGS.filter((_, index) => index !== stdinIndex)
    : [...CODEX_ARGS]
  if (base[0] !== 'exec') return CODEX_ARGS
  return [...base, 'resume', clean, '-']
}

function isPlainObject(value) {
  return Boolean(value && typeof value === 'object' && !Array.isArray(value))
}

function isNdcAgentCorePayload(payload) {
  const context = isPlainObject(payload?.context) ? payload.context : {}
  return String(context.modeId || '').trim() === 'ndc-agent-core'
}

function insertBeforePromptStdin(args, extras) {
  const base = Array.isArray(args) ? [...args] : []
  const stdinIndex = base.lastIndexOf('-')
  if (stdinIndex < 0) return [...base, ...extras]
  return [...base.slice(0, stdinIndex), ...extras, ...base.slice(stdinIndex)]
}

function buildNdcAgentMcpContext(payload, cwd) {
  const context = payload?.context && typeof payload.context === 'object' ? payload.context : {}
  const runProfile = payload?.runProfile && typeof payload.runProfile === 'object' ? payload.runProfile : {}
  const toolProfile = runProfile.toolProfile && typeof runProfile.toolProfile === 'object' ? runProfile.toolProfile : {}
  const assistantActions = toolProfile.assistantActions && typeof toolProfile.assistantActions === 'object'
    ? toolProfile.assistantActions
    : {}
  const apiBaseUrl = deriveNdcAgentMcpApiBaseUrl(context)
  return {
    workflowId: String(context.workflowId || '').trim(),
    workflowTitle: String(context.workflowTitle || '').trim(),
    agentNodeId: String(context.agentNodeId || '').trim(),
    agentNodeTitle: String(context.agentNodeTitle || '').trim(),
    roleId: String(context.roleId || '').trim(),
    roleTitle: String(context.roleTitle || '').trim(),
    ndcAgentMcpApiBaseUrl: apiBaseUrl.replace(/\/+$/, ''),
    schemaVersion: 'v2.3.2',
    n8nMcpRefPath: NDC_AGENT_MCP_REF_PATH,
    workspacePath: cwd,
    assistantActions: {
      actionIds: Array.isArray(assistantActions.actionIds)
        ? assistantActions.actionIds.map((item) => String(item || '').trim()).filter(Boolean)
        : [],
    },
    assistantActionOwner: runProfile.owner && typeof runProfile.owner === 'object' ? runProfile.owner : {},
    assistantActionGatewayUrl: String(assistantActions.gatewayUrl || '').trim(),
    assistantActionGatewayToken: String(assistantActions.gatewayToken || '').trim(),
  }
}

function deriveNdcAgentMcpApiBaseUrl(context) {
  const explicit = cleanApiBaseUrl(context?.ndcAgentMcpApiBaseUrl || context?.engineApiBaseUrl || '')
  const hubDerived = deriveNdcAgentMcpApiBaseUrlFromHub()
  if (!explicit) return hubDerived || DEFAULT_NDC_AGENT_MCP_API_BASE
  if (!isHttpUrl(explicit)) return hubDerived || DEFAULT_NDC_AGENT_MCP_API_BASE
  if (hubDerived && isProtectedNodedcEngineUrl(explicit)) return hubDerived
  if (isLoopbackUrl(explicit) && hubDerived) return hubDerived
  if (isPrivateNetworkUrl(explicit) && hubDerived) return hubDerived
  if (!explicit.endsWith('/api/ndc-agent-mcp')) return `${explicit.replace(/\/+$/, '')}/api/ndc-agent-mcp`
  return explicit
}

function cleanApiBaseUrl(value) {
  const text = String(value || '').trim().replace(/\/+$/, '')
  return text || ''
}

function isLoopbackUrl(value) {
  try {
    const url = new URL(value)
    const host = url.hostname.toLowerCase()
    return host === 'localhost' || host === '0.0.0.0' || host === '::1' || host.startsWith('127.')
  } catch {
    return false
  }
}

function isPrivateNetworkUrl(value) {
  try {
    const url = new URL(value)
    const host = url.hostname.toLowerCase()
    if (host === 'localhost' || host === '0.0.0.0' || host === '::1' || host.startsWith('127.')) return true
    if (host.startsWith('192.168.')) return true
    if (host.startsWith('10.')) return true
    if (host.startsWith('169.254.')) return true
    const match = host.match(/^172\.(\d+)\./)
    return Boolean(match && Number(match[1]) >= 16 && Number(match[1]) <= 31)
  } catch {
    return false
  }
}

function isHttpUrl(value) {
  try {
    const url = new URL(value)
    return url.protocol === 'http:' || url.protocol === 'https:'
  } catch {
    return false
  }
}

function deriveNdcAgentMcpApiBaseUrlFromHub() {
  return deriveNdcAgentMcpBaseFromHubUrl(hubState.url)
    || HUB_URLS.map(deriveNdcAgentMcpBaseFromHubUrl).find(Boolean)
    || ''
}

function deriveNdcAgentMcpBaseFromHubUrl(value) {
  try {
    const url = new URL(String(value || '').trim())
    const hostname = url.hostname.toLowerCase()
    if (!hostname) return ''
    if (url.protocol === 'wss:') url.protocol = 'https:'
    else if (url.protocol === 'ws:') url.protocol = 'http:'
    else if (url.protocol !== 'http:' && url.protocol !== 'https:') return ''
    url.pathname = ''
    url.search = ''
    url.hash = ''
    const origin = url.toString().replace(/\/+$/, '')
    if (hostname === 'ai-hub.nodedc.ru') {
      const pairingCode = cleanHubPairingCode(PAIRING_CODE)
      return pairingCode ? `${origin}/api/ai-workspace/hub/v1/ndc-agent-mcp/${pairingCode}` : ''
    }
    return `${origin}/api/ndc-agent-mcp`
  } catch {
    return ''
  }
}

function cleanHubPairingCode(value) {
  return String(value || '').trim().toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 32)
}

function isProtectedNodedcEngineUrl(value) {
  try {
    const url = new URL(String(value || '').trim())
    return url.hostname.toLowerCase() === 'engine.nodedc.ru'
  } catch {
    return false
  }
}

function tomlString(value) {
  return JSON.stringify(String(value || ''))
}

function tomlKey(value) {
  const key = String(value || '').trim()
  if (/^[A-Za-z0-9_-]+$/.test(key)) return key
  return JSON.stringify(key)
}

function cleanMcpServerName(value) {
  const text = String(value || '').trim().replace(/[^A-Za-z0-9_-]+/g, '_').replace(/^_+|_+$/g, '')
  return text.slice(0, 80)
}

function cleanRunDirectoryName(value) {
  const text = String(value || '').trim().replace(/[^A-Za-z0-9._-]+/g, '_').replace(/^_+|_+$/g, '')
  return (text || `run-${Date.now()}`).slice(0, 120)
}

function positiveInteger(value, fallback, min = 1, max = 600) {
  const parsed = Number(value)
  if (!Number.isFinite(parsed)) return fallback
  return Math.max(min, Math.min(max, Math.trunc(parsed)))
}

function runProfileFromPayload(payload) {
  return isPlainObject(payload?.runProfile) ? payload.runProfile : {}
}

function runProfileMcpServers(payload) {
  const runProfile = runProfileFromPayload(payload)
  const toolProfile = isPlainObject(runProfile.toolProfile) ? runProfile.toolProfile : {}
  const rawServers = Array.isArray(toolProfile.mcpServers) ? toolProfile.mcpServers : []
  const byName = new Map()
  for (const raw of rawServers) {
    const server = sanitizeRunMcpServer(raw)
    if (!server) continue
    byName.set(server.serverName, server)
  }
  return Array.from(byName.values())
}

function sanitizeRunMcpServer(raw) {
  if (!isPlainObject(raw) || raw.enabled === false) return null
  const serverName = cleanMcpServerName(raw.serverName || raw.server_name || raw.name)
  const url = String(raw.url || '').trim()
  if (!serverName || !isHttpUrl(url)) return null
  return {
    serverName,
    url,
    required: raw.required === true,
    startupTimeoutSec: positiveInteger(raw.startupTimeoutSec || raw.startup_timeout_sec, 20, 1, 120),
    toolTimeoutSec: positiveInteger(raw.toolTimeoutSec || raw.tool_timeout_sec, 60, 1, 600),
    httpHeaders: sanitizeRunMcpHeaders(raw.httpHeaders || raw.http_headers || raw.headers),
  }
}

function sanitizeRunMcpHeaders(value) {
  if (!isPlainObject(value)) return {}
  const out = {}
  for (const [key, item] of Object.entries(value)) {
    const headerName = String(key || '').trim()
    const headerValue = String(item ?? '').trim()
    if (!headerName || !headerValue) continue
    if (/[\r\n\0]/.test(headerName) || /[\r\n\0]/.test(headerValue)) continue
    out[headerName] = headerValue
  }
  return out
}

function runtimeCodexConfig() {
  return [
    'approval_policy = "never"',
    'sandbox_mode = "danger-full-access"',
  ].join('\n')
}

function dynamicMcpServerConfig(server) {
  const lines = [
    `[mcp_servers.${tomlKey(server.serverName)}]`,
    `url = ${tomlString(server.url)}`,
    'enabled = true',
    `required = ${server.required ? 'true' : 'false'}`,
    `startup_timeout_sec = ${positiveInteger(server.startupTimeoutSec, 20, 1, 120)}`,
    `tool_timeout_sec = ${positiveInteger(server.toolTimeoutSec, 60, 1, 600)}`,
  ]
  const headers = isPlainObject(server.httpHeaders) ? server.httpHeaders : {}
  const headerEntries = Object.entries(headers)
  if (headerEntries.length) {
    lines.push('', `[mcp_servers.${tomlKey(server.serverName)}.http_headers]`)
    for (const [key, value] of headerEntries) {
      lines.push(`${tomlKey(key)} = ${tomlString(value)}`)
    }
  }
  return lines.join('\n')
}

function dynamicMcpConfig(servers) {
  return (Array.isArray(servers) ? servers : [])
    .map(dynamicMcpServerConfig)
    .filter(Boolean)
    .join('\n\n')
}

function ndcAgentMcpServerConfig(mcpContext, cwd) {
  const ndcConfig = [
    '[mcp_servers.ndc_agent_core]',
    `command = ${tomlString('node')}`,
    `args = [${tomlString(NDC_AGENT_MCP_SERVER)}]`,
    'startup_timeout_sec = 20',
    'tool_timeout_sec = 120',
  ].join('\n')
  return `${ndcConfig}\n${ndcAgentMcpEnvConfig(mcpContext, cwd)}`
}

function dynamicCodexHomePath(payload, needsNdcAgentCore) {
  const runProfile = runProfileFromPayload(payload)
  const runId = cleanRunDirectoryName(runProfile.runId || payload?.requestId || payload?.threadId || '')
  return path.join(RUN_CODEX_HOME_ROOT, needsNdcAgentCore ? `ndc-${runId}` : runId)
}

function stripTomlSection(raw, sectionName) {
  const section = String(sectionName || '').trim()
  if (!section) return String(raw || '')
  const lines = String(raw || '').split(/\r?\n/)
  const result = []
  let skipping = false
  for (const line of lines) {
    const header = normalizedTomlHeader(line)
    if (header) {
      skipping = header === `[${section}]` || header.startsWith(`[${section}.`)
    }
    if (!skipping) result.push(line)
  }
  return result.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()
}

function stripTopLevelTomlKeys(raw, keyNames) {
  const keys = new Set((Array.isArray(keyNames) ? keyNames : []).map((key) => String(key || '').trim()).filter(Boolean))
  if (!keys.size) return String(raw || '')
  const lines = String(raw || '').split(/\r?\n/)
  const result = []
  let inTable = false
  for (const line of lines) {
    if (normalizedTomlHeader(line)) {
      inTable = true
      result.push(line)
      continue
    }
    if (!inTable) {
      const match = stripTomlComment(line).match(/^\s*([A-Za-z0-9_-]+)\s*=/)
      if (match && keys.has(match[1])) continue
    }
    result.push(line)
  }
  return result.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()
}

function extractTomlSection(raw, sectionName) {
  const section = String(sectionName || '').trim()
  if (!section) return ''
  const headerKey = `[${section}]`
  const lines = String(raw || '').split(/\r?\n/)
  const result = []
  let collecting = false
  for (const line of lines) {
    const header = normalizedTomlHeader(line)
    if (header) {
      if (header === headerKey) {
        collecting = true
        result.push(line)
        continue
      }
      if (collecting) break
    }
    if (collecting) result.push(line)
  }
  return result.join('\n').trim()
}

function normalizeMcpServerSection(section, serverName) {
  const lines = String(section || '').split(/\r?\n/)
  const result = []
  let hasEnabled = false
  let hasRequired = false
  for (const line of lines) {
    const header = normalizedTomlHeader(line)
    if (header) {
      result.push(`[mcp_servers.${serverName}]`)
      continue
    }
    const activeLine = stripTomlComment(line)
    if (/^\s*transport\s*=/.test(activeLine)) continue
    if (/^\s*required\s*=/.test(activeLine)) {
      result.push('required = false')
      hasRequired = true
      continue
    }
    if (/^\s*enabled\s*=/.test(activeLine)) {
      result.push('enabled = true')
      hasEnabled = true
      continue
    }
    result.push(line)
  }
  if (!hasEnabled) result.push('enabled = true')
  if (!hasRequired) result.push('required = false')
  return result.join('\n').trim()
}

function normalizeMcpHeadersSection(section, serverName) {
  if (!section) return ''
  const lines = String(section || '').split(/\r?\n/)
  const result = []
  for (const line of lines) {
    const header = normalizedTomlHeader(line)
    if (header) {
      result.push(`[mcp_servers.${serverName}.http_headers]`)
      continue
    }
    result.push(line)
  }
  return result.join('\n').trim()
}

function extractOptionalOpsMcpConfig(raw) {
  const serverNames = ['nodedc-ops-agent', 'nodedc_tasker']
  const configs = []
  for (const serverName of serverNames) {
    const baseSection = extractTomlSection(raw, `mcp_servers.${serverName}`)
    if (!baseSection) continue
    const headersSection = extractTomlSection(raw, `mcp_servers.${serverName}.http_headers`)
      || extractTomlSection(raw, `mcp_servers.${serverName}.headers`)
    const normalized = [
      normalizeMcpServerSection(baseSection, serverName),
      normalizeMcpHeadersSection(headersSection, serverName),
    ].filter(Boolean).join('\n\n').trim()
    if (normalized) configs.push(normalized)
  }
  return configs.join('\n\n').trim()
}

async function readOptionalOpsMcpConfig() {
  try {
    const raw = await fs.readFile(path.join(CODEX_HOME, 'config.toml'), 'utf8')
    return extractOptionalOpsMcpConfig(raw)
  } catch {
    return ''
  }
}

async function copyIfExists(from, to) {
  try {
    await fs.copyFile(from, to)
    return true
  } catch (error) {
    if (error?.code === 'ENOENT') return false
    throw error
  }
}

function ndcAgentMcpEnvConfig(mcpContext, cwd) {
  return [
    '[mcp_servers.ndc_agent_core.env]',
    `NDC_AGENT_MCP_ROOT = ${tomlString(CODEX_CWD)}`,
    `NDC_AGENT_MCP_CONTEXT = ${tomlString(JSON.stringify(mcpContext || {}))}`,
    `NDC_AGENT_MCP_API_BASE_URL = ${tomlString(mcpContext?.ndcAgentMcpApiBaseUrl || '')}`,
    `AI_BRIDGE_HUB_URL = ${tomlString(hubState.url || HUB_URL)}`,
    `AI_BRIDGE_HUB_URLS = ${tomlString(HUB_URLS.join(','))}`,
    `AI_BRIDGE_PAIRING_CODE = ${tomlString(PAIRING_CODE)}`,
    `AI_BRIDGE_CODEX_CWD = ${tomlString(cwd || CODEX_CWD)}`,
  ].join('\n')
}

async function prepareRunCodexHome({ payload = {}, cwd = CODEX_CWD, mcpContext = null, dynamicMcpServers = [] } = {}) {
  const needsNdcAgentCore = isPlainObject(mcpContext)
  const hasDynamicMcp = Array.isArray(dynamicMcpServers) && dynamicMcpServers.length > 0
  const codexHome = hasDynamicMcp
    ? dynamicCodexHomePath(payload, needsNdcAgentCore)
    : NDC_AGENT_CODEX_HOME
  await fs.mkdir(codexHome, { recursive: true })
  await copyIfExists(path.join(CODEX_HOME, 'auth.json'), path.join(codexHome, 'auth.json'))
  const legacyOpsMcpConfig = ''
  const config = [
    runtimeCodexConfig(),
    needsNdcAgentCore ? ndcAgentMcpServerConfig(mcpContext, cwd) : '',
    hasDynamicMcp ? dynamicMcpConfig(dynamicMcpServers) : legacyOpsMcpConfig,
  ].filter(Boolean).join('\n\n')
  await fs.writeFile(path.join(codexHome, 'config.toml'), `${config}\n`, 'utf8')
  return codexHome
}

async function codexInvocationForPayload(baseArgs, payload, cwd) {
  const dynamicMcpServers = runProfileMcpServers(payload)
  const needsNdcAgentCore = isNdcAgentCorePayload(payload)
  if (!needsNdcAgentCore && !dynamicMcpServers.length) return { args: baseArgs, env: {}, dynamicMcpServerNames: [] }
  const mcpContext = needsNdcAgentCore ? buildNdcAgentMcpContext(payload, cwd) : null
  const codexHome = await prepareRunCodexHome({ payload, cwd, mcpContext, dynamicMcpServers })
  return {
    args: baseArgs,
    env: {
      CODEX_HOME: codexHome,
      ...(needsNdcAgentCore ? {
        NDC_AGENT_MCP_ROOT: CODEX_CWD,
        NDC_AGENT_MCP_CONTEXT: JSON.stringify(mcpContext),
        NDC_AGENT_MCP_API_BASE_URL: mcpContext.ndcAgentMcpApiBaseUrl,
        NDC_AGENT_MCP_FETCH_TIMEOUT_MS: process.env.NDC_AGENT_MCP_FETCH_TIMEOUT_MS || '30000',
        AI_WORKSPACE_ASSISTANT_ACTION_FETCH_TIMEOUT_MS: process.env.AI_WORKSPACE_ASSISTANT_ACTION_FETCH_TIMEOUT_MS || '45000',
        AI_BRIDGE_PAIRING_CODE: PAIRING_CODE,
      } : {}),
    },
    dynamicMcpServerNames: dynamicMcpServers.map((server) => server.serverName),
    codexHome,
  }
}

function isResumeRecoverableError(error) {
  const text = String(error?.message || error || '')
  return /unknown command|unrecognized|unexpected argument|invalid subcommand|usage:|failed to refresh available models|timeout waiting for child process to exit|codex_timeout|session.*not.*found|no such session|could not find.*session|session.*does not exist|no rollout found|thread\/resume.*failed|error resuming thread/i.test(text)
}

function usesJsonOutput(args) {
  return Array.isArray(args) && args.includes('--json')
}

const CODEX_ASSISTANT_TEXT_MAX = 250000

function truncateText(value, max = 1400) {
  const text = String(value || '').trim()
  return text.length > max ? `${text.slice(0, max).trim()}...` : text
}

function markdownText(value, max = 12000) {
  return truncateText(String(value || '').replace(/\r\n/g, '\n'), max)
}

function sanitizeCodexErrorText(value, max = 1400) {
  const text = String(value || '').replace(/\r\n/g, '\n').trim()
  if (!text) return ''
  const statusMatch = text.match(/\b(?:HTTP error:\s*)?((?:401|403|429|5\d\d)\s+[A-Za-z][A-Za-z\s-]*)\b/i)
  const urlMatch = text.match(/\burl:\s*((?:https?|wss):\/\/[^\s,]+)/i)
    || text.match(/\b((?:https?|wss):\/\/chatgpt\.com\/[^\s,]+)/i)
  const rayMatch = text.match(/\bcf-ray:\s*([A-Za-z0-9-]+)/i)
    || text.match(/\bRay ID:?\s*([A-Za-z0-9-]+)/i)
  if (/chatgpt\.com\/backend-api\/codex|Unable to load site|cf-ray|Cloudflare|403 Forbidden/i.test(text)) {
    return [
      `Codex backend rejected request${statusMatch ? `: ${statusMatch[1].trim()}` : ''}.`,
      urlMatch ? `url: ${urlMatch[1].replace(/[)>]+$/, '')}` : '',
      rayMatch ? `cf-ray: ${rayMatch[1]}` : '',
    ].filter(Boolean).join(' ')
  }
  return truncateText(text, max)
}

function eventText(event) {
  return [
    event?.message,
    event?.text,
    event?.command ? `$ ${event.command}` : '',
  ].map((item) => String(item || '').trim()).filter(Boolean).join('\n')
}

function renderMirrorSignal(label, value) {
  const text = markdownText(value, 1800)
  return text ? `- ${label}: ${text.replace(/\n/g, '\n  ')}` : ''
}

function renderActiveMirrorMarkdown(state) {
  const activity = state.events.length
    ? state.events.map((event) => {
      const text = markdownText(event.text, 2200)
      const inline = text.includes('\n') ? `\n\n${text}\n` : ` ${text}`
      return `- ${event.at} ${event.kind}:${inline}`
    }).join('\n')
    : '- waiting for Codex events'
  const signal = [
    renderMirrorSignal('Reasoning', state.currentReasoning),
    renderMirrorSignal('Command', state.currentCommand),
    renderMirrorSignal('Files', state.currentFiles),
    renderMirrorSignal('Tool', state.currentTool),
    renderMirrorSignal('Todo', state.currentTodo),
  ].filter(Boolean).join('\n') || '- waiting for first Codex signal'
  return [
    '# NDC AI Workspace Active Run',
    '',
    `Status: ${state.status}`,
    `Machine: ${state.machineName}`,
    `Workspace: ${state.cwd}`,
    `Mode: ${state.modeTitle || state.modeId || 'unknown'}`,
    `Thread: ${state.threadTitle || state.threadId || 'unknown'}`,
    `Request: ${state.requestId}`,
    `Started: ${state.startedAt}`,
    `Updated: ${state.updatedAt}`,
    state.finishedAt ? `Finished: ${state.finishedAt}` : '',
    '',
    '## Active Mirror',
    '',
    `Current file: ${state.currentFile}`,
    `Events file: ${state.eventsFile}`,
    '',
    '## User Message',
    '',
    markdownText(state.userMessage || ''),
    '',
    '## Current Signal',
    '',
    signal,
    '',
    '## Live Activity',
    '',
    activity,
    '',
    '## Last Assistant Output',
    '',
    markdownText(state.assistantText || 'No assistant output yet.'),
    '',
  ].filter((line) => line !== '').join('\n')
}

function openActiveMirrorInIde(filePath) {
  if (!ACTIVE_MIRROR_OPEN_IDE || !ACTIVE_MIRROR_IDE_BIN || !filePath) return
  try {
    const child = spawn(ACTIVE_MIRROR_IDE_BIN, ['-r', filePath], {
      detached: true,
      shell: process.platform === 'win32',
      stdio: 'ignore',
      windowsHide: true,
    })
    child.unref()
  } catch {}
}

function activeMirrorPathsForCwd(cwd) {
  const workspace = resolveWorkspacePath(cwd || CODEX_CWD)
  const mirrorDir = workspace === CODEX_CWD
    ? ACTIVE_MIRROR_DIR
    : path.join(workspace, '.nodedc', 'ai-workspace')
  return {
    mirrorDir,
    currentFile: path.join(mirrorDir, 'current.md'),
    eventsFile: path.join(mirrorDir, 'events.jsonl'),
  }
}

async function hasGitMarker(dir) {
  try {
    await fs.stat(path.join(dir, '.git'))
    return true
  } catch {
    return false
  }
}

async function addWorkspace(map, workspacePath, source, patch = {}) {
  const cleanPath = String(workspacePath || '').trim()
  if (!cleanPath) return
  let stat = null
  try {
    stat = await fs.stat(cleanPath)
  } catch {
    return
  }
  if (!stat.isDirectory()) return
  const id = workspaceId(cleanPath)
  const previous = map.get(id) || {}
  map.set(id, {
    id,
    path: cleanPath,
    title: patch.title || previous.title || workspaceTitle(cleanPath),
    kind: patch.kind || previous.kind || (await hasGitMarker(cleanPath) ? 'git' : 'folder'),
    source: Array.from(new Set([...(previous.source ? String(previous.source).split(', ') : []), source].filter(Boolean))).join(', '),
    lastUsedAt: patch.lastUsedAt || previous.lastUsedAt || null,
    sessionCount: Number(previous.sessionCount || 0) + Number(patch.sessionCount || 0),
  })
}

async function discoverWorkspaces() {
  const map = new Map()
  await addWorkspace(map, CODEX_CWD, 'configured', { title: `${workspaceTitle(CODEX_CWD)} (default)` })
  const conversations = await readConversations()
  for (const conversation of conversations) {
    if (!conversation.workspacePath) continue
    await addWorkspace(map, conversation.workspacePath, 'codex-session', {
      lastUsedAt: conversation.workspaceUpdatedAt || conversation.updatedAt || null,
      sessionCount: 1,
    })
  }
  const workspaces = Array.from(map.values())
    .sort((a, b) => {
      if (a.path === CODEX_CWD) return -1
      if (b.path === CODEX_CWD) return 1
      const byDate = String(b.lastUsedAt || '').localeCompare(String(a.lastUsedAt || ''))
      if (byDate) return byDate
      return String(a.title || '').localeCompare(String(b.title || ''))
    })
    .slice(0, MAX_WORKSPACES)
  return {
    ok: true,
    cwd: CODEX_CWD,
    workspaces,
  }
}

function createActiveMirror(command, payload = {}, meta = {}) {
  const context = payload?.context && typeof payload.context === 'object' ? payload.context : {}
  if (!ACTIVE_MIRROR_ENABLED || command !== 'message' || context.remoteControlMode !== 'active') return null
  const cwd = String(meta.cwd || CODEX_CWD)
  const displayMessage = String(payload.displayMessage || payload.publicUserMessage || payload.message || '')
  const { mirrorDir, currentFile, eventsFile } = activeMirrorPathsForCwd(cwd)
  const startedAt = new Date().toISOString()
  const state = {
    requestId: String(meta.requestId || payload.threadId || `local-${Date.now()}`),
    threadId: String(payload.threadId || ''),
    threadTitle: String(payload.threadTitle || ''),
    userMessage: displayMessage,
    modeId: String(context.modeId || ''),
    modeTitle: String(context.modeTitle || ''),
    machineName: MACHINE_NAME,
    cwd,
    status: 'running',
    startedAt,
    updatedAt: startedAt,
    finishedAt: '',
    assistantText: '',
    currentFile,
    eventsFile,
    currentReasoning: '',
    currentCommand: '',
    currentFiles: '',
    currentTool: '',
    currentTodo: '',
    events: [],
    ideOpened: false,
  }
  let chain = Promise.resolve()

  const write = (row) => {
    chain = chain.then(async () => {
      state.updatedAt = new Date().toISOString()
      await fs.mkdir(mirrorDir, { recursive: true })
      if (row) {
        await fs.appendFile(eventsFile, `${JSON.stringify({
          requestId: state.requestId,
          threadId: state.threadId,
          threadTitle: state.threadTitle,
          machineName: state.machineName,
          cwd: state.cwd,
          ...row,
        })}\n`, 'utf8')
      }
      await fs.writeFile(currentFile, renderActiveMirrorMarkdown(state), 'utf8')
      if (!state.ideOpened) {
        state.ideOpened = true
        openActiveMirrorInIde(currentFile)
      }
    }).catch(() => {})
    return chain
  }

  const record = (event = {}) => {
    const kind = String(event.kind || event.type || 'event')
    const text = eventText(event)
    const row = {
      at: new Date().toISOString(),
      kind,
      text: markdownText(text, 6000),
    }
    if (event.currentFile) row.currentFile = String(event.currentFile)
    if (event.eventsFile) row.eventsFile = String(event.eventsFile)
    if (kind === 'codex_reasoning') state.currentReasoning = markdownText(event.text || text, 6000)
    if (kind === 'codex_command' || kind === 'start') state.currentCommand = markdownText(event.command || text, 3000)
    if (kind === 'codex_files') state.currentFiles = markdownText(event.text || text, 3000)
    if (kind === 'codex_tool') state.currentTool = markdownText(event.text || text, 3000)
    if (kind === 'codex_todo') state.currentTodo = markdownText(event.text || text, 3000)
    if (kind === 'codex_message') state.assistantText = markdownText(event.text || text, CODEX_ASSISTANT_TEXT_MAX)
    if (kind === 'done' || kind === 'codex_turn_completed') state.status = 'completed'
    if (kind === 'timeout') state.status = 'timeout'
    if (kind === 'error' && !/reconnecting/i.test(text)) state.status = 'failed'
    state.events.push(row)
    state.events = state.events.slice(-ACTIVE_MIRROR_MAX_EVENTS)
    write(row)
  }

  return {
    currentFile,
    eventsFile,
    record,
    async complete(status, assistantText = '') {
      state.status = status
      state.finishedAt = new Date().toISOString()
      if (assistantText) state.assistantText = markdownText(assistantText, CODEX_ASSISTANT_TEXT_MAX)
      await write({
        at: state.finishedAt,
        kind: `mirror_${status}`,
        text: status,
      })
    },
  }
}

function textFromUnknown(value) {
  if (typeof value === 'string') return value
  if (Array.isArray(value)) return value.map(textFromUnknown).filter(Boolean).join('\n')
  if (value && typeof value === 'object') {
    return [
      value.text,
      value.summary,
      value.content,
      value.message,
      value.delta,
    ].map(textFromUnknown).filter(Boolean).join('\n')
  }
  return ''
}

function reasoningTextFromItem(item) {
  return textFromUnknown([
    item?.summary,
    item?.summary_text,
    item?.text,
    item?.content,
  ])
}

function reasoningSummaryEventKey(event) {
  const itemId = String(event?.item_id || event?.item?.id || event?.response_id || 'reasoning').trim() || 'reasoning'
  const summaryIndex = String(event?.summary_index ?? event?.content_index ?? event?.output_index ?? 0)
  return `${itemId}:${summaryIndex}`
}

function reasoningSummaryTextFromEvent(event) {
  return textFromUnknown([
    event?.text,
    event?.delta,
    event?.part,
    event?.summary,
    event?.summary_text,
  ])
}

function shouldEmitReasoningSummaryDelta(record, pending) {
  const text = String(pending || '').trim()
  if (!text) return false
  if (/[.!?。！？]\s*$/.test(text)) return true
  if (text.length >= REASONING_SUMMARY_DELTA_MIN_CHARS) return true
  return nowMs() - Number(record.lastEmittedAt || 0) >= REASONING_SUMMARY_DELTA_MAX_WAIT_MS && text.length >= 32
}

function updateReasoningSummaryState(state, event, mode) {
  if (!state) return null
  const key = reasoningSummaryEventKey(event)
  const record = state.get(key) || { text: '', emittedLength: 0, lastEmittedAt: 0 }
  if (mode === 'delta') {
    const delta = textFromUnknown(event?.delta)
    if (!delta) return null
    record.text += delta
    const pending = record.text.slice(record.emittedLength).trim()
    if (!shouldEmitReasoningSummaryDelta(record, pending)) {
      state.set(key, record)
      return null
    }
    record.emittedLength = record.text.length
    record.lastEmittedAt = nowMs()
    state.set(key, record)
    return pending
  }

  const text = reasoningSummaryTextFromEvent(event)
  if (text) record.text = text
  const pending = record.text.slice(record.emittedLength).trim() || (!record.emittedLength ? record.text.trim() : '')
  record.emittedLength = record.text.length
  record.lastEmittedAt = nowMs()
  state.set(key, record)
  return pending
}

function createCodexJsonEventMapper() {
  const reasoningSummaryState = new Map()
  return (event) => codexJsonEventToBridgeEvent(event, reasoningSummaryState)
}

function summarizeFileChanges(changes) {
  const list = Array.isArray(changes) ? changes : []
  if (!list.length) return 'File changes reported.'
  return list
    .slice(0, 12)
    .map((change) => `${String(change.kind || 'update')}: ${String(change.path || '')}`.trim())
    .join('\n')
}

function codexJsonEventToBridgeEvent(event, reasoningSummaryState = null) {
  const type = String(event?.type || '')
  const item = event?.item && typeof event.item === 'object' ? event.item : null
  const itemType = String(item?.type || '')
  if (type === 'response.reasoning_summary_text.delta') {
    const text = truncateText(updateReasoningSummaryState(reasoningSummaryState, event, 'delta'))
    return text ? { kind: 'codex_reasoning', message: text } : null
  }
  if (type === 'response.reasoning_summary_text.done') {
    const text = truncateText(updateReasoningSummaryState(reasoningSummaryState, event, 'done'))
    return text ? { kind: 'codex_reasoning', message: text } : null
  }
  if (type === 'response.reasoning_summary_part.done' || type === 'response.reasoning_summary_part.added') {
    const text = truncateText(reasoningSummaryTextFromEvent(event))
    return text ? { kind: 'codex_reasoning', message: text } : null
  }
  if (type === 'response.reasoning_text.delta' || type === 'response.reasoning_text.done') return null
  if (type === 'thread.started') {
    const sessionId = String(event.thread_id || '').trim()
    return { kind: 'codex_thread', sessionId, message: `Thread started: ${sessionId}`.trim() }
  }
  if (type === 'turn.started') return { kind: 'codex_turn', message: 'Turn started.' }
  if (type === 'turn.completed') {
    const usage = event.usage || {}
    const tokens = Number(usage.input_tokens || 0) + Number(usage.output_tokens || 0)
    return { kind: 'codex_turn_completed', message: tokens ? `Turn completed. Tokens: ${tokens}.` : 'Turn completed.' }
  }
  if (type === 'turn.failed') return { kind: 'error', message: event?.error?.message || 'Turn failed.' }
  if (type === 'error') return { kind: 'error', message: event.message || 'Codex stream error.' }
  if (!item) return null

  const prefix = type === 'item.started' ? 'Started' : type === 'item.updated' ? 'Updated' : 'Completed'
  if (itemType === 'reasoning') {
    const text = truncateText(reasoningTextFromItem(item))
    return text ? { kind: 'codex_reasoning', message: text } : null
  }
  if (itemType === 'agent_message') return { kind: 'codex_message', message: 'Assistant response received.', text: markdownText(item.text || '', CODEX_ASSISTANT_TEXT_MAX) }
  if (itemType === 'command_execution') {
    const status = item.status ? ` - ${item.status}` : ''
    return {
      kind: 'codex_command',
      command: item.command || '',
      message: `${prefix} command${status}: ${item.command || ''}`.trim(),
      text: truncateText(item.aggregated_output || ''),
    }
  }
  if (itemType === 'file_change') {
    return {
      kind: 'codex_files',
      message: `${prefix} file change${item.status ? ` - ${item.status}` : ''}.`,
      text: summarizeFileChanges(item.changes),
    }
  }
  if (itemType === 'mcp_tool_call') {
    return {
      kind: 'codex_tool',
      message: `${prefix} MCP ${item.server || ''}.${item.tool || ''}${item.status ? ` - ${item.status}` : ''}`.trim(),
      text: truncateText(item.error?.message || ''),
    }
  }
  if (itemType === 'todo_list') {
    const todos = Array.isArray(item.items)
      ? item.items.map((todo) => `${todo.completed ? '[x]' : '[ ]'} ${todo.text || ''}`).join('\n')
      : ''
    return { kind: 'codex_todo', message: `${prefix} todo list.`, text: truncateText(todos) }
  }
  if (itemType === 'web_search') return { kind: 'codex_tool', message: `${prefix} web search: ${item.query || ''}`.trim() }
  if (itemType === 'error') return { kind: 'error', message: item.message || 'Codex item error.' }
  return { kind: 'codex_event', message: `${prefix} ${itemType || type}.` }
}

function finalMessageFromCodexEvent(event) {
  const item = event?.item && typeof event.item === 'object' ? event.item : null
  if (!item || item.type !== 'agent_message') return ''
  return String(item.text || '').trim()
}

async function runCodexForPayload(prompt, payload, onEvent = () => {}, cwd = CODEX_CWD, meta = {}) {
  const baseArgs = payload?.resume ? buildResumeArgs(payload?.threadId) : CODEX_ARGS
  const invocation = await codexInvocationForPayload(baseArgs, payload, cwd)
  if (isNdcAgentCorePayload(payload)) {
    try {
      const context = buildNdcAgentMcpContext(payload, cwd)
      onEvent({
        kind: 'ndc_agent_mcp_context',
        message: `NDC Agent MCP API base: ${context.ndcAgentMcpApiBaseUrl}`,
      })
    } catch {}
  }
  if (Array.isArray(invocation.dynamicMcpServerNames) && invocation.dynamicMcpServerNames.length) {
    onEvent({
      kind: 'run_profile',
      message: `Dynamic run profile MCP servers: ${invocation.dynamicMcpServerNames.join(', ')}`,
    })
  }
  const control = {
    requestId: meta?.requestId,
    threadId: payload?.threadId,
  }
  if (baseArgs === CODEX_ARGS) {
    return runCodex(prompt, invocation.args, onEvent, MESSAGE_TIMEOUT_MS, cwd, control, invocation.env)
  }
  try {
    return await runCodex(prompt, invocation.args, onEvent, RESUME_TIMEOUT_MS, cwd, control, invocation.env)
  } catch (error) {
    if (!isResumeRecoverableError(error)) throw error
    onEvent({ kind: 'fallback', message: 'Codex resume failed; falling back to a new exec run.' })
    const fallback = await codexInvocationForPayload(CODEX_ARGS, payload, cwd)
    return runCodex(prompt, fallback.args, onEvent, MESSAGE_TIMEOUT_MS, cwd, control, fallback.env)
  }
}

function runCodex(prompt, args = CODEX_ARGS, onEvent = () => {}, timeoutMs = MESSAGE_TIMEOUT_MS, cwd = CODEX_CWD, control = {}, envOverrides = {}) {
  return new Promise((resolve, reject) => {
    const startedAt = nowMs()
    let firstOutputSeen = false
    const jsonOutput = usesJsonOutput(args)
    let jsonBuffer = ''
    let finalJsonMessage = ''
    let plainStdout = ''
    const mapCodexJsonEvent = createCodexJsonEventMapper()
    onEvent({
      kind: 'start',
      command: `${CODEX_BIN} ${args.join(' ')}`,
      cwd,
      message: 'Codex process started.',
    })
    const child = spawn(CODEX_BIN, args, {
      cwd,
      env: { ...process.env, ...(envOverrides || {}) },
      shell: process.platform === 'win32',
      stdio: ['pipe', 'pipe', 'pipe'],
      windowsHide: true,
    })
    const activeRun = registerActiveCodexRun(control, child, onEvent)
    let stdout = ''
    let stderr = ''
    let killed = false
    const timeout = setTimeout(() => {
      killed = true
      onEvent({ kind: 'timeout', message: 'Codex process timed out.' })
      terminateProcessTree(child, 'SIGTERM')
    }, timeoutMs)

    child.stdout.on('data', (chunk) => {
      const text = String(chunk)
      if (!firstOutputSeen) {
        firstOutputSeen = true
        onEvent({ kind: 'first_output', message: `First Codex output after ${elapsedLabel(startedAt)}.` })
      }
      stdout += text
      if (jsonOutput) {
        jsonBuffer += text
        const lines = jsonBuffer.split(/\r?\n/)
        jsonBuffer = lines.pop() || ''
        lines.forEach((line) => {
          const trimmed = line.trim()
          if (!trimmed) return
          try {
            const event = JSON.parse(trimmed)
            const finalMessage = finalMessageFromCodexEvent(event)
            if (finalMessage) finalJsonMessage = finalMessage
            const bridgeEvent = mapCodexJsonEvent(event)
            if (bridgeEvent) onEvent(bridgeEvent)
          } catch {
            plainStdout += `${line}\n`
            onEvent({ kind: 'stdout', stream: 'stdout', text: line })
          }
        })
      } else {
        plainStdout += text
        onEvent({ kind: 'stdout', stream: 'stdout', text })
      }
      if (Buffer.byteLength(stdout) > MAX_STDIO_BYTES) terminateProcessTree(child, 'SIGTERM')
    })
    child.stderr.on('data', (chunk) => {
      const text = String(chunk)
      if (!firstOutputSeen) {
        firstOutputSeen = true
        onEvent({ kind: 'first_output', message: `First Codex output after ${elapsedLabel(startedAt)}.` })
      }
      stderr += text
      onEvent({ kind: 'stderr', stream: 'stderr', text: sanitizeCodexErrorText(text) })
      if (Buffer.byteLength(stderr) > MAX_STDIO_BYTES) terminateProcessTree(child, 'SIGTERM')
    })
    child.on('error', (error) => {
      clearTimeout(timeout)
      activeRun?.unregister()
      onEvent({ kind: 'error', message: String(error?.message || error || 'codex_error') })
      reject(error)
    })
    child.on('close', (code) => {
      clearTimeout(timeout)
      activeRun?.unregister()
      if (activeRun?.stopRequested) {
        onEvent({ kind: 'stopped', message: 'Codex process stopped.' })
        return resolve('')
      }
      const stderrMessage = sanitizeCodexErrorText(stderr.trim())
      if (killed) return reject(new Error(`codex_timeout${stderrMessage ? `: ${stderrMessage.slice(0, 1000)}` : ''}`))
      if (code !== 0) {
        onEvent({ kind: 'error', message: stderrMessage || `codex_exit_${code}` })
        return reject(new Error(stderrMessage || `codex_exit_${code}`))
      }
      onEvent({ kind: 'done', message: `Codex process completed in ${elapsedLabel(startedAt)}.` })
      resolve(finalJsonMessage || plainStdout.trim() || stdout.trim() || stderr.trim())
    })
    child.stdin.end(prompt)
  })
}

function stripTomlComment(line) {
  const text = String(line || '')
  let inSingle = false
  let inDouble = false
  let escaped = false
  for (let i = 0; i < text.length; i += 1) {
    const char = text[i]
    if (escaped) {
      escaped = false
      continue
    }
    if (inDouble && char === '\\') {
      escaped = true
      continue
    }
    if (!inDouble && char === "'") {
      inSingle = !inSingle
      continue
    }
    if (!inSingle && char === '"') {
      inDouble = !inDouble
      continue
    }
    if (!inSingle && !inDouble && char === '#') {
      return text.slice(0, i)
    }
  }
  return text
}

function normalizedTomlKey(line) {
  return stripTomlComment(line).replace(/\s+/g, '').replace(/["']/g, '')
}

function normalizedTomlHeader(line) {
  const text = stripTomlComment(line).trim()
  if (!text.startsWith('[') || !text.endsWith(']')) return ''
  return normalizedTomlKey(text)
}

function hasOpsAgentInlineTransport(line) {
  const text = normalizedTomlKey(line)
  return /^nodedc-ops-agent=\{.*(?:^|[,}])?transport=/.test(text)
}

async function inspectCodexRuntime() {
  const configPath = path.join(CODEX_HOME, 'config.toml')
  const authPath = path.join(CODEX_HOME, 'auth.json')
  try {
    await fs.access(authPath)
  } catch {
    return {
      ok: false,
      error: 'codex_auth_missing',
      configPath,
      authPath,
    }
  }
  try {
    const raw = await fs.readFile(configPath, 'utf8')
    let inMcpServers = false
    let inOpsAgent = false
    for (const line of raw.split(/\r?\n/)) {
      const header = normalizedTomlHeader(line)
      if (header) {
        if (header === '[mcp_servers.nodedc-ops-agent.headers]') {
          return {
            ok: false,
            error: 'invalid_codex_mcp_headers:nodedc-ops-agent',
            configPath,
            authPath,
          }
        }
        inMcpServers = header === '[mcp_servers]'
        inOpsAgent = header === '[mcp_servers.nodedc-ops-agent]'
        continue
      }
      const activeLine = stripTomlComment(line)
      if (
        (inOpsAgent && /^\s*transport\s*=/.test(activeLine)) ||
        (inMcpServers && hasOpsAgentInlineTransport(activeLine))
      ) {
        return {
          ok: false,
          error: 'invalid_codex_mcp_transport:nodedc-ops-agent',
          configPath,
          authPath,
        }
      }
    }
  } catch (error) {
    if (error?.code !== 'ENOENT') {
      return {
        ok: false,
        error: `codex_config_read_failed:${String(error?.message || error)}`,
        configPath,
        authPath,
      }
    }
  }
  return { ok: true, error: '', configPath, authPath }
}

async function handleBridgeCommand(command, payload = {}, onEvent = () => {}, meta = {}) {
  if (command === 'health') {
    const runtime = await inspectCodexRuntime()
    return {
      ok: runtime.ok,
      error: runtime.error,
      service: 'ai-workspace-bridge',
      agentVersion: BRIDGE_AGENT_VERSION,
      protocolVersion: BRIDGE_PROTOCOL_VERSION,
      capabilities: BRIDGE_CAPABILITIES,
      host: os.hostname(),
      machineName: MACHINE_NAME,
      runtimeStatus: runtime.ok ? 'ready' : 'error',
      runtimeError: runtime.error,
      runtimeCheckedAt: new Date().toISOString(),
      codexHome: CODEX_HOME,
      codexAuthPath: runtime.authPath,
      codexBin: CODEX_BIN,
      codexArgs: CODEX_ARGS,
      dynamicRunProfile: {
        enabled: true,
        codexHomeRoot: RUN_CODEX_HOME_ROOT,
      },
      cwd: CODEX_CWD,
      hubMode: hubState.mode,
      hubConnected: hubState.connected,
      hub: { ...hubState },
      activeMirror: {
        enabled: ACTIVE_MIRROR_ENABLED,
        currentFile: ACTIVE_MIRROR_CURRENT_FILE,
        eventsFile: ACTIVE_MIRROR_EVENTS_FILE,
      },
    }
  }

  if (command === 'conversations') {
    const conversations = await readConversations()
    return { ok: true, conversations }
  }

  if (command === 'workspaces') {
    return discoverWorkspaces()
  }

  if (command === 'stop') {
    return stopActiveCodexRun(payload)
  }

  if (command === 'message') {
    const message = String(payload?.message || '').trim()
    if (!message) {
      const error = new Error('message_empty')
      error.status = 400
      throw error
    }
    const runtime = await inspectCodexRuntime()
    if (!runtime.ok) {
      const error = new Error(runtime.error || 'codex_runtime_not_ready')
      error.status = 503
      throw error
    }
    const cwd = await resolveExistingDirectory(payload?.workspacePath || payload?.context?.workspacePath || CODEX_CWD)
    const messagePayload = {
      ...payload,
      workspacePath: cwd,
      context: {
        ...(payload?.context && typeof payload.context === 'object' ? payload.context : {}),
        workspacePath: cwd,
        workspaceTitle: workspaceTitle(cwd),
      },
    }
    const mirror = createActiveMirror(command, messagePayload, { ...meta, cwd })
    const emitEvent = (event) => {
      mirror?.record(event)
      onEvent(event)
    }
    if (mirror) {
      emitEvent({
        kind: 'active_mirror',
        message: `Active mirror ready: ${mirror.currentFile}`,
        currentFile: mirror.currentFile,
        eventsFile: mirror.eventsFile,
        cwd,
      })
    }
    try {
      const reply = await runCodexForPayload(buildPrompt(messagePayload), messagePayload, emitEvent, cwd, meta)
      await mirror?.complete('completed', reply)
      return {
        ok: true,
        threadId: String(payload.threadId || ''),
        workspacePath: cwd,
        workspaceTitle: workspaceTitle(cwd),
        activeMirror: mirror ? {
          currentFile: mirror.currentFile,
          eventsFile: mirror.eventsFile,
        } : null,
        message: {
          role: 'assistant',
          text: reply,
          createdAt: new Date().toISOString(),
        },
      }
    } catch (error) {
      await mirror?.complete('failed', String(error?.message || error || 'codex_failed'))
      throw error
    }
  }

  const error = new Error('command_unknown')
  error.status = 404
  throw error
}

async function dataToText(data) {
  if (typeof data === 'string') return data
  if (Buffer.isBuffer(data)) return data.toString('utf8')
  if (data instanceof ArrayBuffer) return Buffer.from(data).toString('utf8')
  if (data?.arrayBuffer) return Buffer.from(await data.arrayBuffer()).toString('utf8')
  return String(data || '')
}

function buildHubSocketUrl(hubUrl) {
  const socketUrl = new URL(hubUrl)
  socketUrl.searchParams.set('pairingCode', PAIRING_CODE)
  socketUrl.searchParams.set('machineName', MACHINE_NAME)
  return socketUrl.toString()
}

function attachHubSocket(socket, hubUrl) {
  console.log(`[ai-workspace-bridge] hub connected: ${hubUrl}`)
  hubState.connected = true
  hubState.url = hubUrl
  hubState.lastConnectedAt = new Date().toISOString()
  hubState.lastError = ''
  void inspectCodexRuntime().then((runtime) => {
    try {
      socket.send(JSON.stringify({
        type: 'hello',
        machineName: MACHINE_NAME,
        agentVersion: BRIDGE_AGENT_VERSION,
        protocolVersion: BRIDGE_PROTOCOL_VERSION,
        capabilities: BRIDGE_CAPABILITIES,
        host: os.hostname(),
        cwd: CODEX_CWD,
        runtimeStatus: runtime.ok ? 'ready' : 'error',
        runtimeError: runtime.error,
        runtimeCheckedAt: new Date().toISOString(),
        sentAt: new Date().toISOString(),
      }))
    } catch {}
  }).catch((error) => {
    try {
      socket.send(JSON.stringify({
        type: 'hello',
        machineName: MACHINE_NAME,
        agentVersion: BRIDGE_AGENT_VERSION,
        protocolVersion: BRIDGE_PROTOCOL_VERSION,
        capabilities: BRIDGE_CAPABILITIES,
        host: os.hostname(),
        cwd: CODEX_CWD,
        runtimeStatus: 'error',
        runtimeError: `codex_runtime_check_failed:${String(error?.message || error)}`,
        runtimeCheckedAt: new Date().toISOString(),
        sentAt: new Date().toISOString(),
      }))
    } catch {}
  })

  socket.onmessage = async (event) => {
    let request = null
    try {
      request = JSON.parse(await dataToText(event.data))
    } catch {
      return
    }
    if (request?.type !== 'request') return
    const requestId = String(request.requestId || '')
    const emitEvent = (payload) => {
      try {
        socket.send(JSON.stringify({
          type: 'event',
          requestId,
          event: {
            ...(payload || {}),
            at: new Date().toISOString(),
          },
        }))
      } catch {}
    }
    try {
      const receivedAfter = elapsedFromIso(request?.payload?.client?.sentAt)
      emitEvent({
        kind: 'bridge_received',
        message: receivedAfter
          ? `Bridge received request after ${receivedAfter}.`
          : 'Bridge received request.',
      })
      const payload = await handleBridgeCommand(String(request.command || ''), request.payload || {}, emitEvent, { requestId })
      socket.send(JSON.stringify({ type: 'response', requestId, ok: true, payload }))
    } catch (error) {
      emitEvent({ kind: 'error', message: String(error?.message || error || 'bridge_agent_failed') })
      socket.send(JSON.stringify({
        type: 'response',
        requestId,
        ok: false,
        status: Number(error?.status || 502),
        error: String(error?.message || error || 'bridge_agent_failed'),
      }))
    }
  }
  socket.onerror = (event) => {
    const message = String(event?.message || 'websocket_error')
    hubState.lastError = message
    console.error(`[ai-workspace-bridge] hub error: ${message} (${hubUrl})`)
  }
  socket.onclose = () => {
    hubState.connected = false
    hubState.lastDisconnectedAt = new Date().toISOString()
    hubState.lastError = 'hub_disconnected'
    console.error(`[ai-workspace-bridge] hub disconnected; reconnecting in ${HUB_RECONNECT_MS}ms (${hubUrl})`)
    setTimeout(() => connectHub(), HUB_RECONNECT_MS)
  }
}

function connectHub() {
  if (!HUB_URLS.length || !PAIRING_CODE) return
  if (typeof WebSocket === 'undefined') {
    hubState.lastError = 'websocket_runtime_unavailable'
    console.error('[ai-workspace-bridge] WebSocket is not available in this Node.js runtime; use Node.js 22+ for hub mode.')
    return
  }

  const attempts = []
  let reconnectScheduled = false
  let selected = false
  const scheduleReconnect = (delayMs, message) => {
    if (reconnectScheduled) return
    reconnectScheduled = true
    if (message) {
      hubState.lastError = message
      console.error(message)
    }
    for (const attempt of attempts) {
      try { attempt.socket.close() } catch {}
    }
    setTimeout(() => connectHub(), delayMs)
  }

  const openTimer = setTimeout(() => {
    if (selected) return
    scheduleReconnect(1000, `[ai-workspace-bridge] hub candidates unavailable after ${HUB_CONNECT_TIMEOUT_MS}ms; retrying.`)
  }, HUB_CONNECT_TIMEOUT_MS)

  for (const hubUrl of HUB_URLS) {
    let socketUrl = ''
    try {
      socketUrl = buildHubSocketUrl(hubUrl)
    } catch (error) {
      const message = `invalid_hub_url:${String(error?.message || error)}`
      hubState.lastError = message
      console.error(`[ai-workspace-bridge] invalid hub URL: ${String(error?.message || error)} (${hubUrl})`)
      continue
    }

    const socket = new WebSocket(socketUrl)
    const attempt = { socket, hubUrl, closed: false }
    attempts.push(attempt)
    socket.onopen = () => {
      if (selected) {
        try { socket.close() } catch {}
        return
      }
      selected = true
      clearTimeout(openTimer)
      for (const other of attempts) {
        if (other.socket !== socket) {
          try { other.socket.close() } catch {}
        }
      }
      attachHubSocket(socket, hubUrl)
    }
    socket.onerror = (event) => {
      if (!selected) {
        const message = String(event?.message || 'websocket_error')
        hubState.lastError = message
        console.error(`[ai-workspace-bridge] hub error: ${message} (${hubUrl})`)
      }
    }
    socket.onclose = () => {
      attempt.closed = true
      if (selected || reconnectScheduled) return
      if (attempts.length && attempts.every((item) => item.closed)) {
        clearTimeout(openTimer)
        scheduleReconnect(1000, '[ai-workspace-bridge] all hub candidates closed before ready; retrying.')
      }
    }
  }

  if (!attempts.length) {
    clearTimeout(openTimer)
    scheduleReconnect(5000, '[ai-workspace-bridge] no valid hub candidates; retrying.')
  }
}

async function handle(req, res) {
  const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
  if (req.method === 'OPTIONS') return sendJson(res, 204, {})

  if (req.method === 'GET' && url.pathname === '/api/ai-workspace/bridge/v1/health') {
    return sendJson(res, 200, await handleBridgeCommand('health'))
  }

  if (req.method === 'GET' && url.pathname === '/api/ai-workspace/bridge/v1/conversations') {
    return sendJson(res, 200, await handleBridgeCommand('conversations'))
  }

  if (req.method === 'GET' && url.pathname === '/api/ai-workspace/bridge/v1/workspaces') {
    return sendJson(res, 200, await handleBridgeCommand('workspaces'))
  }

  if (req.method === 'POST' && url.pathname === '/api/ai-workspace/bridge/v1/message') {
    const payload = await readBody(req)
    try {
      return sendJson(res, 200, await handleBridgeCommand('message', payload, () => {}, { requestId: `local-${Date.now()}` }))
    } catch (error) {
      return sendJson(res, Number(error?.status || 502), { ok: false, error: String(error?.message || error || 'codex_failed') })
    }
  }

  if (req.method === 'POST' && url.pathname === '/api/ai-workspace/bridge/v1/stop') {
    const payload = await readBody(req)
    return sendJson(res, 200, await handleBridgeCommand('stop', payload))
  }

  return sendJson(res, 404, { ok: false, error: 'not_found' })
}

const server = http.createServer((req, res) => {
  handle(req, res).catch((error) => {
    sendJson(res, 500, { ok: false, error: String(error?.message || error || 'worker_failed') })
  })
})

server.listen(PORT, HOST, () => {
  console.log(`[ai-workspace-bridge] http://${HOST}:${PORT}`)
  console.log(`[ai-workspace-bridge] codex: ${CODEX_BIN} ${CODEX_ARGS.join(' ')}`)
  console.log(`[ai-workspace-bridge] codex home: ${CODEX_HOME}`)
  console.log(`[ai-workspace-bridge] cwd: ${CODEX_CWD}`)
  if (HUB_URLS.length && PAIRING_CODE) console.log(`[ai-workspace-bridge] hub candidates: ${HUB_URLS.join(', ')}`)
})

connectHub()
