#!/usr/bin/env node
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0; PJ2026-010401 Web工作台 - Workbench 可见性与性能探针
import { createHash } from "node:crypto";
import { spawn } from "node:child_process";
import fs from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { importPlaywright, launchChromium } from "./src/browser-launcher.mjs";
import { ensureNotRepoReportsPath, tempReportPath } from "./src/report-paths.mjs";

const scriptPath = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(scriptPath), "..");
const stateRoot = path.join(repoRoot, ".state", "web-live-dom-probe");

if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
  try {
    const args = parseProbeArgs(process.argv.slice(2));
    const result = args.command === "start"
      ? await startProbe(args)
      : args.command === "status"
        ? await probeStatus(args)
        : args.command === "help"
          ? helpPayload()
          : await runProbe(args);
    process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
    process.exitCode = ["pass", "running", "usage"].includes(result.status) ? 0 : 2;
  } catch (error) {
    process.stderr.write(`[web-live-dom-probe] ${error instanceof Error ? error.stack : String(error)}\n`);
    process.stdout.write(`${JSON.stringify({ ok: false, command: "web-live-dom-probe", status: "blocked", error: error instanceof Error ? error.message : String(error) }, null, 2)}\n`);
    process.exitCode = 2;
  }
}

export function parseProbeArgs(argv) {
  const command = argv[0] && !argv[0].startsWith("--") ? argv[0] : "run";
  const startIndex = command === "run" ? (argv[0] === "run" ? 1 : 0) : 1;
  const args = {
    command,
    url: process.env.HWLAB_WEB_PROBE_URL ?? process.env.HWLAB_WEB_URL ?? "http://74.48.78.17:19666/",
    user: process.env.HWLAB_WEB_USER ?? "admin",
    pass: process.env.HWLAB_WEB_PASS ?? "",
    credentials: {
      userSource: process.env.HWLAB_WEB_USER ? "env:HWLAB_WEB_USER" : "default",
      passSource: process.env.HWLAB_WEB_PASS ? "env:HWLAB_WEB_PASS" : "unresolved",
      valuesRedacted: true
    },
    credentialError: null,
    viewport: process.env.HWLAB_WEB_PROBE_VIEWPORT ?? "1440x900",
    message: process.env.HWLAB_WEB_PROBE_MESSAGE ?? "",
    freshSession: false,
    cancelRunning: true,
    waitAfterSubmitMs: 1500,
    timeoutMs: 30000,
    projectId: process.env.HWLAB_WEB_PROBE_PROJECT_ID ?? "prj_hwpod_workbench",
    conversationId: process.env.HWLAB_WEB_PROBE_CONVERSATION_ID ?? "",
    waitMessagesMs: 2500,
    waitAgentTerminalMs: Number(process.env.HWLAB_WEB_PROBE_WAIT_AGENT_TERMINAL_MS ?? "0"),
    traceSampleCount: envNonNegativeInteger("HWLAB_WEB_PROBE_TRACE_SAMPLE_COUNT", 0),
    traceSampleIntervalMs: envNonNegativeInteger("HWLAB_WEB_PROBE_TRACE_SAMPLE_INTERVAL_MS", 0),
    reportPath: tempReportPath("web-live-dom-probe.json"),
    screenshotPath: tempReportPath("web-live-dom-probe.png"),
    jobId: ""
  };
  if (command === "help" || argv.includes("--help") || argv.includes("-h")) return { ...args, command: "help" };
  for (let index = startIndex; index < argv.length; index += 1) {
    const arg = argv[index];
    const readValue = (name) => {
      const inline = arg.match(new RegExp(`^${name}=(.+)$`, "u"));
      if (inline) return inline[1];
      const value = argv[index + 1];
      if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
      index += 1;
      return value;
    };
    if (arg === "--url" || arg.startsWith("--url=")) args.url = readValue("--url");
    else if (arg === "--user" || arg.startsWith("--user=")) {
      args.user = readValue("--user");
      args.credentials.userSource = "cli:--user";
    }
    else if (arg === "--pass" || arg.startsWith("--pass=")) {
      args.pass = readValue("--pass");
      args.credentials.passSource = "cli:--pass";
    }
    else if (arg === "--viewport" || arg.startsWith("--viewport=")) args.viewport = readValue("--viewport");
    else if (arg === "--message" || arg.startsWith("--message=")) args.message = readValue("--message");
    else if (arg === "--project-id" || arg.startsWith("--project-id=")) args.projectId = readValue("--project-id");
    else if (arg === "--conversation-id" || arg.startsWith("--conversation-id=")) args.conversationId = readValue("--conversation-id");
    else if (arg === "--report" || arg.startsWith("--report=")) args.reportPath = readValue("--report");
    else if (arg === "--screenshot" || arg.startsWith("--screenshot=")) args.screenshotPath = readValue("--screenshot");
    else if (arg === "--job-id" || arg.startsWith("--job-id=")) args.jobId = readValue("--job-id");
    else if (arg === "--wait-after-submit-ms" || arg.startsWith("--wait-after-submit-ms=")) args.waitAfterSubmitMs = positiveInteger(readValue("--wait-after-submit-ms"), "--wait-after-submit-ms");
    else if (arg === "--wait-messages-ms" || arg.startsWith("--wait-messages-ms=")) args.waitMessagesMs = positiveInteger(readValue("--wait-messages-ms"), "--wait-messages-ms");
    else if (arg === "--wait-agent-terminal-ms" || arg.startsWith("--wait-agent-terminal-ms=")) args.waitAgentTerminalMs = positiveInteger(readValue("--wait-agent-terminal-ms"), "--wait-agent-terminal-ms");
    else if (arg === "--trace-sample-count" || arg.startsWith("--trace-sample-count=")) args.traceSampleCount = nonNegativeInteger(readValue("--trace-sample-count"), "--trace-sample-count");
    else if (arg === "--trace-sample-interval-ms" || arg.startsWith("--trace-sample-interval-ms=")) args.traceSampleIntervalMs = nonNegativeInteger(readValue("--trace-sample-interval-ms"), "--trace-sample-interval-ms");
    else if (arg === "--timeout-ms" || arg.startsWith("--timeout-ms=")) args.timeoutMs = positiveInteger(readValue("--timeout-ms"), "--timeout-ms");
    else if (arg === "--fresh-session") args.freshSession = true;
    else if (arg === "--no-cancel-running") args.cancelRunning = false;
    else if (command === "status" && !args.jobId) args.jobId = arg;
    else throw new Error(`unknown web-live-dom-probe argument: ${arg}`);
  }
  resolveProbeCredentials(args);
  return args;
}

function resolveProbeCredentials(args) {
  if (args.command === "help" || args.command === "status") return;
  if (args.pass) return;
  const target = inferProbeCredentialTarget(args.url);
  if (!target) {
    args.pass = "hwlab2026";
    args.credentials = { ...args.credentials, passSource: "legacy-default", valuesRedacted: true };
    return;
  }
  if (args.credentials.userSource === "default") {
    args.user = target.username;
    args.credentials.userSource = "target-default";
  }
  const material = readProbeSecretSource(target.sourceRef, target.sourceKey);
  args.credentials = {
    ...args.credentials,
    node: target.node,
    lane: target.lane,
    sourceRef: target.sourceRef,
    sourceKey: target.sourceKey,
    sourcePath: material.sourcePath ? displayProbeSecretPath(material.sourcePath) : null,
    sourcePresent: material.present,
    sourceFingerprint: material.fingerprint,
    passSource: material.ok ? "secret-source" : "missing-secret-source",
    valuesRedacted: true
  };
  if (material.ok) {
    args.pass = material.value;
    return;
  }
  args.credentialError = {
    code: "web_login_secret_missing",
    message: `Web login secret source is missing for ${target.node}/${target.lane}`,
    reason: material.reason
  };
}

function inferProbeCredentialTarget(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    return null;
  }
  if (parsed.hostname === "hwlab.pikapython.com") {
    return {
      node: "D601",
      lane: "v03",
      username: "admin",
      sourceRef: "hwlab/d601-v03-bootstrap-admin.env",
      sourceKey: "HWLAB_BOOTSTRAP_ADMIN_PASSWORD"
    };
  }
  if (parsed.hostname === "74.48.78.17" && (parsed.port === "20666" || parsed.port === "20667")) {
    return {
      node: "G14",
      lane: "v03",
      username: "admin",
      sourceRef: "hwlab/g14-v03-bootstrap-admin.env",
      sourceKey: "HWLAB_BOOTSTRAP_ADMIN_PASSWORD"
    };
  }
  return null;
}

function readProbeSecretSource(sourceRef, sourceKey) {
  const sourcePath = probeSecretSourcePaths(sourceRef).find((candidate) => fs.existsSync(candidate));
  if (!sourcePath) return { ok: false, present: false, sourcePath: probeSecretSourcePaths(sourceRef)[0] ?? null, fingerprint: null, reason: "secret-source-missing" };
  const values = parseEnvFile(fs.readFileSync(sourcePath, "utf8"));
  const value = values[sourceKey];
  if (!value) return { ok: false, present: true, sourcePath, fingerprint: null, reason: "secret-key-missing" };
  return { ok: true, present: true, sourcePath, value, fingerprint: `sha256:${createHash("sha256").update(value).digest("hex").slice(0, 16)}`, reason: null };
}

function probeSecretSourcePaths(sourceRef) {
  const roots = [
    process.env.HWLAB_WEB_SECRET_SOURCE_ROOT,
    process.env.UNIDESK_SECRET_SOURCE_ROOT,
    path.join(repoRoot, ".state", "secrets"),
    repoRoot.includes("/.worktree/") ? path.join(repoRoot.slice(0, repoRoot.indexOf("/.worktree/")), ".state", "secrets") : null,
    "/root/unidesk/.state/secrets"
  ].filter(Boolean);
  return [...new Set(roots.map((root) => path.join(root, sourceRef)))];
}

function parseEnvFile(text) {
  const values = {};
  for (const rawLine of text.split(/\r?\n/u)) {
    const line = rawLine.trim();
    if (!line || line.startsWith("#")) continue;
    const eq = line.indexOf("=");
    if (eq <= 0) continue;
    const key = line.slice(0, eq).trim();
    const rawValue = line.slice(eq + 1).trim();
    values[key] = rawValue.startsWith("'") && rawValue.endsWith("'")
      ? rawValue.slice(1, -1).replace(/'"'"'/gu, "'")
      : rawValue.startsWith('"') && rawValue.endsWith('"')
        ? rawValue.slice(1, -1)
        : rawValue;
  }
  return values;
}

function displayProbeSecretPath(sourcePath) {
  if (sourcePath.startsWith(`${repoRoot}/`)) return sourcePath.slice(repoRoot.length + 1);
  const unideskRoot = "/root/unidesk/";
  if (sourcePath.startsWith(unideskRoot)) return sourcePath.slice(unideskRoot.length);
  return sourcePath;
}

export async function runProbe(args) {
  const startedAt = new Date().toISOString();
  const reportPath = ensureNotRepoReportsPath(repoRoot, path.resolve(repoRoot, args.reportPath), "--report");
  const screenshotPath = ensureNotRepoReportsPath(repoRoot, path.resolve(repoRoot, args.screenshotPath), "--screenshot");
  const { width, height } = parseViewport(args.viewport);
  const actions = [];
  let browser;
  let page;
  let result;
  let traceSamples = [];
  let sessionSummary = null;
  let traceSummary = null;
  let performanceSummary = null;
  if (args.credentialError) {
    result = {
      ok: false,
      status: "blocked",
      command: "web-live-dom-probe run",
      generatedAt: new Date().toISOString(),
      startedAt,
      url: args.url,
      actions,
      credentials: args.credentials,
      error: args.credentialError.code,
      credentialError: args.credentialError,
      artifacts: { reportPath, screenshotPath }
    };
    await mkdir(path.dirname(reportPath), { recursive: true });
    await writeFile(reportPath, `${JSON.stringify(result, null, 2)}\n`, "utf8");
    result.artifacts = {
      ...(result.artifacts ?? {}),
      reportPath,
      reportSha256: await fileSha256(reportPath),
      screenshotPath,
      screenshotSha256: await fileSha256(screenshotPath)
    };
    return result;
  }
  try {
    const { chromium } = await importPlaywright();
    browser = await launchChromium(chromium);
    const context = await browser.newContext({ viewport: { width, height } });
    page = await context.newPage();
    const bootstrapRoutes = observeBootstrapRoutes(page);
    await page.goto(args.url, { waitUntil: "domcontentloaded", timeout: args.timeoutMs });
    await waitForAuthBootstrap(page, args, actions);
    await maybeLogin(page, args, actions);
    await page.locator("#workspace").waitFor({ state: "visible", timeout: args.timeoutMs });
    await page.locator("#command-input").waitFor({ state: "visible", timeout: args.timeoutMs });
    if (args.conversationId) await selectConversationInBrowserSession(page, args, actions);
    if (args.freshSession) await createFreshSession(page, args, actions);
    if (args.message) await submitPrompt(page, args, actions);
    if (args.message && args.traceSampleCount > 0) traceSamples = await sampleTraceTimeline(page, args, actions);
    if (args.waitAgentTerminalMs) await waitForAgentTerminal(page, args, actions);
    if (args.message && args.cancelRunning) await maybeClick(page, ".message-action-cancel", actions, "cancel-running-message");
    await waitForMessageCards(page, args, actions);
    await page.screenshot({ path: screenshotPath, fullPage: true });
    const dom = await collectDomEvidence(page, args);
    const promptValidation = validatePromptFinalResponse(dom, args);
    sessionSummary = summarizeSessionRun(actions, dom);
    traceSummary = summarizeTraceRun(traceSamples, dom, sessionSummary);
    performanceSummary = summarizeProbePerformance(actions, startedAt, traceSamples, traceSummary);
    const degradedReason = runProbeDegradedReason({ traceSummary, sessionSummary, promptValidation, actions });
    result = {
      ok: true,
      status: dom.requiredSelectors.workspace && dom.requiredSelectors.commandInput && dom.requiredSelectors.conversationList && promptValidation.ok && !degradedReason ? "pass" : "blocked",
      command: "web-live-dom-probe run",
      generatedAt: new Date().toISOString(),
      startedAt,
      url: args.url,
      finalUrl: page.url(),
      viewport: { width, height },
      browser: browser.hwlabLaunchPlan ?? null,
      actions,
      credentials: args.credentials,
      bootstrapRoutes: summarizeBootstrapRoutes(bootstrapRoutes),
      session: sessionSummary,
      trace: traceSummary,
      performance: performanceSummary,
      traceSamples,
      dom,
      promptValidation,
      degradedReason,
      artifacts: { reportPath, screenshotPath },
      safety: {
        repoOwnedLauncher: true,
        directChromiumLaunch: false,
        projectId: args.projectId,
        conversationSelected: Boolean(args.conversationId),
        freshSessionRequested: args.freshSession,
        promptSubmitted: Boolean(args.message),
        traceSamplingRequested: args.traceSampleCount > 0,
        cancelRunningRequested: args.cancelRunning
      }
    };
  } catch (error) {
    const finalUrl = page ? page.url() : null;
    const failureDom = page ? await collectFailureDomEvidence(page).catch((domError) => ({ error: domError instanceof Error ? domError.message : String(domError) })) : null;
    let screenshotCapture = null;
    if (page && !page.isClosed()) {
      try {
        await page.screenshot({ path: screenshotPath, fullPage: true });
        screenshotCapture = { ok: true };
      } catch (screenshotError) {
        screenshotCapture = { ok: false, error: screenshotError instanceof Error ? screenshotError.message : String(screenshotError) };
      }
    }
    result = {
      ok: false,
      status: "blocked",
      command: "web-live-dom-probe run",
      generatedAt: new Date().toISOString(),
      startedAt,
      url: args.url,
      browser: browser?.hwlabLaunchPlan ?? error?.hwlabBrowserLaunch ?? null,
      actions,
      credentials: args.credentials,
      error: error instanceof Error ? error.message : String(error),
      degradedReason: classifyRunProbeError(error, { finalUrl, failureDom }),
      finalUrl,
      failureDom,
      artifacts: { reportPath, screenshotPath, screenshotCapture }
    };
  } finally {
    if (browser) await browser.close().catch(() => {});
  }
  await mkdir(path.dirname(reportPath), { recursive: true });
  await writeFile(reportPath, `${JSON.stringify(result, null, 2)}\n`, "utf8");
  result.artifacts = {
    ...(result.artifacts ?? {}),
    reportPath,
    reportSha256: await fileSha256(reportPath),
    screenshotPath,
    screenshotSha256: await fileSha256(screenshotPath)
  };
  return result;
}

async function collectFailureDomEvidence(page) {
  return page.evaluate(() => {
    const text = (selector) => document.querySelector(selector)?.textContent?.trim() ?? null;
    const bodyText = document.body?.innerText?.replace(/\s+/gu, " ").trim() ?? "";
    return {
      authState: document.body?.getAttribute("data-auth-state") ?? null,
      title: document.title || null,
      locationPath: window.location.pathname,
      login: {
        cardVisible: Boolean(document.querySelector(".login-card")),
        usernameVisible: Boolean(document.querySelector('#login-username, input[autocomplete="username"]')),
        passwordVisible: Boolean(document.querySelector('#login-password, input[autocomplete="current-password"], input[type="password"]')),
        errorText: text(".form-error, .login-error, [role='alert'], .error, .text-red-500, .text-destructive")
      },
      requiredSelectors: {
        workspace: Boolean(document.querySelector("#workspace")),
        commandInput: Boolean(document.querySelector("#command-input")),
        conversationList: Boolean(document.querySelector("#conversation-list"))
      },
      bodyTextPreview: bodyText.slice(0, 500)
    };
  });
}

function validatePromptFinalResponse(dom, args) {
  if (!args.message) return { ok: true, applicable: false, reason: "no-prompt-submitted" };
  if (args.traceSampleCount > 0 && !args.waitAgentTerminalMs) return { ok: true, applicable: false, reason: "trace-sampling-without-terminal-wait" };
  const messages = Array.isArray(dom.messages) ? dom.messages : [];
  const latestAgent = [...messages].reverse().find((message) => message.role === "agent");
  const finalResponse = latestAgent?.finalResponse ?? null;
  const markdown = finalResponse?.markdown ?? null;
  const failures = [];
  if (!latestAgent) failures.push("agent-message-missing");
  if (latestAgent?.status !== "completed") failures.push("agent-not-completed");
  if (finalResponse?.present !== true) failures.push("final-response-missing");
  if (!finalResponse || Number(finalResponse.textChars ?? 0) <= 0) failures.push("final-response-empty");
  if (Number(markdown?.headings ?? 0) <= 0) failures.push("markdown-heading-missing");
  if (Number(markdown?.lists ?? 0) <= 0 || Number(markdown?.listItems ?? 0) <= 0) failures.push("markdown-list-missing");
  if (Number(markdown?.codeBlocks ?? 0) <= 0) failures.push("markdown-code-block-missing");
  return {
    ok: failures.length === 0,
    applicable: true,
    failures,
    finalResponse: finalResponse ? {
      textChars: finalResponse.textChars ?? null,
      textPreview: finalResponse.textPreview ?? null,
      markdown: finalResponse.markdown ?? null
    } : null
  };
}

async function startProbe(args) {
  if (args.credentialError) {
    return {
      ok: false,
      status: "blocked",
      command: "web-live-dom-probe start",
      error: args.credentialError.code,
      credentialError: args.credentialError,
      credentials: args.credentials
    };
  }
  await mkdir(stateRoot, { recursive: true });
  const jobId = args.jobId || `web-live-dom-probe-${Date.now()}-${process.pid}`;
  const reportPath = path.join(stateRoot, `${jobId}.result.json`);
  const screenshotPath = path.join(stateRoot, `${jobId}.png`);
  const stdoutPath = path.join(stateRoot, `${jobId}.stdout.log`);
  const stderrPath = path.join(stateRoot, `${jobId}.stderr.log`);
  const metaPath = path.join(stateRoot, `${jobId}.json`);
  const childArgs = [
    scriptPath,
    "run",
    "--url", args.url,
    "--user", args.user,
    "--viewport", args.viewport,
    "--project-id", args.projectId,
    "--report", reportPath,
    "--screenshot", screenshotPath,
    "--timeout-ms", String(args.timeoutMs),
    "--wait-after-submit-ms", String(args.waitAfterSubmitMs),
    "--wait-messages-ms", String(args.waitMessagesMs)
  ];
  if (args.waitAgentTerminalMs > 0) childArgs.push("--wait-agent-terminal-ms", String(args.waitAgentTerminalMs));
  if (args.traceSampleCount > 0) childArgs.push("--trace-sample-count", String(args.traceSampleCount), "--trace-sample-interval-ms", String(args.traceSampleIntervalMs));
  if (args.freshSession) childArgs.push("--fresh-session");
  if (!args.cancelRunning) childArgs.push("--no-cancel-running");
  if (args.conversationId) childArgs.push("--conversation-id", args.conversationId);
  if (args.message) childArgs.push("--message", args.message);
  const stdout = fs.openSync(stdoutPath, "a");
  const stderr = fs.openSync(stderrPath, "a");
  const child = spawn(process.execPath, childArgs, {
    cwd: repoRoot,
    detached: true,
    env: { ...process.env, HWLAB_WEB_USER: args.user, HWLAB_WEB_PASS: args.pass },
    stdio: ["ignore", stdout, stderr]
  });
  child.unref();
  const meta = {
    ok: true,
    status: "running",
    command: "web-live-dom-probe start",
    jobId,
    pid: child.pid,
    startedAt: new Date().toISOString(),
    url: args.url,
    projectId: args.projectId,
    conversationId: args.conversationId || null,
    traceSampling: args.traceSampleCount > 0 ? { count: args.traceSampleCount, intervalMs: args.traceSampleIntervalMs } : null,
    reportPath,
    screenshotPath,
    stdoutPath,
    stderrPath,
    statusCommand: `node scripts/web-live-dom-probe.mjs status ${jobId}`
  };
  await writeFile(metaPath, `${JSON.stringify(meta, null, 2)}\n`, "utf8");
  return meta;
}

async function probeStatus(args) {
  if (!args.jobId) throw new Error("status requires a job id");
  const metaPath = path.join(stateRoot, `${args.jobId}.json`);
  const meta = JSON.parse(await readFile(metaPath, "utf8"));
  const report = fs.existsSync(meta.reportPath) ? await attachReportArtifactFingerprints(JSON.parse(await readFile(meta.reportPath, "utf8")), meta) : null;
  const running = !report && typeof meta.pid === "number" && processAlive(meta.pid);
  return {
    ok: report ? report.ok === true : running,
    status: report ? report.status : running ? "running" : "blocked",
    command: "web-live-dom-probe status",
    jobId: args.jobId,
    pid: meta.pid,
    startedAt: meta.startedAt,
    reportPath: meta.reportPath,
    screenshotPath: meta.screenshotPath,
    stdoutPath: meta.stdoutPath,
    stderrPath: meta.stderrPath,
    running,
    report,
    reportSummary: report ? { status: report.status, url: report.url, finalUrl: report.finalUrl, browser: report.browser, dom: report.dom } : null,
    stdoutTail: await readTail(meta.stdoutPath),
    stderrTail: await readTail(meta.stderrPath)
  };
}

async function attachReportArtifactFingerprints(report, meta) {
  if (!report) return report;
  const reportPath = report.artifacts?.reportPath ?? meta.reportPath ?? null;
  const screenshotPath = report.artifacts?.screenshotPath ?? meta.screenshotPath ?? null;
  return {
    ...report,
    artifacts: {
      ...(report.artifacts ?? {}),
      reportPath,
      reportSha256: await fileSha256(reportPath),
      screenshotPath,
      screenshotSha256: await fileSha256(screenshotPath)
    }
  };
}

async function maybeLogin(page, args, actions) {
  const username = page.locator("#login-username");
  if (await username.isVisible({ timeout: Math.min(args.timeoutMs, 12000) }).catch(() => false)) {
    await username.fill(args.user);
    await page.locator("#login-password").fill(args.pass);
    await page.locator("#login-submit").click();
    actions.push({ action: "login", user: args.user, selectorMode: "legacy-id" });
    await page.locator("#command-input").waitFor({ state: "visible", timeout: args.timeoutMs });
    const finalUrl = new URL(page.url());
    if (finalUrl.searchParams.has("username") || finalUrl.searchParams.has("password")) {
      throw new Error("login credentials leaked into URL query");
    }
    return;
  }
  const loginSurface = await waitForLoginSurface(page, args);
  if (loginSurface === "workspace") {
    actions.push({ action: "login", skipped: "already-authenticated", selectorMode: "workspace", authState: await page.locator("body").getAttribute("data-auth-state").catch(() => null) });
    return;
  }
  if (loginSurface === "legacy-id") {
    await username.fill(args.user);
    await page.locator("#login-password").fill(args.pass);
    await page.locator("#login-submit").click();
    actions.push({ action: "login", user: args.user, selectorMode: "legacy-id-delayed" });
    await page.locator("#command-input").waitFor({ state: "visible", timeout: args.timeoutMs });
    const finalUrl = new URL(page.url());
    if (finalUrl.searchParams.has("username") || finalUrl.searchParams.has("password")) {
      throw new Error("login credentials leaked into URL query");
    }
    return;
  }
  if (loginSurface !== "login-card") {
    actions.push({ action: "login", skipped: "login-form-not-visible", authState: await page.locator("body").getAttribute("data-auth-state").catch(() => null) });
    return;
  }
  await submitCurrentLoginForm(page, args, actions, "current-autocomplete");
}

async function waitForLoginSurface(page, args) {
  const handle = await page.waitForFunction(() => {
    if (document.querySelector("#workspace")) return "workspace";
    if (document.querySelector("#login-username")) return "legacy-id";
    if (document.querySelector('input[autocomplete="username"]') && document.querySelector('input[autocomplete="current-password"]')) return "login-card";
    if (document.querySelector(".login-card input")) return "login-card";
    return "";
  }, null, { timeout: args.timeoutMs }).catch(() => null);
  if (!handle) return null;
  return handle.jsonValue().catch(() => null);
}

async function submitCurrentLoginForm(page, args, actions, selectorMode) {
  const username = page.locator('input[autocomplete="username"], .login-card input').first();
  const password = page.locator('input[autocomplete="current-password"], .login-card input[type="password"], input[type="password"]').first();
  const submit = page.locator('.login-card button[type="submit"], button[type="submit"]').first();
  const inputCount = await page.locator(".login-card input, input[autocomplete=\"username\"], input[autocomplete=\"current-password\"]").count().catch(() => 0);
  if (!(await username.isVisible({ timeout: 5000 }).catch(() => false)) || !(await password.isVisible({ timeout: 5000 }).catch(() => false))) {
    actions.push({ action: "login", skipped: "login-form-incomplete", selectorMode, inputCount, authState: await page.locator("body").getAttribute("data-auth-state").catch(() => null) });
    return;
  }
  await username.fill(args.user);
  await password.fill(args.pass);
  const valuesReady = await waitForCurrentLoginValues(page, args);
  await Promise.allSettled([
    page.waitForURL(/\/workbench/u, { timeout: Math.min(args.timeoutMs, 15000) }),
    submit.click()
  ]);
  actions.push({ action: "login", user: args.user, selectorMode, valuesReady });
  const outcome = await waitForLoginOutcome(page, args);
  if (outcome?.kind === "error") throw new Error(`login failed: ${outcome.errorText || "unknown login error"}`);
  await page.locator("#command-input").waitFor({ state: "visible", timeout: args.timeoutMs });
  assertNoCredentialLeak(page.url());
}

async function waitForCurrentLoginValues(page, args) {
  const ready = await page.waitForFunction((expected) => {
    const username = document.querySelector('input[autocomplete="username"], .login-card input');
    const password = document.querySelector('input[autocomplete="current-password"], .login-card input[type="password"], input[type="password"]');
    const submit = document.querySelector('.login-card button[type="submit"], button[type="submit"]');
    return Boolean(username && password && username.value === expected.user && password.value.length === expected.passwordLength && !submit?.disabled);
  }, { user: args.user, passwordLength: args.pass.length }, { timeout: Math.min(args.timeoutMs, 5000) }).catch(() => null);
  return Boolean(ready);
}

async function waitForLoginOutcome(page, args) {
  const handle = await page.waitForFunction(() => {
    if (document.querySelector("#command-input")) return { kind: "workspace" };
    const errorText = document.querySelector(".form-error, .login-error, [role='alert'], .error, .text-red-500, .text-destructive")?.textContent?.trim();
    if (errorText) return { kind: "error", errorText };
    return null;
  }, null, { timeout: Math.min(args.timeoutMs, 15000) }).catch(() => null);
  if (!handle) return null;
  return handle.jsonValue().catch(() => null);
}

function assertNoCredentialLeak(value) {
  const finalUrl = new URL(value);
  if (finalUrl.searchParams.has("username") || finalUrl.searchParams.has("password")) {
    throw new Error("login credentials leaked into URL query");
  }
}

async function waitForAuthBootstrap(page, args, actions) {
  const timeout = Math.min(args.timeoutMs, 12000);
  const settled = await page.waitForFunction(() => {
    const authState = document.body.getAttribute("data-auth-state");
    const loginVisible = Boolean(document.querySelector("#login-username") || document.querySelector('input[autocomplete="username"]') || document.querySelector(".login-card input"));
    const workspaceVisible = Boolean(document.querySelector("#workspace"));
    return authState !== "checking" || loginVisible || workspaceVisible;
  }, null, { timeout }).catch(() => null);
  actions.push({
    action: "auth-bootstrap",
    settled: Boolean(settled),
    authState: await page.locator("body").getAttribute("data-auth-state").catch(() => null)
  });
}

async function maybeClick(page, selector, actions, action) {
  const locator = page.locator(selector).first();
  if (await locator.isVisible({ timeout: 3000 }).catch(() => false)) {
    await locator.click();
    actions.push({ action, selector });
    await page.waitForTimeout(1000);
    return true;
  }
  actions.push({ action, selector, skipped: "not-visible" });
  return false;
}

async function createFreshSession(page, args, actions) {
  const selector = "#session-create";
  const locator = page.locator(selector).first();
  if (!(await locator.isVisible({ timeout: 3000 }).catch(() => false))) {
    actions.push({ action: "fresh-session", selector, skipped: "not-visible" });
    throw new Error("fresh session create button is not visible");
  }
  const shellReady = await waitForSessionShellReady(page, args, actions);
  const before = await collectSessionState(page, args);
  await locator.click();
  const timeoutMs = Math.min(args.timeoutMs, 30000);
  const settled = await page.waitForFunction((initial) => {
    const statusText = document.querySelector("#session-status")?.textContent?.trim() ?? null;
    const statusReady = Boolean(statusText && !/加载中|等待 workspace/u.test(statusText));
    const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']");
    const activeTitle = activeTab?.querySelector("strong")?.textContent?.trim() ?? activeTab?.getAttribute("title") ?? null;
    const activeChanged = Boolean(activeTitle && activeTitle !== initial.activeTabTitle);
    const statusChanged = Boolean(statusText && statusText !== initial.statusText);
    const messageCount = document.querySelectorAll(".message-card").length;
    const input = document.querySelector("#command-input");
    return statusReady && (activeChanged || statusChanged) && messageCount === 0 && input && !input.disabled;
  }, before, { timeout: timeoutMs }).catch(() => null);
  const alignment = await waitForFreshSessionAlignment(page, args, before, timeoutMs);
  const after = alignment.after;
  actions.push({ action: "fresh-session", selector, settled: Boolean(settled), aligned: alignment.ok, timeoutMs, shellReady, before, after, alignment });
  if (!alignment.ok) throw new Error(`fresh session did not settle: ${JSON.stringify({ settled: Boolean(settled), alignment, timeoutMs })}`);
  return true;
}

async function waitForFreshSessionAlignment(page, args, before, timeoutMs) {
  const startedAt = Date.now();
  const attempts = [];
  let after = await collectSessionState(page, args);
  let bestCandidate = null;
  while (Date.now() - startedAt < timeoutMs) {
    const check = freshSessionAlignment(before, after);
    bestCandidate = bestCandidate ?? freshSessionCandidate(before, after);
    attempts.push(freshSessionAttemptSummary(check, Date.now() - startedAt));
    if (check.ok) return { ok: true, reason: null, attempts, after, check };
    await page.waitForTimeout(500);
    after = await collectSessionState(page, args);
  }
  const check = freshSessionAlignment(before, after);
  bestCandidate = bestCandidate ?? freshSessionCandidate(before, after);
  attempts.push(freshSessionAttemptSummary(check, Date.now() - startedAt));
  if (!check.ok && bestCandidate?.id) {
    const repair = await realignFreshSession(page, args, bestCandidate.id);
    after = await collectSessionState(page, args);
    const repaired = freshSessionAlignment(before, after);
    return { ok: repaired.ok, reason: repaired.reason, attempts, after, check: repaired, repair };
  }
  return { ok: false, reason: check.reason, attempts, after, check };
}

function freshSessionAttemptSummary(check, elapsedMs) {
  return {
    elapsedMs,
    ok: check.ok,
    reason: check.reason,
    id: check.id,
    routeId: check.routeId,
    selectedConversationId: check.selectedConversationId,
    activeTabSessionId: check.activeTabSessionId,
    activeTabConversationId: check.activeTabConversationId,
    apiConversationPresent: check.apiConversationPresent,
    workspaceApiAvailable: check.workspaceApiAvailable,
    conversationsApiAvailable: check.conversationsApiAvailable
  };
}

function freshSessionCandidate(before, after) {
  const previousConversationId = sessionAuthorityId(before);
  const items = Array.isArray(after?.api?.conversations?.items) ? after.api.conversations.items : [];
  const apiCandidate = items.find((item) => item.conversationId && item.conversationId !== previousConversationId && Number(item.messagesCount ?? 0) === 0);
  if (apiCandidate) return { ...apiCandidate, id: apiCandidate.conversationId, source: "api-conversations" };
  const tabs = Array.isArray(after?.sessionTabs) ? after.sessionTabs : [];
  const tabCandidate = tabs.find((item) => firstNonEmpty(item.sessionId, item.conversationId) && firstNonEmpty(item.sessionId, item.conversationId) !== previousConversationId && item.running !== "true");
  return tabCandidate ? { ...tabCandidate, id: firstNonEmpty(tabCandidate.sessionId, tabCandidate.conversationId), source: "session-tab" } : null;
}

async function realignFreshSession(page, args, sessionOrConversationId) {
  const selected = await selectSessionViaRoute(page, args, sessionOrConversationId, "web-live-dom-probe-fresh-realign").catch((error) => ({ ok: false, phase: "route-session", error: error instanceof Error ? error.message : String(error) }));
  const targetUrl = new URL(`/workbench/sessions/${encodeURIComponent(sessionOrConversationId)}`, args.url);
  targetUrl.searchParams.set("projectId", args.projectId);
  const target = targetUrl.toString();
  const attempts = [];
  for (let attempt = 1; attempt <= 2; attempt += 1) {
    const navigation = await page.goto(target, { waitUntil: "domcontentloaded", timeout: Math.min(args.timeoutMs, 30000) })
      .then((response) => ({ ok: true, status: response?.status() ?? null, url: page.url() }))
      .catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error), url: page.url() }));
    const ready = await waitForWorkbenchSessionRouteReady(page, args, attempt === 1 ? 15000 : 30000);
    attempts.push({ attempt, navigation, ready });
    if (ready.ok) return { action: "fresh-session-realign", id: sessionOrConversationId, target, selected, attempts };
    await page.waitForTimeout(1000);
  }
  return { action: "fresh-session-realign", id: sessionOrConversationId, target, selected, attempts, ok: false };
}

async function waitForWorkbenchSessionRouteReady(page, args, timeoutMs) {
  await page.waitForFunction(() => {
    const authState = document.body.getAttribute("data-auth-state");
    const workspace = document.querySelector("#workspace");
    const loginVisible = Boolean(document.querySelector("#login-username") || document.querySelector('input[autocomplete="username"]') || document.querySelector(".login-card input"));
    return authState !== "checking" || workspace || loginVisible;
  }, null, { timeout: Math.min(timeoutMs, 10000) }).catch(() => null);
  const workspace = await page.locator("#workspace").waitFor({ state: "visible", timeout: timeoutMs }).then(() => ({ ok: true })).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) }));
  const commandInput = workspace.ok
    ? await page.locator("#command-input").waitFor({ state: "visible", timeout: timeoutMs }).then(() => ({ ok: true })).catch((error) => ({ ok: false, error: error instanceof Error ? error.message : String(error) }))
    : { ok: false, skipped: "workspace-not-visible" };
  const dom = await page.evaluate(() => ({
    href: window.location.href,
    authState: document.body.getAttribute("data-auth-state"),
    title: document.title,
    workspaceVisible: Boolean(document.querySelector("#workspace")),
    commandInputVisible: Boolean(document.querySelector("#command-input")),
    bodyTextPreview: document.body.textContent?.replace(/\s+/gu, " ").trim().slice(0, 200) ?? ""
  })).catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
  return { ok: workspace.ok && commandInput.ok, timeoutMs, workspace, commandInput, dom };
}

async function selectSessionViaRoute(page, args, conversationId, updatedByClient) {
  const targetUrl = new URL(`/workbench/sessions/${encodeURIComponent(conversationId)}`, args.url);
  const target = targetUrl.toString();
  const navigation = await page.goto(target, { waitUntil: "domcontentloaded", timeout: Math.min(args.timeoutMs, 30000) })
    .then((response) => ({ ok: true, status: response?.status() ?? null, url: page.url() }))
    .catch((error) => ({ ok: false, status: null, error: error instanceof Error ? error.message : String(error), url: page.url() }));
  const ready = await waitForWorkbenchSessionRouteReady(page, args, Math.min(args.timeoutMs, 30000));
  return {
    ok: navigation.ok && ready.ok,
    phase: "route-session",
    status: navigation.status,
    sessionId: conversationId,
    conversationId,
    updatedByClient,
    target,
    navigation,
    ready
  };
}

function freshSessionAlignment(before, after) {
  const previousConversationId = sessionAuthorityId(before);
  const conversationId = sessionAuthorityId(after);
  const selectedConversationId = after?.api?.workspace?.selectedConversationId ?? null;
  const routeConversationId = after?.routeConversationId ?? null;
  const activeTabSessionId = after?.activeTab?.sessionId ?? null;
  const activeTabConversationId = after?.activeTab?.conversationId ?? null;
  const workspaceApiAvailable = after?.api?.workspace?.ok === true;
  const conversationsApiAvailable = after?.api?.conversations?.ok === true;
  const apiConversation = conversationId ? after?.api?.conversations?.items?.find((item) => item.conversationId === conversationId || item.sessionId === conversationId) ?? null : null;
  const routeMatches = Boolean(routeConversationId && conversationId && routeConversationId === conversationId);
  const workspaceMatches = workspaceApiAvailable ? Boolean(selectedConversationId && conversationId && selectedConversationId === conversationId) : true;
  const apiConversationPresent = conversationsApiAvailable ? Boolean(apiConversation) : true;
  const activeTabMatches = Boolean(!activeTabSessionId && !activeTabConversationId) || activeTabSessionId === conversationId || activeTabConversationId === conversationId;
  const changed = Boolean(conversationId && conversationId !== previousConversationId);
  const commandReady = after?.command?.inputDisabled === false;
  const empty = after?.messageCount === 0;
  const ok = changed && routeMatches && workspaceMatches && apiConversationPresent && activeTabMatches && commandReady && empty;
  const reason = ok ? null
    : !changed ? "fresh-session-conversation-not-changed"
      : !routeMatches ? "fresh-session-route-not-aligned"
        : !workspaceMatches ? "fresh-session-workspace-not-aligned"
          : !apiConversationPresent ? "fresh-session-conversation-api-missing"
            : !activeTabMatches ? "fresh-session-active-tab-not-aligned"
              : !commandReady ? "fresh-session-command-not-ready"
                : "fresh-session-not-empty";
  return { ok, reason, previousConversationId, conversationId, id: conversationId, routeId: routeConversationId, routeConversationId, selectedConversationId, workspaceApiAvailable, conversationsApiAvailable, apiConversationPresent, activeTabConversationId, activeTabSessionId, commandReady, command: after?.command ?? null, messageCount: after?.messageCount ?? null };
}

function sessionAuthorityId(state) {
  return firstNonEmpty(state?.routeConversationId, state?.api?.workspace?.selectedConversationId, state?.activeTab?.conversationId, state?.activeTab?.sessionId);
}

function sessionAuthorityAlignment(state, expectedId, options = {}) {
  const routeConversationId = state?.routeConversationId ?? null;
  const selectedConversationId = state?.api?.workspace?.selectedConversationId ?? null;
  const selectedAgentSessionId = state?.api?.workspace?.selectedAgentSessionId ?? null;
  const activeTabSessionId = state?.activeTab?.sessionId ?? null;
  const activeTabConversationId = state?.activeTab?.conversationId ?? null;
  const workspaceApiAvailable = state?.api?.workspace?.ok === true;
  const conversationsApiAvailable = state?.api?.conversations?.ok === true;
  const apiConversation = expectedId ? state?.api?.conversations?.items?.find((item) => item.conversationId === expectedId || item.sessionId === expectedId) ?? null : null;
  const routeMatches = Boolean(routeConversationId && expectedId && routeConversationId === expectedId);
  const workspaceMatches = workspaceApiAvailable
    ? Boolean(expectedId && (selectedConversationId === expectedId || selectedAgentSessionId === expectedId))
    : true;
  const apiConversationPresent = conversationsApiAvailable ? Boolean(apiConversation) : true;
  const activeTabMatches = Boolean(!activeTabSessionId && !activeTabConversationId) || activeTabSessionId === expectedId || activeTabConversationId === expectedId;
  const commandReady = state?.command?.inputDisabled === false;
  const empty = options.requireEmpty === true ? state?.messageCount === 0 : true;
  const ok = Boolean(expectedId && routeMatches && workspaceMatches && apiConversationPresent && activeTabMatches && commandReady && empty);
  const reason = ok ? null
    : !expectedId ? "session-id-missing"
      : !routeMatches ? "session-route-not-aligned"
        : !workspaceMatches ? "session-workspace-not-aligned"
          : !apiConversationPresent ? "session-conversation-api-missing"
            : !activeTabMatches ? "session-active-tab-not-aligned"
              : !commandReady ? "session-command-not-ready"
                : "session-not-empty";
  return { ok, reason, expectedId, routeConversationId, selectedConversationId, selectedAgentSessionId, activeTabConversationId, activeTabSessionId, workspaceApiAvailable, conversationsApiAvailable, apiConversationPresent, commandReady, messageCount: state?.messageCount ?? null };
}

async function waitForSessionShellReady(page, args, actions) {
  const timeoutMs = Math.min(args.timeoutMs, 30000);
  const before = await collectSessionState(page, args);
  const ready = await page.waitForFunction(() => {
    const statusText = document.querySelector("#session-status")?.textContent?.trim() ?? "";
    const workspace = document.querySelector("#workspace");
    const create = document.querySelector("#session-create");
    const input = document.querySelector("#command-input");
    return Boolean(workspace && create && !create.disabled && input && !input.disabled && !/加载中|等待 workspace/u.test(statusText));
  }, null, { timeout: timeoutMs }).catch(() => null);
  const after = await collectSessionState(page, args);
  const result = { action: "session-shell-ready", ready: Boolean(ready), timeoutMs, before, after };
  actions.push(result);
  if (!ready) throw new Error(`session shell did not finish initial loading: ${JSON.stringify(result)}`);
  return after;
}

async function collectSessionState(page, args = null) {
  const dom = await page.evaluate(() => {
    const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']");
    const sessionTabs = [...document.querySelectorAll(".session-tab")].map((tab) => ({
      title: tab.querySelector("strong")?.textContent?.trim() ?? tab.getAttribute("title") ?? null,
      conversationId: tab.getAttribute("data-conversation-id") ?? null,
      sessionId: tab.getAttribute("data-session-id") ?? null,
      status: tab.getAttribute("data-status") ?? null,
      running: tab.getAttribute("data-running") ?? null,
      active: tab.getAttribute("data-active") === "true" || tab.getAttribute("aria-selected") === "true"
    }));
    const routeMatch = window.location.pathname.match(/\/workbench\/sessions\/([^/]+)/u) ?? window.location.pathname.match(/\/workspace\/sessions\/([^/]+)/u);
    return {
      finalUrl: window.location.href,
      locationPath: window.location.pathname,
      routeConversationId: routeMatch ? decodeURIComponent(routeMatch[1]) : null,
      statusText: document.querySelector("#session-status")?.textContent?.trim() ?? null,
      activeTabTitle: activeTab?.querySelector("strong")?.textContent?.trim() ?? activeTab?.getAttribute("title") ?? null,
      activeTab: activeTab ? {
        title: activeTab.querySelector("strong")?.textContent?.trim() ?? activeTab.getAttribute("title") ?? null,
        conversationId: activeTab.getAttribute("data-conversation-id") ?? null,
        sessionId: activeTab.getAttribute("data-session-id") ?? null,
        status: activeTab.getAttribute("data-status") ?? null,
        running: activeTab.getAttribute("data-running") ?? null
      } : null,
      tabCount: document.querySelectorAll(".session-tab").length,
      sessionTabs: sessionTabs.slice(0, 20),
      messageCount: document.querySelectorAll(".message-card").length,
      emptyVisible: Boolean(document.querySelector("[data-conversation-empty]")),
      command: {
        inputDisabled: Boolean(document.querySelector("#command-input")?.disabled),
        sendDisabled: Boolean(document.querySelector("#command-send")?.disabled),
        sendText: document.querySelector("#command-send")?.textContent?.trim() ?? null,
        formTitle: document.querySelector("#command-form")?.getAttribute("title") ?? null
      }
    };
  });
  if (!args) return dom;
  const currentSessionId = firstNonEmpty(dom.routeConversationId, dom.activeTab?.sessionId, dom.activeTab?.conversationId);
  const sessions = await fetchJsonFromPage(page, "/v1/workbench/sessions?limit=50", { timeoutMs: 8000 });
  const sessionDetail = currentSessionId ? await fetchJsonFromPage(page, `/v1/workbench/sessions/${encodeURIComponent(currentSessionId)}`, { timeoutMs: 8000 }) : null;
  const sessionMessages = currentSessionId ? await fetchJsonFromPage(page, `/v1/workbench/sessions/${encodeURIComponent(currentSessionId)}/messages`, { timeoutMs: 8000 }) : null;
  return {
    ...dom,
    api: {
      workspace: summarizeCurrentSessionFetch(currentSessionId, sessionDetail, sessionMessages),
      conversations: summarizeWorkbenchSessionsFetch(sessions)
    }
  };
}

async function fetchJsonFromPage(page, requestPath, options = {}) {
  const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 8000;
  return page.evaluate(async ({ requestPath: path, timeoutMs: fetchTimeoutMs }) => {
    const startedAt = Date.now();
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), fetchTimeoutMs);
    try {
      const response = await fetch(path, { credentials: "same-origin", headers: { accept: "application/json" }, signal: controller.signal });
      const text = await response.text();
      let json = null;
      let parseError = null;
      try {
        json = text ? JSON.parse(text) : null;
      } catch (error) {
        parseError = error instanceof Error ? error.message : String(error);
      }
      return {
        ok: response.ok && parseError === null,
        path,
        status: response.status,
        statusText: response.statusText,
        elapsedMs: Date.now() - startedAt,
        contentType: response.headers.get("content-type"),
        bodyPreview: text.slice(0, 500),
        json,
        parseError,
        error: parseError
      };
    } catch (error) {
      return {
        ok: false,
        path,
        status: null,
        statusText: null,
        elapsedMs: Date.now() - startedAt,
        contentType: null,
        bodyPreview: null,
        json: null,
        parseError: null,
        error: error instanceof Error ? error.message : String(error),
        errorName: error instanceof Error ? error.name : null
      };
    } finally {
      clearTimeout(timer);
    }
  }, { requestPath, timeoutMs });
}

function summarizeCurrentSessionFetch(sessionId, detailFetch, messagesFetch) {
  const detailBody = detailFetch?.json?.session ?? detailFetch?.json?.data?.session ?? detailFetch?.json?.data ?? detailFetch?.json ?? null;
  const messages = messagesFromFetch(messagesFetch);
  const latestMessage = messages.at(-1) ?? null;
  return {
    ok: detailFetch?.ok === true || messagesFetch?.ok === true,
    path: detailFetch?.path ?? messagesFetch?.path ?? null,
    status: detailFetch?.status ?? messagesFetch?.status ?? null,
    error: detailFetch?.error ?? messagesFetch?.error ?? null,
    bodyPreview: detailFetch?.ok === true || messagesFetch?.ok === true ? null : detailFetch?.bodyPreview ?? messagesFetch?.bodyPreview ?? null,
    workspaceId: null,
    projectId: normalizeProbeProjectId(detailBody?.projectId) ?? null,
    revision: detailBody?.revision ?? detailBody?.updatedAt ?? null,
    selectedConversationId: firstNonEmpty(detailBody?.sessionId, detailBody?.id, sessionId),
    selectedAgentSessionId: firstNonEmpty(detailBody?.sessionId, detailBody?.id, sessionId),
    activeTraceId: firstNonEmpty(detailBody?.lastTraceId, detailBody?.traceId, latestMessage?.traceId, latestMessage?.runnerTrace?.traceId),
    selectedConversationStatus: detailBody?.status ?? detailBody?.state ?? latestMessage?.status ?? null,
    selectedMessagesCount: messages.length
  };
}

function summarizeWorkbenchSessionsFetch(fetchResult) {
  const sessions = sessionsFromFetch(fetchResult);
  return {
    ok: fetchResult?.ok === true,
    path: fetchResult?.path ?? null,
    status: fetchResult?.status ?? null,
    error: fetchResult?.error ?? null,
    bodyPreview: fetchResult?.ok === true ? null : fetchResult?.bodyPreview ?? null,
    count: sessions.length,
    items: sessions.slice(0, 16).map((item) => ({
      conversationId: firstNonEmpty(item?.sessionId, item?.id),
      projectId: normalizeProbeProjectId(item?.projectId) ?? null,
      sessionId: firstNonEmpty(item?.sessionId, item?.id),
      threadId: item?.threadId ?? null,
      status: item?.status ?? null,
      messagesCount: Array.isArray(item?.messages) ? item.messages.length : item?.messagesCount ?? item?.messageCount ?? null,
      lastTraceId: firstNonEmpty(item?.lastTraceId, item?.traceId),
      updatedAt: item?.updatedAt ?? null
    }))
  };
}

function sessionsFromFetch(fetchResult) {
  if (Array.isArray(fetchResult?.json?.sessions)) return fetchResult.json.sessions;
  if (Array.isArray(fetchResult?.json?.data?.sessions)) return fetchResult.json.data.sessions;
  if (Array.isArray(fetchResult?.json?.items)) return fetchResult.json.items;
  if (Array.isArray(fetchResult?.json?.data?.items)) return fetchResult.json.data.items;
  return [];
}

function messagesFromFetch(fetchResult) {
  if (Array.isArray(fetchResult?.json?.messages)) return fetchResult.json.messages;
  if (Array.isArray(fetchResult?.json?.data?.messages)) return fetchResult.json.data.messages;
  if (Array.isArray(fetchResult?.json?.items)) return fetchResult.json.items;
  if (Array.isArray(fetchResult?.json?.data?.items)) return fetchResult.json.data.items;
  return [];
}

async function submitPrompt(page, args, actions) {
  const readyBefore = await waitForCommandReady(page, args, actions);
  const fresh = [...actions].reverse().find((item) => item.action === "fresh-session") ?? null;
  const targetConversationId = firstNonEmpty(fresh?.alignment?.check?.conversationId, fresh?.alignment?.after?.routeConversationId, fresh?.after?.routeConversationId);
  if (targetConversationId) await ensureConversationAligned(page, args, targetConversationId, actions, "pre-submit-realign");
  await page.locator("#command-input").fill(args.message);
  const enabled = await page.waitForFunction((expected) => {
    const input = document.querySelector("#command-input");
    const send = document.querySelector("#command-send");
    return input?.value === expected && send && !send.disabled;
  }, args.message, { timeout: Math.min(args.timeoutMs, 5000) }).catch(() => null);
  const afterFill = await collectCommandState(page);
  if (!enabled) {
    const session = await collectSessionState(page, args).catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
    throw new Error(`command composer is not ready for submit: ${JSON.stringify({ readyBefore, afterFill, session: compactSessionState(session) })}`);
  }
  const submittedAt = new Date().toISOString();
  await page.locator("#command-send").click();
  const afterClick = await collectCommandState(page);
  actions.push({ action: "submit-prompt", chars: args.message.length, submittedAt, readyBefore, afterFill, afterClick });
  await page.waitForTimeout(args.waitAfterSubmitMs);
}

async function ensureConversationAligned(page, args, conversationId, actions, action) {
  const before = await collectSessionState(page, args);
  const beforeAlignment = sessionAuthorityAlignment(before, conversationId);
  if (beforeAlignment.ok) {
    actions.push({ action, conversationId, aligned: true, repaired: false, before: compactSessionState(before), alignment: beforeAlignment });
    return before;
  }
  const repair = await realignFreshSession(page, args, conversationId);
  const after = await collectSessionState(page, args);
  const afterAlignment = sessionAuthorityAlignment(after, conversationId);
  actions.push({ action, conversationId, aligned: afterAlignment.ok, repaired: true, before: compactSessionState(before), after: compactSessionState(after), beforeAlignment, afterAlignment, repair });
  if (!afterAlignment.ok) throw new Error(`${action} failed: ${JSON.stringify({ conversationId, reason: afterAlignment.reason, before: compactSessionState(before), after: compactSessionState(after), beforeAlignment, afterAlignment, repair })}`);
  return after;
}

async function sampleTraceTimeline(page, args, actions) {
  const samples = [];
  for (let index = 0; index < args.traceSampleCount; index += 1) {
    if (index > 0 && args.traceSampleIntervalMs > 0) await page.waitForTimeout(args.traceSampleIntervalMs);
    samples.push(await collectTraceTimelineSample(page, args, index));
  }
  actions.push({ action: "trace-interval-samples", count: samples.length, intervalMs: args.traceSampleIntervalMs, samples });
  return samples;
}

async function collectTraceTimelineSample(page, args, index) {
  const dom = await page.evaluate((sampleIndex) => {
    const firstTraceId = (...sources) => {
      for (const source of sources) {
        const match = String(source ?? "").match(/\btrc_[A-Za-z0-9_.:-]+\b/u);
        if (match) return match[0];
      }
      return null;
    };
    const cardRole = (card) => card.getAttribute("data-role") ?? card.getAttribute("data-message-role") ?? [...card.classList].find((item) => item === "message-agent" || item === "message-user" || item === "message-system")?.replace(/^message-/, "") ?? null;
    const cardStatus = (card) => card.getAttribute("data-status") ?? card.getAttribute("data-message-status") ?? null;
    const cards = [...document.querySelectorAll(".message-card")];
    const latestAgent = [...cards].reverse().find((card) => cardRole(card) === "agent" || card.classList.contains("message-agent"));
    const trace = latestAgent?.querySelector(".trace-timeline") ?? null;
    const details = trace?.querySelector("details") ?? null;
    const rows = latestAgent ? [...latestAgent.querySelectorAll(".trace-render-row")] : [];
    const rowPreview = (row) => row?.textContent?.replace(/\s+/gu, " ").trim().slice(0, 240) ?? null;
    const rowSummary = (row) => row ? {
      rowId: row.getAttribute("data-row-id") ?? null,
      kind: row.getAttribute("data-row-kind") ?? null,
      terminal: row.getAttribute("data-terminal") === "true",
      status: row.getAttribute("data-event-status") ?? null,
      preview: rowPreview(row)
    } : null;
    const isUserVisibleOutputRow = (row) => {
      const rowId = row.getAttribute("data-row-id") ?? "";
      const kind = row.getAttribute("data-row-kind") ?? "";
      const text = row.textContent?.replace(/\s+/gu, " ").trim() ?? "";
      if (kind === "tool" || rowId.startsWith("tool:")) return true;
      if (rowId.startsWith("trace-final-response:")) return true;
      if (/助手(?:最终)?消息|assistant (?:final )?message|final response/iu.test(text)) return true;
      if (row.getAttribute("data-terminal") === "true" && row.querySelector(".trace-row-body")?.textContent?.trim()) return true;
      return false;
    };
    const latestRow = rows.at(-1) ?? null;
    const userVisibleRows = rows.filter(isUserVisibleOutputRow);
    const latestUserVisibleRow = userVisibleRows.at(-1) ?? null;
    const empty = latestAgent?.querySelector(".trace-empty") ?? null;
    const finalResponse = latestAgent?.querySelector(".message-final-response") ?? null;
    const modeText = document.querySelector(".composer-mode")?.textContent?.trim() ?? null;
    const traceId = firstTraceId(latestAgent?.getAttribute("data-trace-id"), trace?.getAttribute("data-trace-id"), trace?.getAttribute("data-traceid"), modeText, latestAgent?.textContent, trace?.textContent);
    return {
      index: sampleIndex,
      sampledAt: new Date().toISOString(),
      messageCount: cards.length,
      agentStatus: latestAgent ? cardStatus(latestAgent) : null,
      traceId,
      modeText,
      tracePresent: Boolean(trace),
      traceStatus: trace?.getAttribute("data-status") ?? null,
      traceOpen: details instanceof HTMLDetailsElement ? details.open : null,
      rowCount: rows.length,
      emptyLabel: empty?.textContent?.replace(/\s+/gu, " ").trim() ?? null,
      latestRowPreview: rowPreview(latestRow),
      latestRow: rowSummary(latestRow),
      userVisibleRowCount: userVisibleRows.length,
      latestUserVisibleRowPreview: rowPreview(latestUserVisibleRow),
      latestUserVisibleRow: rowSummary(latestUserVisibleRow),
      finalTextChars: finalResponse?.textContent?.trim().length ?? 0
    };
  }, index);
  const session = await collectSessionState(page, args);
  const conversationId = firstNonEmpty(session.routeConversationId, session.api?.workspace?.selectedConversationId, session.activeTab?.conversationId);
  const conversation = conversationId ? session.api?.conversations?.items?.find((item) => item.conversationId === conversationId || item.sessionId === conversationId) ?? null : null;
  const traceId = firstNonEmpty(dom.traceId, conversation?.lastTraceId, session.api?.workspace?.activeTraceId);
  const traceProjectId = normalizeProbeProjectId(conversation?.projectId) ?? normalizeProbeProjectId(session.api?.workspace?.projectId) ?? normalizeProbeProjectId(args.projectId);
  const traceFetch = traceId
    ? await fetchTraceSummaryWithRetry(page, args, traceId, traceProjectId)
    : { ok: false, traceId: null, degradedReason: "trace-id-missing", attempts: [] };
  return { ...dom, conversationId, projectId: traceProjectId, sessionId: conversation?.sessionId ?? session.activeTab?.sessionId ?? null, traceId, traceFetch };
}

async function fetchTraceSummaryWithRetry(page, args, traceId, projectId) {
  const paths = [traceSummaryPath(traceId, projectId)];
  const attempts = [];
  for (const candidate of paths) {
    for (let attempt = 1; attempt <= 3; attempt += 1) {
      const fetched = await fetchJsonFromPage(page, candidate.path, { timeoutMs: 8000 });
      const summary = summarizeTraceFetch(fetched, traceId, attempt, candidate);
      attempts.push(summary);
      if (summary.ok) return { ...summary, attempts };
      if (summary.errorCode === "trace_project_mismatch" && candidate.projectId) break;
      if (attempt < 3) await page.waitForTimeout(500 * attempt);
    }
  }
  const last = attempts.at(-1) ?? { ok: false, traceId, degradedReason: "trace-fetch-failed" };
  return { ...last, attempts, degradedReason: last.degradedReason ?? "trace-fetch-failed" };
}

function traceSummaryPath(traceId, projectId) {
  const params = new URLSearchParams({ sinceSeq: "0", limit: "20" });
  return { path: `/v1/workbench/traces/${encodeURIComponent(traceId)}/events?${params.toString()}`, projectId, scoped: Boolean(projectId) };
}

function summarizeTraceFetch(fetchResult, expectedTraceId, attempt, request) {
  const body = fetchResult?.json?.data ?? fetchResult?.json ?? null;
  const trace = body?.trace ?? body?.runnerTrace ?? body;
  const events = Array.isArray(trace?.events) ? trace.events : Array.isArray(body?.events) ? body.events : [];
  const latest = events.at(-1) ?? null;
  const ok = fetchResult?.ok === true;
  return {
    ok,
    attempt,
    requestProjectId: request?.projectId ?? null,
    requestScoped: request?.scoped === true,
    path: fetchResult?.path ?? null,
    httpStatus: fetchResult?.status ?? null,
    elapsedMs: fetchResult?.elapsedMs ?? null,
    traceId: trace?.traceId ?? body?.traceId ?? expectedTraceId,
    traceStatus: trace?.traceStatus ?? trace?.status ?? body?.traceStatus ?? body?.status ?? null,
    running: trace?.running ?? body?.running ?? null,
    terminal: trace?.terminal ?? body?.terminal ?? null,
    eventCount: events.length,
    firstEventAt: events[0]?.createdAt ?? events[0]?.at ?? null,
    latestEventAt: latest?.createdAt ?? latest?.at ?? null,
    latestEventPreview: latest ? JSON.stringify(latest).replace(/\s+/gu, " ").slice(0, 300) : null,
    error: fetchResult?.error ?? null,
    errorCode: body?.error?.code ?? null,
    bodyPreview: ok ? null : fetchResult?.bodyPreview ?? null,
    degradedReason: ok ? null : fetchResult?.status ? "trace-fetch-http-status" : "trace-fetch-failed"
  };
}

function summarizeSessionRun(actions, dom) {
  const fresh = [...actions].reverse().find((item) => item.action === "fresh-session") ?? null;
  const alignment = fresh?.alignment?.check ?? null;
  const sessionRail = dom?.sessionRail ?? null;
  const active = sessionRail?.active ?? null;
  return {
    freshSessionRequested: Boolean(fresh),
    freshSessionAligned: fresh ? fresh.aligned === true : null,
    conversationId: firstNonEmpty(alignment?.conversationId, active?.conversationId, dom?.routeConversationId),
    routeConversationId: alignment?.routeConversationId ?? dom?.routeConversationId ?? null,
    selectedConversationId: alignment?.selectedConversationId ?? null,
    sessionId: active?.sessionId ?? null,
    revision: fresh?.after?.api?.workspace?.revision ?? null,
    messageCount: dom?.messageCount ?? null,
    degradedReason: fresh && fresh.aligned !== true ? fresh.alignment?.reason ?? "fresh-session-not-aligned" : null,
    freshSession: fresh ? {
      settled: fresh.settled === true,
      aligned: fresh.aligned === true,
      reason: fresh.alignment?.reason ?? null,
      before: compactSessionState(fresh.before),
      after: compactSessionState(fresh.after)
    } : null
  };
}

function compactSessionState(state) {
  if (!state) return null;
  return {
    finalUrl: state.finalUrl ?? null,
    routeConversationId: state.routeConversationId ?? null,
    selectedConversationId: state.api?.workspace?.selectedConversationId ?? null,
    revision: state.api?.workspace?.revision ?? null,
    activeTab: state.activeTab ?? null,
    sessionTabs: Array.isArray(state.sessionTabs) ? state.sessionTabs.slice(0, 6) : [],
    messageCount: state.messageCount ?? null,
    command: state.command ?? null,
    conversationsCount: state.api?.conversations?.count ?? null
  };
}

function summarizeTraceRun(samples, dom, sessionSummary) {
  const firstDomRow = samples.find((sample) => Number(sample.rowCount ?? 0) > 0) ?? null;
  const firstUserVisibleRow = samples.find((sample) => Number(sample.userVisibleRowCount ?? 0) > 0) ?? null;
  const firstRestEvent = samples.find((sample) => Number(sample.traceFetch?.eventCount ?? 0) > 0) ?? null;
  const last = samples.at(-1) ?? null;
  const failedFetches = samples.filter((sample) => sample.traceFetch && sample.traceFetch.ok !== true);
  return {
    requested: samples.length > 0,
    sampleCount: samples.length,
    conversationId: firstNonEmpty(last?.conversationId, sessionSummary?.conversationId),
    sessionId: firstNonEmpty(last?.sessionId, sessionSummary?.sessionId),
    traceId: firstNonEmpty(last?.traceId, firstRestEvent?.traceId, dom?.sessionRail?.traceIds?.[0]),
    finalAgentStatus: last?.agentStatus ?? null,
    finalTraceStatus: last?.traceStatus ?? null,
    finalDomRowCount: last?.rowCount ?? null,
    finalUserVisibleDomRowCount: last?.userVisibleRowCount ?? null,
    firstDomRowVisibleAt: firstDomRow?.sampledAt ?? null,
    firstDomRowPreview: firstDomRow?.latestRowPreview ?? null,
    firstUserVisibleRowAt: firstUserVisibleRow?.sampledAt ?? null,
    firstUserVisibleRowPreview: firstUserVisibleRow?.latestUserVisibleRowPreview ?? null,
    firstUserVisibleRow: firstUserVisibleRow?.latestUserVisibleRow ?? null,
    firstRestEventVisibleAt: firstRestEvent?.sampledAt ?? null,
    firstRestEventCount: firstRestEvent?.traceFetch?.eventCount ?? null,
    restTraceOk: samples.some((sample) => sample.traceFetch?.ok === true),
    failedFetchCount: failedFetches.length,
    latestFetch: last?.traceFetch ?? null,
    degradedReason: samples.length === 0 ? null : failedFetches.length === samples.length ? failedFetches.at(-1)?.traceFetch?.degradedReason ?? "trace-fetch-failed" : null
  };
}

function summarizeProbePerformance(actions, startedAt, samples, traceSummary) {
  const submit = [...actions].reverse().find((item) => item.action === "submit-prompt") ?? null;
  const submittedAt = submit?.submittedAt ?? null;
  return {
    startedAt,
    submittedAt,
    firstDomTraceRowAt: traceSummary?.firstDomRowVisibleAt ?? null,
    firstUserVisibleTraceRowAt: traceSummary?.firstUserVisibleRowAt ?? null,
    firstRestTraceEventAt: traceSummary?.firstRestEventVisibleAt ?? null,
    submitToFirstDomTraceRowMs: submittedAt && traceSummary?.firstDomRowVisibleAt ? Math.max(0, new Date(traceSummary.firstDomRowVisibleAt).getTime() - new Date(submittedAt).getTime()) : null,
    submitToFirstUserVisibleTraceRowMs: submittedAt && traceSummary?.firstUserVisibleRowAt ? Math.max(0, new Date(traceSummary.firstUserVisibleRowAt).getTime() - new Date(submittedAt).getTime()) : null,
    submitToFirstRestTraceEventMs: submittedAt && traceSummary?.firstRestEventVisibleAt ? Math.max(0, new Date(traceSummary.firstRestEventVisibleAt).getTime() - new Date(submittedAt).getTime()) : null,
    totalSampleWindowMs: samples.length >= 2 ? Math.max(0, new Date(samples.at(-1).sampledAt).getTime() - new Date(samples[0].sampledAt).getTime()) : 0
  };
}

function runProbeDegradedReason({ traceSummary, sessionSummary, promptValidation, actions }) {
  if (sessionSummary?.degradedReason) return sessionSummary.degradedReason;
  if (traceSummary?.degradedReason) return traceSummary.degradedReason;
  const terminalWait = Array.isArray(actions) ? [...actions].reverse().find((item) => item.action === "wait-agent-terminal") : null;
  if (terminalWait?.terminal === false) return terminalWait.degradedReason ?? "agent-terminal-timeout";
  if (promptValidation?.ok === false) return "prompt-validation-failed";
  return null;
}

function classifyRunProbeError(error, context = {}) {
  const message = error instanceof Error ? error.message : String(error);
  const finalUrl = typeof context.finalUrl === "string" ? context.finalUrl : "";
  const failureDom = context.failureDom && typeof context.failureDom === "object" ? context.failureDom : {};
  const login = failureDom.login && typeof failureDom.login === "object" ? failureDom.login : {};
  if (/\/login(?:\?|$)/u.test(finalUrl) || failureDom.authState === "login" || login.cardVisible === true) return "auth-redirect-login";
  if (/session_required|session-not-selected/u.test(message)) return "session-not-selected";
  if (/pre-submit-realign|session-(?:route|workspace|active-tab)-not-aligned|route-session/u.test(message)) return "session-not-selected";
  if (/composer|command-send|command bar|disabledReason|button_disabled/u.test(message)) return "composer-disabled";
  if (/fresh session/u.test(message)) return "fresh-session-not-aligned";
  if (/agent final response did not reach terminal DOM state/u.test(message)) return "agent-terminal-timeout";
  if (/login|auth/u.test(message)) return "auth-login-failed";
  if (/Timeout|timed out|timeout/u.test(message)) return "browser-timeout";
  if (/fetch/u.test(message)) return "trace-fetch-failed";
  return "web-probe-failed";
}

function firstNonEmpty(...values) {
  for (const value of values) {
    if (typeof value === "string" && value.trim()) return value;
  }
  return null;
}

function normalizeProbeProjectId(value) {
  const text = typeof value === "string" ? value.trim() : "";
  return /^prj_[A-Za-z0-9_.:-]{3,96}$/u.test(text) ? text : null;
}

async function waitForCommandReady(page, args, actions) {
  const timeoutMs = Math.min(args.timeoutMs, 30000);
  const before = await collectCommandState(page);
  const ready = await page.waitForFunction(() => {
    const input = document.querySelector("#command-input");
    const send = document.querySelector("#command-send");
    const warning = document.querySelector(".composer-warning")?.textContent?.trim() ?? "";
    return Boolean(input && send && !input.disabled && !warning);
  }, null, { timeout: timeoutMs }).catch(() => null);
  const after = await collectCommandState(page);
  const result = { action: "command-ready", ready: Boolean(ready), timeoutMs, before, after };
  actions.push(result);
  if (!ready) throw new Error(`command composer is not ready for submit: ${JSON.stringify(result)}`);
  return after;
}

async function collectCommandState(page) {
  return page.evaluate(() => {
    const input = document.querySelector("#command-input");
    const send = document.querySelector("#command-send");
    const form = document.querySelector("#command-form");
    return {
      inputVisible: Boolean(input),
      inputDisabled: input ? Boolean(input.disabled) : null,
      inputLength: input && typeof input.value === "string" ? input.value.length : null,
      sendVisible: Boolean(send),
      sendDisabled: send ? Boolean(send.disabled) : null,
      sendAction: send?.getAttribute("data-action") ?? null,
      disabledReason: document.querySelector(".composer-warning")?.textContent?.trim() ?? null,
      modeText: document.querySelector(".composer-mode")?.textContent?.trim() ?? null,
      sendText: send?.textContent?.trim() ?? null,
      formTitle: form?.getAttribute("title") ?? null,
      messageCount: document.querySelectorAll(".message-card").length,
      authState: document.body.getAttribute("data-auth-state")
    };
  });
}

async function selectConversationInBrowserSession(page, args, actions) {
  const authSession = await page.evaluate(async () => {
    const response = await fetch("/auth/session", { credentials: "same-origin" });
    const body = await response.json().catch(() => null);
    return {
      status: response.status,
      authenticated: body?.authenticated ?? null,
      userId: body?.user?.id ?? body?.actor?.id ?? null,
      username: body?.user?.username ?? body?.actor?.username ?? null,
      role: body?.user?.role ?? body?.actor?.role ?? null,
      accountStatus: body?.user?.status ?? body?.actor?.status ?? null
    };
  });
  const result = await selectSessionViaRoute(page, args, args.conversationId, "web-live-dom-probe");
  actions.push({ action: "select-session", authSession, ...result });
  if (!result.ok) throw new Error(`browser session route selection failed: ${JSON.stringify(result)}`);
}

async function waitForMessageCards(page, args, actions) {
  if (!args.waitMessagesMs) return;
  const before = await page.locator(".message-card").count().catch(() => 0);
  const found = await page.waitForFunction(() => document.querySelectorAll(".message-card").length > 0, null, { timeout: args.waitMessagesMs }).catch(() => null);
  const after = await page.locator(".message-card").count().catch(() => 0);
  actions.push({ action: "wait-message-cards", before, after, found: Boolean(found), timeoutMs: args.waitMessagesMs });
}

async function waitForAgentTerminal(page, args, actions) {
  const timeoutMs = Math.min(args.waitAgentTerminalMs, args.timeoutMs);
  const before = await collectLatestAgentMessageState(page);
  const terminal = await page.waitForFunction(() => {
    const cardRole = (card) => card.getAttribute("data-role") ?? card.getAttribute("data-message-role") ?? [...card.classList].find((item) => item === "message-agent" || item === "message-user" || item === "message-system")?.replace(/^message-/, "") ?? null;
    const cardStatus = (card) => card.getAttribute("data-status") ?? card.getAttribute("data-message-status") ?? null;
    const cards = [...document.querySelectorAll(".message-card")];
    const latestAgent = [...cards].reverse().find((card) => cardRole(card) === "agent" || card.classList.contains("message-agent"));
    if (!latestAgent) return false;
    const status = cardStatus(latestAgent) ?? "";
    const finalResponse = latestAgent.querySelector(".message-final-response");
    const text = finalResponse?.textContent?.trim() ?? "";
    return status && status !== "running" && text.length > 0;
  }, null, { timeout: timeoutMs }).catch(() => null);
  const after = await collectLatestAgentMessageState(page);
  actions.push({ action: "wait-agent-terminal", terminal: Boolean(terminal), timeoutMs, before, after, degradedReason: terminal ? null : "agent-terminal-timeout" });
  return Boolean(terminal);
}

async function collectLatestAgentMessageState(page) {
  return page.evaluate(() => {
    const firstTraceId = (...sources) => {
      for (const source of sources) {
        const match = String(source ?? "").match(/\btrc_[A-Za-z0-9_.:-]+\b/u);
        if (match) return match[0];
      }
      return null;
    };
    const cardRole = (card) => card.getAttribute("data-role") ?? card.getAttribute("data-message-role") ?? [...card.classList].find((item) => item === "message-agent" || item === "message-user" || item === "message-system")?.replace(/^message-/, "") ?? null;
    const cardStatus = (card) => card.getAttribute("data-status") ?? card.getAttribute("data-message-status") ?? null;
    const cards = [...document.querySelectorAll(".message-card")];
    const latestAgent = [...cards].reverse().find((card) => cardRole(card) === "agent" || card.classList.contains("message-agent"));
    const trace = latestAgent?.querySelector(".trace-timeline, .message-trace") ?? null;
    const finalResponse = latestAgent?.querySelector(".message-final-response");
    const modeText = document.querySelector(".composer-mode")?.textContent?.trim() ?? null;
    return {
      messageCount: cards.length,
      status: latestAgent ? cardStatus(latestAgent) : null,
      traceId: firstTraceId(latestAgent?.getAttribute("data-trace-id"), trace?.getAttribute("data-trace-id"), trace?.getAttribute("data-traceid"), modeText, latestAgent?.textContent, trace?.textContent),
      modeText,
      textChars: finalResponse?.textContent?.trim().length ?? 0,
      textPreview: (finalResponse?.textContent ?? "").replace(/\s+/gu, " ").trim().slice(0, 240)
    };
  });
}

async function collectDomEvidence(page, args) {
  return page.evaluate(async ({ projectId }) => {
    const firstTraceId = (...sources) => {
      for (const source of sources) {
        const match = String(source ?? "").match(/\btrc_[A-Za-z0-9_.:-]+\b/u);
        if (match) return match[0];
      }
      return null;
    };
    const cardRole = (card) => card.getAttribute("data-role") ?? card.getAttribute("data-message-role") ?? [...card.classList].find((item) => item === "message-user" || item === "message-agent" || item === "message-system")?.replace(/^message-/, "") ?? null;
    const cardStatus = (card) => card.getAttribute("data-status") ?? card.getAttribute("data-message-status") ?? null;
    const workspace = document.querySelector("#workspace");
    const list = document.querySelector("#conversation-list");
    const cards = [...document.querySelectorAll(".message-card")];
    const sessionsResponse = await fetch("/v1/workbench/sessions?limit=50", { credentials: "same-origin" }).catch(() => null);
    const sessionsBody = sessionsResponse ? await sessionsResponse.json().catch(() => null) : null;
    const sessions = Array.isArray(sessionsBody?.sessions)
      ? sessionsBody.sessions
      : Array.isArray(sessionsBody?.data?.sessions)
        ? sessionsBody.data.sessions
        : Array.isArray(sessionsBody?.items)
          ? sessionsBody.items
          : Array.isArray(sessionsBody?.data?.items)
            ? sessionsBody.data.items
            : [];
    const sessionTabs = [...document.querySelectorAll("#session-tabs .session-tab")];
    const activeSessionTab = sessionTabs.find((item) => item.getAttribute("data-active") === "true") ?? null;
    const routeMatch = window.location.pathname.match(/\/workbench\/sessions\/([^/]+)/u) ?? window.location.pathname.match(/\/workspace\/sessions\/([^/]+)/u);
    const sessionIds = sessions.map((item) => item?.sessionId ?? item?.id).filter((value) => typeof value === "string" && value).slice(0, 8);
    const sessionTraceIds = sessions.map((item) => item?.lastTraceId ?? item?.traceId).filter((value) => typeof value === "string" && value);
    const domTraceIds = cards.map((card) => {
      const trace = card.querySelector(".trace-timeline, .message-trace");
      return firstTraceId(card.getAttribute("data-trace-id"), trace?.getAttribute("data-trace-id"), trace?.getAttribute("data-traceid"), card.textContent, trace?.textContent, document.querySelector(".composer-mode")?.textContent);
    }).filter((value) => typeof value === "string" && value);
    const traceIds = [...new Set([...sessionTraceIds, ...domTraceIds])].slice(0, 8);
    const scroll = workspace ? {
      scrollTop: Math.round(workspace.scrollTop),
      scrollHeight: Math.round(workspace.scrollHeight),
      clientHeight: Math.round(workspace.clientHeight),
      overflowY: getComputedStyle(workspace).overflowY,
      followState: workspace.getAttribute("data-scroll-follow")
    } : null;
    return {
      title: document.title,
      authState: document.body.getAttribute("data-auth-state"),
      routeConversationId: routeMatch ? decodeURIComponent(routeMatch[1]) : null,
      requiredSelectors: {
        workspace: Boolean(workspace),
        commandInput: Boolean(document.querySelector("#command-input")),
        commandSend: Boolean(document.querySelector("#command-send")),
        conversationList: Boolean(list),
        sessionCreate: Boolean(document.querySelector("#session-create"))
      },
      sessionRail: {
        tabCount: sessionTabs.length,
        activeTitle: activeSessionTab?.querySelector("strong")?.textContent?.trim() ?? null,
        active: activeSessionTab ? {
          title: activeSessionTab.querySelector("strong")?.textContent?.trim() ?? activeSessionTab.getAttribute("title") ?? null,
          conversationId: activeSessionTab.getAttribute("data-conversation-id") ?? null,
          sessionId: activeSessionTab.getAttribute("data-session-id") ?? null,
          status: activeSessionTab.getAttribute("data-status") ?? null,
          running: activeSessionTab.getAttribute("data-running") ?? null
        } : null,
        objectTitleCount: sessionTabs.map((item) => item.querySelector("strong")?.textContent?.trim() ?? "").filter((text) => text.includes("[object Object]")).length,
        titles: sessionTabs.map((item) => item.querySelector("strong")?.textContent?.trim() ?? "").slice(0, 8),
        sessionIds,
        traceIds,
        sessionsStatus: sessionsResponse?.status ?? null,
        sessionsCount: sessions.length
      },
      messageCount: cards.length,
      messageRoles: cards.map((card) => cardRole(card) ?? "unknown"),
      messages: cards.map((card) => {
        const trace = card.querySelector(".trace-timeline, .message-trace");
        const traceEvents = card.querySelector(".trace-disclosure-body ol, .message-trace-events");
        const finalResponse = card.querySelector(".message-final-response, .message-text");
        const traceStyle = traceEvents ? getComputedStyle(traceEvents) : null;
        const finalStyle = finalResponse ? getComputedStyle(finalResponse) : null;
        const traceRect = traceEvents?.getBoundingClientRect();
        const finalRect = finalResponse?.getBoundingClientRect();
        const finalBody = finalResponse?.querySelector(".message-body");
        const traceId = firstTraceId(card.getAttribute("data-trace-id"), trace?.getAttribute("data-trace-id"), trace?.getAttribute("data-traceid"), card.textContent, trace?.textContent, document.querySelector(".composer-mode")?.textContent);
        return {
          status: cardStatus(card),
          role: cardRole(card),
          traceId,
          className: card.className,
          trace: trace ? {
            present: true,
            traceId,
            open: trace instanceof HTMLDetailsElement ? trace.open : null,
            status: trace.getAttribute("data-trace-status"),
            mode: trace.getAttribute("data-trace-mode"),
            maxHeight: traceStyle?.maxHeight ?? null,
            overflowY: traceStyle?.overflowY ?? null,
            height: traceRect ? Math.round(traceRect.height) : null,
            scrollHeight: traceEvents ? Math.round(traceEvents.scrollHeight) : null
          } : { present: false },
          finalResponse: finalResponse ? {
            present: true,
            maxHeight: finalStyle?.maxHeight ?? null,
            overflowY: finalStyle?.overflowY ?? null,
            height: finalRect ? Math.round(finalRect.height) : null,
            scrollHeight: Math.round(finalResponse.scrollHeight),
            textChars: finalResponse.textContent?.trim().length ?? 0,
            textPreview: (finalResponse.textContent ?? "").replace(/\s+/gu, " ").trim().slice(0, 500),
            markdown: finalBody ? {
              paragraphs: finalBody.querySelectorAll("p").length,
              headings: finalBody.querySelectorAll("h1,h2,h3,h4,h5,h6").length,
              lists: finalBody.querySelectorAll("ul,ol").length,
              listItems: finalBody.querySelectorAll("li").length,
              codeBlocks: finalBody.querySelectorAll("pre code").length,
              inlineCode: finalBody.querySelectorAll("p code,li code").length,
              tables: finalBody.querySelectorAll("table").length
            } : null
          } : { present: false }
        };
      }),
      scroll,
      textPreview: (document.body.textContent ?? "").replace(/\s+/gu, " ").trim().slice(0, 800)
    };
  }, { projectId: args.projectId });
}

function observeBootstrapRoutes(page) {
  const routes = [];
  page.on("request", (request) => {
    try {
      const url = new URL(request.url());
      if (!["/auth/bootstrap", "/auth/session", "/auth/workspace-bootstrap", "/v1/workbench/sessions"].includes(url.pathname)) return;
      routes.push({ method: request.method(), path: url.pathname });
    } catch {
      // Ignore malformed browser-internal URLs.
    }
  });
  return routes;
}

function summarizeBootstrapRoutes(routes) {
  const paths = routes.map((route) => route.path);
  return {
    authBootstrapRequested: paths.includes("/auth/bootstrap"),
    authSessionRequested: paths.includes("/auth/session"),
    workbenchSessionsRequested: paths.includes("/v1/workbench/sessions"),
    legacyWorkspaceBootstrapRequested: paths.includes("/auth/workspace-bootstrap"),
    routes: routes.slice(0, 20)
  };
}

async function fileSha256(file) {
  if (!file || !fs.existsSync(file)) return null;
  return createHash("sha256").update(await readFile(file)).digest("hex");
}

function parseViewport(value) {
  const [width, height] = String(value).split("x").map(Number);
  if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) throw new Error(`invalid --viewport: ${value}`);
  return { width, height };
}

function positiveInteger(value, label) {
  const parsed = Number(value);
  if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${label} must be a positive integer`);
  return parsed;
}

function nonNegativeInteger(value, label) {
  const parsed = Number(value);
  if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${label} must be a non-negative integer`);
  return parsed;
}

function envNonNegativeInteger(name, fallback) {
  const parsed = Number(process.env[name] ?? fallback);
  return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
}

function processAlive(pid) {
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}

async function readTail(file, maxChars = 4000) {
  if (!file || !fs.existsSync(file)) return "";
  const text = await readFile(file, "utf8");
  return text.length > maxChars ? text.slice(-maxChars) : text;
}

function helpPayload() {
  return {
    ok: true,
    status: "usage",
    command: "web-live-dom-probe",
    usage: [
      "node scripts/web-live-dom-probe.mjs run --url http://74.48.78.17:19666/ --fresh-session --report /tmp/hwlab-dev-gate/web-live-dom-probe.json",
      "node scripts/web-live-dom-probe.mjs start --url http://74.48.78.17:19666/ --fresh-session --message '...' --no-cancel-running --wait-agent-terminal-ms 180000",
      "node scripts/web-live-dom-probe.mjs run --url https://hwlab.pikapython.com/ --fresh-session --message '...' --no-cancel-running --trace-sample-count 6 --trace-sample-interval-ms 5000",
      "node scripts/web-live-dom-probe.mjs run --url http://74.48.78.17:19666/#/workspace --conversation-id cnv_...",
      "node scripts/web-live-dom-probe.mjs start --url http://74.48.78.17:19666/ --fresh-session",
      "node scripts/web-live-dom-probe.mjs status <jobId>"
    ],
    output: "JSON with browser launch plan, DOM selectors, report/screenshot paths, and short-poll status metadata."
  };
}
