// SPEC: PJ2026-01040111 long-running Workbench observation.
// Responsibility: Runner page sampling, screenshots, DOM digest, response body summaries, and command record source fragment.

export const MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR = [
  '[data-testid="mdtodo-task-tree-facts"]',
  '[data-testid="mdtodo-task-page-fact"]',
  "[data-task-ref]",
  '[role="treeitem"]',
  '[role="listitem"]',
  "li",
  "button",
  "input",
  "select",
  ".task-row-shell",
  ".task-tools",
].join(", ");

export function nodeWebObserveRunnerSamplingSource(): string {
  return String.raw`async function preflightSummary() {
  return { currentUrl: currentPageUrl(), title: await page.title().catch(() => null), pageId, auth: publicAuth(auth) };
}

async function samplePage(reason, options = {}) {
  if (options?.refreshObserver !== false) await maybeRefreshObserverPage(reason);
  const groupSeq = sampleSeq + 1;
  if (page && !page.isClosed()) {
    await sampleOnePage(page, { reason, groupSeq, pageRole: "control", targetPageId: pageId, pageEpoch: controlPageEpoch })
      .catch((error) => appendJsonl(files.errors, eventRecord("control-sample-error", { pageRole: "control", pageId, pageEpoch: controlPageEpoch, error: errorSummary(error) })));
    await drainPagePerformanceEvents(page, { reason, groupSeq, pageRole: "control", targetPageId: pageId, pageEpoch: controlPageEpoch })
      .catch((error) => appendJsonl(files.errors, eventRecord("control-performance-drain-error", { pageRole: "control", pageId, pageEpoch: controlPageEpoch, error: errorSummary(error) })));
  }
  if (observerPage && !observerPage.isClosed()) {
    await sampleOnePage(observerPage, { reason, groupSeq, pageRole: "observer", targetPageId: observerPageId, pageEpoch: observerPageEpoch }).catch((error) => appendJsonl(files.errors, eventRecord("observer-sample-error", { pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, error: errorSummary(error) })));
    await drainPagePerformanceEvents(observerPage, { reason, groupSeq, pageRole: "observer", targetPageId: observerPageId, pageEpoch: observerPageEpoch })
      .catch((error) => appendJsonl(files.errors, eventRecord("observer-performance-drain-error", { pageRole: "observer", pageId: observerPageId, pageEpoch: observerPageEpoch, error: errorSummary(error) })));
  }
  if (options?.screenshot !== false && screenshotIntervalMs > 0 && Date.now() - lastScreenshotAtMs >= screenshotIntervalMs) {
    await captureScreenshot("checkpoint", "jpeg")
      .catch((error) => appendJsonl(files.errors, eventRecord("screenshot-error", { pageRole: "control", pageId, error: errorSummary(error) })));
  }
  await writeHeartbeat({ status: terminalStatus });
}

async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPageId, pageEpoch }) {
  sampleSeq += 1;
  const evaluateTimeoutMs = Math.max(3000, Math.min(8000, Number(sampleIntervalMs) || 5000));
  const dom = await withHardTimeout(targetPage.evaluate((input) => {
    const trim = (value, limit = 500) => String(value || "").replace(/\s+/g, " ").trim().slice(0, limit);
    const visible = (element) => {
      if (!element) return false;
      const rect = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
    };
    const textHashInput = (element) => trim(element.textContent || "", 800);
    const loadingTextPattern = /加载中|\bLoading\b/iu;
    const loadingUiPattern = /loading-state|loading-spinner|spinner|progress|skeleton|busy|pending/iu;
    const codeLikeSelector = "pre,code,.trace-row-body,.trace-row-markdown,.markdown-body,.message-text,[class*='trace-row' i],[class*='terminal' i],[class*='log' i],[class*='output' i]";
    const elementTextForLoading = (element) => [
      element.textContent || "",
      element.getAttribute("aria-label") || "",
      element.getAttribute("title") || "",
      element.getAttribute("data-testid") || ""
    ].map((value) => trim(value, 240)).filter(Boolean).join(" ");
    const elementLooksLikeLoadingUi = (element) => {
      const signal = [
        element.getAttribute("class") || "",
        element.getAttribute("data-testid") || "",
        element.getAttribute("role") || "",
        element.getAttribute("aria-busy") || "",
        element.getAttribute("aria-label") || "",
        element.getAttribute("title") || ""
      ].join(" ");
      return element.getAttribute("aria-busy") === "true" || element.getAttribute("role") === "status" || loadingUiPattern.test(signal);
    };
    const elementIsCodeLike = (element) => Boolean(element.closest(codeLikeSelector)) && !elementLooksLikeLoadingUi(element);
    const hasLoadingText = (element) => {
      const text = elementTextForLoading(element);
      if (!loadingTextPattern.test(text)) return false;
      if (elementIsCodeLike(element)) return false;
      return true;
    };
    const elementDescriptor = (element) => {
      if (!element) return null;
      const className = String(element.className || "").replace(/\s+/g, " ").trim().split(" ").slice(0, 6).join(" ");
      const identityDescendant = element.querySelector("[data-trace-id], [data-message-id], [data-session-id]");
      return {
        tag: element.tagName.toLowerCase(),
        testId: element.getAttribute("data-testid") || null,
        role: element.getAttribute("role") || null,
        id: element.getAttribute("id") || null,
        className: className || null,
        status: element.getAttribute("data-status") || element.getAttribute("aria-busy") || null,
        sessionId: element.getAttribute("data-session-id") || identityDescendant?.getAttribute("data-session-id") || null,
        messageId: element.getAttribute("data-message-id") || identityDescendant?.getAttribute("data-message-id") || null,
        traceId: element.getAttribute("data-trace-id") || identityDescendant?.getAttribute("data-trace-id") || null,
        ariaLabel: element.getAttribute("aria-label") || null
      };
    };
    const ownerKindFor = (element) => {
      const value = [
        element.getAttribute("data-testid") || "",
        element.getAttribute("class") || "",
        element.getAttribute("role") || "",
        element.tagName || ""
      ].join(" ").toLowerCase();
      if (/message|turn|agent|assistant/.test(value)) return "turn";
      if (/session|rail|sidebar|nav/.test(value)) return "session-nav";
      if (/composer|prompt|input|textarea/.test(value)) return "composer";
      if (/diagnostic|alert|error|warning/.test(value)) return "diagnostic";
      if (/trace|event|terminal|log/.test(value)) return "trace";
      if (/performance|metric|chart|table/.test(value)) return "performance";
      if (/main|workspace|workbench|root/.test(value)) return "workbench";
      return "unknown";
    };
    const ownerLabelFor = (element) => {
      const heading = element.querySelector("h1,h2,h3,h4,[data-testid*='title' i],[class*='title' i],[class*='header' i]");
      return trim(
        element.getAttribute("aria-label")
          || element.getAttribute("data-testid")
          || (heading ? heading.textContent || "" : "")
          || element.getAttribute("class")
          || element.tagName,
        160
      );
    };
    const ownerKeyFor = (element) => {
      const descriptor = elementDescriptor(element) || {};
      return [
        ownerKindFor(element),
        descriptor.testId || descriptor.id || descriptor.role || descriptor.className || descriptor.tag || "unknown",
        descriptor.sessionId || descriptor.messageId || descriptor.traceId || ""
      ].filter(Boolean).join(":").slice(0, 240);
    };
    const collectLoadingNodes = () => {
      const candidates = Array.from(document.querySelectorAll("body *"))
        .filter(visible)
        .filter(hasLoadingText)
        .filter((element) => !Array.from(element.children).some((child) => visible(child) && hasLoadingText(child)))
        .slice(-80);
      return candidates.map((element, index) => {
        const rect = element.getBoundingClientRect();
        const identityOwner = element.closest('[data-message-id], [data-trace-id], [data-session-id]');
        const structuralOwner = element.closest('article.message-card, .message-card, [data-testid*="message" i], [data-testid*="turn" i], [data-testid*="composer" i], [data-testid*="session" i], [class*="composer" i], [class*="session" i], [class*="trace" i], [class*="diagnostic" i], article, section, aside, main, form, [role="status"], [role="alert"], [role="article"], [role="navigation"]');
        const owner = identityOwner || structuralOwner || element;
        const ownerDescriptor = elementDescriptor(owner);
        return {
          index,
          tag: element.tagName.toLowerCase(),
          className: String(element.className || "").slice(0, 180),
          testId: element.getAttribute("data-testid"),
          role: element.getAttribute("role"),
          text: trim(elementTextForLoading(element), 240),
          ownerKind: ownerKindFor(owner),
          ownerKey: ownerKeyFor(owner),
          ownerLabel: ownerLabelFor(owner),
          owner: ownerDescriptor,
          ownerText: trim(owner.textContent || "", 300),
          rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
        };
      });
    };
    const sessionIdForElement = (element) => {
      const direct = element.getAttribute("data-session-id");
      if (direct) return direct;
      const href = element.getAttribute("href") || element.closest("a[href]")?.getAttribute("href") || "";
      if (!href) return null;
      try {
        const parsed = new URL(href, location.href);
        const match = parsed.pathname.match(/\/(?:workbench|workspace)\/sessions\/([^/?#]+)/u);
        return match ? decodeURIComponent(match[1] || "") : null;
      } catch {
        const match = href.match(/\/(?:workbench|workspace)\/sessions\/([^/?#]+)/u);
        return match ? decodeURIComponent(match[1] || "") : null;
      }
    };
    const sessionTitleTextForElement = (element) => {
      const titleNode = element.querySelector("[data-testid*='session-title' i], [data-testid*='session-name' i], [class*='session-title' i], [class*='session-name' i], [data-testid*='title' i], [class*='title' i]");
      return trim(
        (titleNode ? titleNode.textContent || "" : "")
          || element.getAttribute("aria-label")
          || element.getAttribute("title")
          || element.textContent
          || "",
        240
      );
    };
    const sessionTitleFallbackPattern = /^(?:Session\s+)?ses_[A-Za-z0-9_.-]+/iu;
    const looksLikeSessionTitleFallback = (title, sessionId) => {
      const text = trim(title, 240);
      const id = String(sessionId || "").trim();
      if (!text) return true;
      if (sessionTitleFallbackPattern.test(text)) return true;
      if (!id) return false;
      return text === id || text.startsWith(id) || text.startsWith("Session " + id);
    };
    const collectSessionRailTitles = () => {
      const candidates = Array.from(document.querySelectorAll(".session-tab[data-session-id], [role='tab'][data-session-id], [data-testid*='session' i][data-session-id], a[href*='/workbench/sessions/'], a[href*='/workspace/sessions/']"))
        .filter(visible);
      const seen = new Set();
      const items = [];
      for (const element of candidates) {
        const sessionId = sessionIdForElement(element);
        const titleText = sessionTitleTextForElement(element);
        const key = (sessionId || "") + "|" + titleText;
        if (seen.has(key)) continue;
        seen.add(key);
        const rect = element.getBoundingClientRect();
        const fallbackTitle = looksLikeSessionTitleFallback(titleText, sessionId);
        const dataStatus = element.getAttribute("data-status") || element.getAttribute("data-state") || null;
        const dataRunning = element.getAttribute("data-running") || element.getAttribute("data-busy") || null;
        const ariaBusy = element.getAttribute("aria-busy") || null;
        items.push({
          index: items.length,
          tag: element.tagName.toLowerCase(),
          testId: element.getAttribute("data-testid"),
          role: element.getAttribute("role"),
          active: element.getAttribute("data-active") === "true" || element.getAttribute("aria-selected") === "true",
          status: dataStatus || ariaBusy || null,
          dataStatus,
          running: dataRunning === "true" || ariaBusy === "true" || element.classList.contains("is-running") || element.classList.contains("running"),
          dataRunning,
          ariaBusy,
          sessionId,
          sessionIdPrefix: sessionId ? String(sessionId).slice(0, 12) : null,
          titleText,
          fallbackTitle,
          rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
        });
      }
      const visibleItems = items.slice(0, 120);
      const fallbackItems = visibleItems.filter((item) => item.fallbackTitle);
      const visibleCount = visibleItems.length;
      const fallbackTitleCount = fallbackItems.length;
      return {
        visibleCount,
        fallbackTitleCount,
        fallbackTitleRatio: visibleCount > 0 ? Number((fallbackTitleCount / visibleCount).toFixed(4)) : 0,
        activeItem: visibleItems.find((item) => item.active) || null,
        items: visibleItems.slice(0, 60),
        fallbackItems: fallbackItems.slice(0, 12),
      };
    };
    const diagnosticSummaryText = (element) => {
      const summarySelectors = [
        ".api-error-diagnostic-summary-text p",
        ".api-error-diagnostic-summary-text",
        "[class*='diagnostic-summary-text' i] p",
        "[class*='diagnostic-summary-text' i]",
        "[data-testid*='diagnostic-summary' i]",
        "[data-testid*='error-summary' i]",
        "[role='alert'] p",
        "[role='alert']"
      ];
      const parts = [];
      for (const selector of summarySelectors) {
        for (const candidate of Array.from(element.querySelectorAll(selector))) {
          if (!visible(candidate)) continue;
          const text = trim(candidate.textContent || "", 800);
          if (text && !parts.includes(text)) parts.push(text);
        }
      }
      const ownText = textHashInput(element);
      const text = parts.length > 0 ? parts.join(" ") : ownText;
      return text.replace(/\s+(?:!|i诊断|诊断详情)$/u, "").trim();
    };
    const diagnosticToggleOnly = (element, text) => {
      const compact = String(text || "").trim();
      if (!/^(?:!|i诊断|诊断|诊断详情)$/u.test(compact)) return false;
      const tag = element.tagName.toLowerCase();
      const role = element.getAttribute("role");
      const aria = element.getAttribute("aria-label") || "";
      const title = element.getAttribute("title") || "";
      return tag === "button" || role === "button" || /诊断/u.test(aria) || /诊断/u.test(title);
    };
    const messageBodyTextDetail = (element) => {
      const bodySelectors = [
        ".message-markdown.message-text",
        ".message-text",
        "[data-message-body]",
        "[data-testid='message-body']",
        "[data-testid*='message-text' i]",
        "[data-testid*='final-response' i]"
      ];
      const parts = [];
      for (const selector of bodySelectors) {
        for (const candidate of Array.from(element.querySelectorAll(selector))) {
          if (!visible(candidate)) continue;
          const text = trim(candidate.textContent || "", 1200);
          if (text && !parts.some((part) => part.text === text)) parts.push({ text, selector });
        }
      }
      if (parts.length > 0) {
        return {
          text: parts.map((part) => part.text).join(" "),
          source: parts[0]?.selector || "body-selector",
          candidateCount: parts.length
        };
      }
      return { text: "", source: null, candidateCount: 0 };
    };
    const stableMessageText = (element) => {
      const body = messageBodyTextDetail(element);
      if (body.text) return body.text;
      const clone = element.cloneNode(true);
      for (const selector of [
        ".message-duration-meta",
        ".message-activity-meta",
        ".api-error-diagnostic",
        "[class*='diagnostic' i]",
        "[class*='trace' i]",
        "[data-trace-id]",
        "[data-testid*='trace' i]",
        "[data-testid*='event' i]",
        "[role='status']",
        "[role='alert']",
        "button"
      ]) {
        for (const child of Array.from(clone.querySelectorAll(selector))) child.remove();
      }
      return trim(clone.textContent || "", 1200);
    };
    const numericAttr = (element, names) => {
      for (const name of names) {
        const raw = element.getAttribute(name);
        const value = Number(raw);
        if (Number.isFinite(value)) return value;
      }
      return null;
    };
    const textAttr = (element, names) => {
      for (const name of names) {
        const raw = element.getAttribute(name);
        if (raw && String(raw).trim()) return String(raw).trim();
      }
      return null;
    };
    const summarizeElements = (elements, limit) => elements.filter(visible).slice(-limit).map((element, index) => {
      const rect = element.getBoundingClientRect();
      const owner = element.closest('article.message-card, .message-card[data-message-id], article[data-message-id], [data-trace-id]');
      const timeElement = element.matches("time,[datetime]") ? element : element.querySelector("time,[datetime]");
      return {
        index,
        tag: element.tagName.toLowerCase(),
        testId: element.getAttribute("data-testid"),
        role: element.getAttribute("role"),
        status: element.getAttribute("data-status") || element.getAttribute("aria-busy") || null,
        sessionId: element.getAttribute("data-session-id") || owner?.getAttribute("data-session-id") || null,
        messageId: element.getAttribute("data-message-id") || owner?.getAttribute("data-message-id") || element.getAttribute("id") || null,
        traceId: element.getAttribute("data-trace-id") || owner?.getAttribute("data-trace-id") || null,
        turnId: element.getAttribute("data-turn-id") || owner?.getAttribute("data-turn-id") || null,
        projectedSeq: numericAttr(element, ["data-projected-seq", "data-projectedseq", "data-seq", "data-sequence", "aria-posinset"]),
        sourceSeq: numericAttr(element, ["data-source-seq", "data-sourceseq", "data-source-event-seq"]),
        eventSeq: numericAttr(element, ["data-event-seq", "data-eventseq"]),
        eventTimestamp: textAttr(element, ["data-event-ts", "data-event-time", "data-timestamp", "datetime"]) || (timeElement ? textAttr(timeElement, ["datetime", "data-event-ts", "data-event-time", "data-timestamp"]) : null),
        eventTimeText: timeElement ? trim(timeElement.textContent || "", 80) : null,
        eventKind: textAttr(element, ["data-event-kind", "data-kind", "data-label", "data-status"]) || element.getAttribute("aria-label") || null,
        text: textHashInput(element),
        rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
      };
    });
    const summarize = (selector, limit) => summarizeElements(Array.from(document.querySelectorAll(selector)), limit);
    const summarizeMessages = (selector, limit) => Array.from(document.querySelectorAll(selector)).filter(visible).slice(-limit).map((element, index) => {
      const rect = element.getBoundingClientRect();
      const body = messageBodyTextDetail(element);
      return {
        index,
        tag: element.tagName.toLowerCase(),
        testId: element.getAttribute("data-testid"),
        role: element.getAttribute("role"),
        dataRole: element.getAttribute("data-role"),
        status: element.getAttribute("data-status") || element.getAttribute("aria-busy") || null,
        sessionId: element.getAttribute("data-session-id") || null,
        messageId: element.getAttribute("data-message-id") || element.getAttribute("id") || null,
        traceId: element.getAttribute("data-trace-id") || null,
        turnId: element.getAttribute("data-turn-id") || null,
        durationText: trim(element.querySelector(".message-duration-meta")?.textContent || "", 120),
        activityText: trim(element.querySelector(".message-activity-meta")?.textContent || "", 120),
        bodyText: body.text,
        bodyTextSource: body.source,
        bodyTextCandidateCount: body.candidateCount,
        text: stableMessageText(element),
        rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
      };
    });
    const resourceTimingSample = (entry) => ({
      name: entry.name.split(/[?#]/u)[0].slice(0, 240),
      initiatorType: entry.initiatorType,
      startTime: Math.round(entry.startTime),
      duration: Math.round(entry.duration),
      workerStart: Math.round(entry.workerStart || 0),
      redirectStart: Math.round(entry.redirectStart || 0),
      redirectEnd: Math.round(entry.redirectEnd || 0),
      fetchStart: Math.round(entry.fetchStart || 0),
      domainLookupStart: Math.round(entry.domainLookupStart || 0),
      domainLookupEnd: Math.round(entry.domainLookupEnd || 0),
      connectStart: Math.round(entry.connectStart || 0),
      connectEnd: Math.round(entry.connectEnd || 0),
      secureConnectionStart: Math.round(entry.secureConnectionStart || 0),
      requestStart: Math.round(entry.requestStart || 0),
      responseStart: Math.round(entry.responseStart || 0),
      responseEnd: Math.round(entry.responseEnd || 0),
      transferSize: Number.isFinite(Number(entry.transferSize)) ? Number(entry.transferSize) : null,
      encodedBodySize: Number.isFinite(Number(entry.encodedBodySize)) ? Number(entry.encodedBodySize) : null,
      decodedBodySize: Number.isFinite(Number(entry.decodedBodySize)) ? Number(entry.decodedBodySize) : null,
      nextHopProtocol: entry.nextHopProtocol || null,
      responseStatus: Number.isFinite(Number(entry.responseStatus)) ? Number(entry.responseStatus) : null,
      serverTiming: Array.from(entry.serverTiming || []).slice(0, 8).map((item) => ({
        name: String(item.name || "").slice(0, 80),
        duration: Number.isFinite(Number(item.duration)) ? Math.round(Number(item.duration)) : null,
        description: String(item.description || "").slice(0, 120)
      })),
    });
    const opaqueDomId = (value) => String(value || "").trim();
    const collectProjectManagement = () => {
      const config = input?.projectManagement || {};
      const targetPaths = Array.isArray(config.targetPaths) ? config.targetPaths : [];
      const path = location.pathname;
      const configuredPath = targetPaths.some((target) => path === target || path.startsWith(String(target) + "/"));
      const root = document.querySelector('[data-testid="project-management-root"]');
      const mdtodoRoot = document.querySelector('[data-testid="project-management-mdtodo"]');
      const rootVisible = visible(root);
      const mdtodoVisible = visible(mdtodoRoot);
      if (!configuredPath && !rootVisible && !mdtodoVisible) return null;
      const sourceItems = Array.from(document.querySelectorAll('[data-testid="mdtodo-source-list"] [data-source-id], [data-source-id]')).filter(visible);
      const fileItems = Array.from(document.querySelectorAll('[data-testid="mdtodo-file-list"] [data-file-ref], [data-file-ref]')).filter(visible);
      const sourceSelect = document.querySelector('[data-testid="mdtodo-source-select"]');
      const fileSelect = document.querySelector('[data-testid="mdtodo-file-select"]');
      const sourceOptionCount = sourceSelect ? Array.from(sourceSelect.options || []).filter((option) => option.value).length : 0;
      const fileOptionCount = fileSelect ? Array.from(fileSelect.options || []).filter((option) => option.value).length : 0;
      const fileOptions = fileSelect ? Array.from(fileSelect.options || []).filter((option) => option.value).map((option) => ({
        value: option.value,
        label: trim(option.textContent || option.label || "", 180),
        selected: option.selected === true,
      })) : [];
      const selectedFileOption = fileOptions.find((option) => option.selected) || null;
      const taskTree = document.querySelector('[data-testid="mdtodo-task-tree"]');
      const semanticTaskCandidates = taskTree
        ? Array.from(taskTree.querySelectorAll('[role="treeitem"], [role="listitem"]')).filter(visible)
        : [];
      const fallbackTaskCandidates = taskTree
        ? Array.from(taskTree.querySelectorAll("li")).filter(visible)
        : [];
      const taskCandidates = semanticTaskCandidates.length > 0 ? semanticTaskCandidates : fallbackTaskCandidates;
      const taskRefElement = (candidate) => candidate.matches("[data-task-ref]")
        ? candidate
        : candidate.querySelector("[data-task-ref]");
      const taskItems = Array.from(new Set(taskCandidates.map(taskRefElement).filter(visible)));
      const selectedSource = document.querySelector('[data-source-id][data-selected="true"], [data-source-id][aria-selected="true"], [data-source-id].selected, [data-source-id].is-selected');
      const selectedFile = document.querySelector('[data-file-ref][data-selected="true"], [data-file-ref][aria-selected="true"], [data-file-ref].selected, [data-file-ref].is-selected');
      const selectedTask = document.querySelector('[data-task-ref][data-selected="true"], [data-task-ref][aria-selected="true"], [data-task-ref].selected, [data-task-ref].is-selected');
      const statusCounts = {};
      for (const task of taskItems) {
        const status = task.getAttribute("data-task-status") || task.querySelector("[data-state]")?.getAttribute("data-state") || "unknown";
        statusCounts[status] = (statusCounts[status] || 0) + 1;
      }
      const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-launch-workbench"], [data-action="launch-workbench"]');
      const bodyRendered = document.querySelector('[data-testid="mdtodo-body-rendered"], [data-testid="mdtodo-body-read"]');
      const reportPreview = document.querySelector('[data-testid="mdtodo-report-preview"]');
      const reportFullscreen = document.querySelector('[data-testid="mdtodo-report-fullscreen-dialog"]');
      const reportLinks = Array.from(document.querySelectorAll('[data-testid="mdtodo-report-link"]')).filter(visible);
      const blockers = Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-launch-blocker"], [data-testid="mdtodo-workbench-launch-error"], [data-testid="mdtodo-create-blocker"], .launch-blocker, [role="alert"]')).filter(visible).slice(0, 12).map((element, index) => ({
        index,
        testId: element.getAttribute("data-testid"),
        role: element.getAttribute("role"),
        text: trim(element.textContent || "", 260),
      })).filter((item) => item.text);
      const workbenchLinks = Array.from(document.querySelectorAll('[data-testid="mdtodo-workbench-link-summary"] li, a[href*="/workbench/sessions/"]')).filter(visible);
      const measurePaneGap = (name, paneSelector, contentSelector) => {
        const pane = document.querySelector(paneSelector);
        if (!visible(pane)) return { name, visible: false };
        const rect = pane.getBoundingClientRect();
        const contentNodes = Array.from(pane.querySelectorAll(contentSelector)).filter(visible);
        const contentBottom = Math.max(rect.top, ...contentNodes.map((element) => element.getBoundingClientRect().bottom));
        const bottomGapPx = Math.max(0, Math.round(rect.bottom - contentBottom));
        const heightPx = Math.max(0, Math.round(rect.height));
        return {
          name,
          visible: true,
          widthPx: Math.max(0, Math.round(rect.width)),
          heightPx,
          bottomGapPx,
          bottomGapRatio: heightPx > 0 ? Number((bottomGapPx / heightPx).toFixed(3)) : 0,
          contentNodeCount: contentNodes.length,
        };
      };
      const paneGaps = [
        measurePaneGap("task-tree", '[data-testid="mdtodo-task-tree"]', ${JSON.stringify(MDTODO_TASK_TREE_PANE_CONTENT_SELECTOR)}),
        measurePaneGap("task-detail", '[data-testid="mdtodo-task-detail"]', '[data-testid="mdtodo-body-rendered"] > *, [data-testid="mdtodo-body-read"], [data-testid="mdtodo-report-section"], [data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-launch-workbench"], [data-testid="mdtodo-delete-task"], [data-testid="mdtodo-task-detail-error"], .mdtodo-detail-header, .document-header, .task-status-stack > *, .task-document-footer, .document-footer'),
        measurePaneGap("report-sidebar", '[data-testid="mdtodo-report-sidebar"]', '[data-testid="mdtodo-report-preview"] > *, [data-testid="mdtodo-report-error"], [data-testid="mdtodo-report-fullscreen"], [data-testid="mdtodo-report-close"], .report-sidebar-header, .report-preview .markdown-body > *'),
      ];
      return {
        pageKind: mdtodoVisible || path.startsWith("/projects/mdtodo") ? "project-management-mdtodo" : rootVisible || path === "/projects" || path.startsWith("/projects/") ? "project-management-root" : "project-management-unknown",
        configuredPath,
        rootVisible,
        mdtodoVisible,
        sourceCount: Math.max(sourceItems.length, sourceOptionCount),
        fileCount: Math.max(fileItems.length, fileOptionCount),
        taskCount: taskItems.length,
        taskRefMissingCount: taskCandidates.filter((candidate) => !taskRefElement(candidate)).length,
        selectedSourceId: opaqueDomId(selectedSource?.getAttribute("data-source-id") || sourceSelect?.value),
        selectedFileRef: opaqueDomId(selectedFile?.getAttribute("data-file-ref") || fileSelect?.value),
        selectedFileLabel: selectedFile ? trim(selectedFile.textContent || "", 180) : selectedFileOption?.label || null,
        fileOptionLabels: fileOptions.map((option) => option.label).filter(Boolean).slice(0, 24),
        selectedTaskRef: opaqueDomId(selectedTask?.getAttribute("data-task-ref")),
        selectedTaskStatus: selectedTask?.getAttribute("data-task-status") || selectedTask?.querySelector("[data-state]")?.getAttribute("data-state") || null,
        sourceSelectVisible: visible(sourceSelect),
        fileSelectVisible: visible(fileSelect),
        sourceConfigVisible: visible(document.querySelector('[data-testid="mdtodo-source-form-hwpod"], [data-testid="mdtodo-source-config-dialog"], [role="dialog"]')),
        taskEditorVisible: visible(document.querySelector('[data-testid="mdtodo-edit-title"], [data-testid="mdtodo-edit-body"], [data-testid="mdtodo-body-editor"]')),
        taskBodyVisible: visible(bodyRendered),
        taskBodyText: visible(bodyRendered) ? trim(bodyRendered.textContent || "", 500) : "",
        newTaskDraftVisible: visible(document.querySelector('[data-testid="mdtodo-new-title"], [data-testid="mdtodo-new-body"]')),
        taskStatusCounts: statusCounts,
        reportLinkCount: reportLinks.length,
        reportPreviewVisible: visible(reportPreview),
        reportPreviewText: visible(reportPreview) ? trim(reportPreview.textContent || "", 500) : "",
        reportFullscreenVisible: visible(reportFullscreen),
        launchButtonVisible: visible(launch),
        launchButtonEnabled: visible(launch) && !launch.disabled && launch.getAttribute("aria-disabled") !== "true",
        launchButtonText: trim(launch?.textContent || "", 120),
        blockerCount: blockers.length,
        blockers,
        paneGaps,
        workbenchLinkCount: workbenchLinks.length,
        valuesRedacted: true,
      };
    };
    const url = location.href;
    const routeSessionMatch = url.match(/\/workbench\/sessions\/([^/?#]+)/u);
    const activeSession = document.querySelector('[data-active="true"][data-session-id], [aria-selected="true"][data-session-id], .active[data-session-id]');
    const activeSessionId = activeSession ? activeSession.getAttribute("data-session-id") : null;
    const commandInput = document.querySelector("#command-input");
    const commandSubmit = document.querySelector('#command-send, #command-submit, [data-testid="command-submit"], [data-testid="composer-submit"], [data-testid="send-command"]');
    const composerWarning = document.querySelector(".composer-warning");
    const messageSelector = 'article.message-card, .message-card[data-message-id], article[data-message-id]';
    const stableTraceSelector = 'li.trace-render-row[data-row-id], li.trace-render-row[data-testid="trace-render-row"], [data-testid="trace-render-row"][data-row-id]';
    const fallbackTraceSelector = '[data-testid*="trace" i], [class*="trace" i], [data-trace-id], [data-testid*="event" i]';
    const diagnosticSelector = '.api-error-diagnostic, [class*="api-error-diagnostic" i], [class*="message-diagnostic" i], [class*="projection-diagnostic" i], [data-testid="api-error-diagnostic" i], [data-testid="error-diagnostic" i], [data-testid*="diagnostic" i], [role="alert"], [aria-live="assertive"]';
    const messages = summarizeMessages(messageSelector, 80);
    const stableTraceElements = Array.from(document.querySelectorAll(stableTraceSelector));
    const fallbackTraceElements = Array.from(document.querySelectorAll(fallbackTraceSelector)).filter((element) => element.matches('li,[role="listitem"],[data-testid*="trace-row" i],[data-testid*="event-row" i]'));
    const traceRows = summarizeElements(stableTraceElements.length > 0 ? stableTraceElements : fallbackTraceElements, 30);
    const loadings = collectLoadingNodes();
    const sessionRail = collectSessionRailTitles();
    const diagnostics = Array.from(document.querySelectorAll(diagnosticSelector)).filter(visible).slice(-40).map((element, index) => {
      const rect = element.getBoundingClientRect();
      const text = diagnosticSummaryText(element);
      if (!text || diagnosticToggleOnly(element, text)) return null;
      const traceMatch = text.match(/\b(?:trace_id=)?(trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu);
      const httpStatusMatch = text.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu);
      const idleMatch = text.match(/\bidle\s+(\d+)s\b/iu);
      const waitingForMatch = text.match(/\bwaitingFor=([^\s；;，,)]+)/iu);
      const lastEventLabelMatch = text.match(/\blastEventLabel=([^\s；;，,)]+)/iu);
      const diagnosticCode = httpStatusMatch ? "http-" + httpStatusMatch[1] : /turn\s*超过|无新活动/iu.test(text) ? "turn-idle-no-activity" : /Failed to fetch/iu.test(text) ? "failed-to-fetch" : "diagnostic";
      return {
        index,
        tag: element.tagName.toLowerCase(),
        className: String(element.className || "").slice(0, 240),
        testId: element.getAttribute("data-testid"),
        role: element.getAttribute("role"),
        compact: element.getAttribute("data-compact"),
        expanded: element.getAttribute("data-expanded") || element.getAttribute("aria-expanded"),
        title: element.getAttribute("title"),
        diagnosticCode,
        traceId: traceMatch?.[1] || null,
        httpStatus: httpStatusMatch ? Number(httpStatusMatch[1]) : null,
        idleSeconds: idleMatch ? Number(idleMatch[1]) : null,
        waitingFor: waitingForMatch?.[1] || null,
        lastEventLabel: lastEventLabelMatch?.[1] || null,
        text,
        rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
      };
    }).filter(Boolean);
    const turns = Array.from(document.querySelectorAll('article.message-card[data-role="agent"], .message-card[data-role="agent"], article[data-role="agent"]')).filter(visible).map((element, index) => {
      const rect = element.getBoundingClientRect();
      const text = textHashInput(element);
      const finalResponse = messageBodyTextDetail(element);
      const traceElement = element.matches("[data-trace-id]") ? element : element.querySelector("[data-trace-id]");
      const traceMatch = text.match(/\btrc_[A-Za-z0-9_-]+\b/u);
      const durationText = trim(element.querySelector(".message-duration-meta")?.textContent || "", 120);
      const activityText = trim(element.querySelector(".message-activity-meta")?.textContent || "", 120);
      const directTraceId = element.getAttribute("data-trace-id") || traceElement?.getAttribute("data-trace-id") || traceMatch?.[0] || null;
      return {
        index,
        role: element.getAttribute("data-role") || "agent",
        status: element.getAttribute("data-status") || null,
        sessionId: element.getAttribute("data-session-id") || null,
        messageId: element.getAttribute("data-message-id") || element.getAttribute("id") || null,
        traceId: directTraceId,
        turnId: element.getAttribute("data-turn-id") || directTraceId,
        durationText,
        activityText,
        finalResponseText: finalResponse.text,
        finalResponseTextSource: finalResponse.source,
        finalResponseCandidateCount: finalResponse.candidateCount,
        finalResponsePresent: Boolean(finalResponse.text && finalResponse.text.trim()),
        text,
        rect: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) },
      };
    }).slice(-80);
    const active = document.activeElement;
    return {
      url,
      path: location.pathname,
      routeSessionId: routeSessionMatch ? decodeURIComponent(routeSessionMatch[1]) : null,
      activeSessionId,
      title: document.title,
      focus: active ? { tag: active.tagName.toLowerCase(), testId: active.getAttribute("data-testid"), role: active.getAttribute("role") } : null,
      viewport: { width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio },
      scroll: { x: Math.round(window.scrollX), y: Math.round(window.scrollY), height: Math.round(document.documentElement.scrollHeight), width: Math.round(document.documentElement.scrollWidth) },
      messages,
      traceRows,
      loadings,
      sessionRail,
      diagnostics,
      turns,
      composer: {
        inputPresent: visible(commandInput),
        inputDisabled: Boolean(commandInput?.disabled) || commandInput?.getAttribute("aria-disabled") === "true",
        warningPresent: visible(composerWarning),
        warningText: trim(composerWarning?.textContent || "", 160),
        submitPresent: visible(commandSubmit),
        submitDisabled: Boolean(commandSubmit?.disabled) || commandSubmit?.getAttribute("aria-disabled") === "true",
        submitAction: commandSubmit?.getAttribute("data-action") || null,
        submitText: trim(commandSubmit?.textContent || "", 80),
        submitTestId: commandSubmit?.getAttribute("data-testid") || null,
        activeStatus: activeSession?.getAttribute("data-status") || null,
        valuesRedacted: true
      },
      projectManagement: collectProjectManagement(),
      pageProvenance: {
        url: location.href,
        path: location.pathname,
        title: document.title,
        readyState: document.readyState,
        timeOrigin: Math.round(performance.timeOrigin || 0),
        navigationStartTime: (performance.getEntriesByType("navigation")[0] || null)?.startTime ?? null,
        scripts: Array.from(document.scripts).map((element) => {
          if (!element.src) return null;
          try {
            const url = new URL(element.src, location.href);
            const keys = Array.from(url.searchParams.keys()).sort();
            return url.pathname + (keys.length > 0 ? "?keys=" + keys.join(",") : "");
          } catch {
            return null;
          }
        }).filter(Boolean).sort(),
        stylesheets: Array.from(document.querySelectorAll('link[rel~="stylesheet"][href]')).map((element) => {
          try {
            const url = new URL(element.href, location.href);
            const keys = Array.from(url.searchParams.keys()).sort();
            return url.pathname + (keys.length > 0 ? "?keys=" + keys.join(",") : "");
          } catch {
            return null;
          }
        }).filter(Boolean).sort(),
        meta: Array.from(document.querySelectorAll("meta[name], meta[property]")).map((element) => ({
          key: String(element.getAttribute("name") || element.getAttribute("property") || "").slice(0, 120),
          content: String(element.getAttribute("content") || "").slice(0, 200),
        })).filter((item) => item.key).sort((a, b) => a.key.localeCompare(b.key)),
      },
      performance: performance.getEntriesByType("resource").slice(-80).map(resourceTimingSample),
    };
  }, { projectManagement }), evaluateTimeoutMs, "sampleOnePage DOM evaluate exceeded " + evaluateTimeoutMs + "ms").catch((error) => ({ error: errorSummary(error), url: pageUrl(targetPage) }));
  const sample = {
    seq: sampleSeq,
    sampleGroupSeq: groupSeq,
    ts: new Date().toISOString(),
    reason,
    pageRole,
    pageId: targetPageId,
    pageEpoch: Number.isFinite(Number(pageEpoch)) ? Number(pageEpoch) : 0,
    commandId: activeCommandId,
    observerInitiated: false,
    ...digestDom(dom, pageRole),
  };
  await appendJsonl(files.samples, sample);
}

function digestDom(dom, pageRole = "control") {
  if (dom && dom.error) return dom;
  const messages = Array.isArray(dom.messages) ? dom.messages.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || ""), bodyTextHash: sha256Text(item.bodyText || ""), bodyTextPreview: truncate(item.bodyText || "", 160), bodyTextBytes: Buffer.byteLength(item.bodyText || ""), bodyTextPresent: Boolean(String(item.bodyText || "").trim()) })) : [];
  const traceRows = Array.isArray(dom.traceRows) ? dom.traceRows.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || "") })) : [];
  const loadings = Array.isArray(dom.loadings) ? dom.loadings.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 160), textBytes: Buffer.byteLength(item.text || ""), ownerTextHash: sha256Text(item.ownerText || ""), ownerTextPreview: truncate(item.ownerText || "", 160) })) : [];
  const sessionRail = digestSessionRail(dom.sessionRail);
  const diagnostics = Array.isArray(dom.diagnostics) ? dom.diagnostics.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 260), textBytes: Buffer.byteLength(item.text || "") })) : [];
  const turns = Array.isArray(dom.turns) ? dom.turns.map((item) => ({ ...item, textHash: sha256Text(item.text || ""), textPreview: truncate(item.text || "", 200), textBytes: Buffer.byteLength(item.text || ""), finalResponseTextHash: sha256Text(item.finalResponseText || ""), finalResponseTextPreview: truncate(item.finalResponseText || "", 200), finalResponseTextBytes: Buffer.byteLength(item.finalResponseText || ""), finalResponsePresent: Boolean(String(item.finalResponseText || "").trim()) })) : [];
  const projectManagementSample = digestProjectManagement(dom.projectManagement);
  const pageProvenance = normalizePageProvenance(dom.pageProvenance, { reason: "sample", pageLoadSeq: currentPageProvenance?.pageLoadSeq ?? pageLoadSeq });
  if (pageRole === "control") currentPageProvenance = pageProvenance;
  return { ...dom, messages, traceRows, loadings, sessionRail, diagnostics, turns, projectManagement: projectManagementSample, pageProvenance: compactPageProvenance(pageProvenance) };
}

function digestProjectManagement(value) {
  if (!value || typeof value !== "object") return null;
  const opaque = (raw) => {
    const text = String(raw || "");
    if (!text) return null;
    return {
      hash: sha256Text(text),
      preview: text.length <= 18 ? text : text.slice(0, 10) + "..." + text.slice(-5),
      bytes: Buffer.byteLength(text),
      valuesRedacted: true
    };
  };
  const textDigest = (raw, limit = 160) => {
    const text = String(raw || "");
    return { textHash: sha256Text(text), textPreview: truncate(text, limit), textBytes: Buffer.byteLength(text), valuesRedacted: true };
  };
  const fileLabelLooksDirect = (label) => /^[^/\\]+\.md$/iu.test(String(label || "").trim());
  const suspiciousFileLabel = (label) => {
    const text = String(label || "").trim();
    return Boolean(text && (!fileLabelLooksDirect(text) || /(?:details\/|_Task_Report|_log_|\/)/iu.test(text)));
  };
  const fileLabels = Array.isArray(value.fileOptionLabels) ? value.fileOptionLabels.map((item) => String(item || "")).filter(Boolean) : [];
  return {
    pageKind: value.pageKind ?? null,
    configuredPath: value.configuredPath === true,
    rootVisible: value.rootVisible === true,
    mdtodoVisible: value.mdtodoVisible === true,
    sourceCount: Number.isFinite(Number(value.sourceCount)) ? Number(value.sourceCount) : 0,
    fileCount: Number.isFinite(Number(value.fileCount)) ? Number(value.fileCount) : 0,
    taskCount: Number.isFinite(Number(value.taskCount)) ? Number(value.taskCount) : 0,
    taskRefMissingCount: Number.isFinite(Number(value.taskRefMissingCount)) ? Number(value.taskRefMissingCount) : 0,
    selectedSourceId: opaque(value.selectedSourceId),
    selectedFileRef: opaque(value.selectedFileRef),
    selectedFileLabel: value.selectedFileLabel ? textDigest(value.selectedFileLabel, 120) : null,
    selectedFileLabelLooksDirect: value.selectedFileLabel ? fileLabelLooksDirect(value.selectedFileLabel) : null,
    fileOptionLabelSamples: fileLabels.slice(0, 10).map((item) => textDigest(item, 120)),
    fileOptionSuspiciousLabelCount: fileLabels.filter(suspiciousFileLabel).length,
    selectedTaskRef: opaque(value.selectedTaskRef),
    selectedTaskStatus: value.selectedTaskStatus ?? null,
    sourceSelectVisible: value.sourceSelectVisible === true,
    fileSelectVisible: value.fileSelectVisible === true,
    sourceConfigVisible: value.sourceConfigVisible === true,
    taskEditorVisible: value.taskEditorVisible === true,
    taskBodyVisible: value.taskBodyVisible === true,
    taskBody: value.taskBodyText ? textDigest(value.taskBodyText, 200) : null,
    newTaskDraftVisible: value.newTaskDraftVisible === true,
    taskStatusCounts: value.taskStatusCounts && typeof value.taskStatusCounts === "object" ? value.taskStatusCounts : {},
    reportLinkCount: Number.isFinite(Number(value.reportLinkCount)) ? Number(value.reportLinkCount) : 0,
    reportPreviewVisible: value.reportPreviewVisible === true,
    reportPreview: value.reportPreviewText ? textDigest(value.reportPreviewText, 200) : null,
    reportFullscreenVisible: value.reportFullscreenVisible === true,
    launchButtonVisible: value.launchButtonVisible === true,
    launchButtonEnabled: value.launchButtonEnabled === true,
    launchButtonText: value.launchButtonText ? textDigest(value.launchButtonText, 120) : null,
    blockerCount: Number.isFinite(Number(value.blockerCount)) ? Number(value.blockerCount) : 0,
    blockers: Array.isArray(value.blockers) ? value.blockers.slice(0, 12).map((item) => ({
      index: item?.index ?? null,
      testId: item?.testId ?? null,
      role: item?.role ?? null,
      ...textDigest(item?.text || "", 160),
    })) : [],
    paneGaps: Array.isArray(value.paneGaps) ? value.paneGaps.slice(0, 8).map((item) => ({
      name: item?.name ?? null,
      visible: item?.visible === true,
      widthPx: Number.isFinite(Number(item?.widthPx)) ? Number(item.widthPx) : null,
      heightPx: Number.isFinite(Number(item?.heightPx)) ? Number(item.heightPx) : null,
      bottomGapPx: Number.isFinite(Number(item?.bottomGapPx)) ? Number(item.bottomGapPx) : null,
      bottomGapRatio: Number.isFinite(Number(item?.bottomGapRatio)) ? Number(item.bottomGapRatio) : null,
      contentNodeCount: Number.isFinite(Number(item?.contentNodeCount)) ? Number(item.contentNodeCount) : null,
      valuesRedacted: true,
    })) : [],
    workbenchLinkCount: Number.isFinite(Number(value.workbenchLinkCount)) ? Number(value.workbenchLinkCount) : 0,
    valuesRedacted: true
  };
}

function digestSessionRail(value) {
  if (!value || typeof value !== "object") return null;
  const items = Array.isArray(value.items) ? value.items.map((item) => {
    const titleText = String(item?.titleText || item?.titlePreview || "");
    return {
      index: item?.index ?? null,
      tag: item?.tag ?? null,
      testId: item?.testId ?? null,
      role: item?.role ?? null,
      active: item?.active === true,
      status: item?.status ?? null,
      dataStatus: item?.dataStatus ?? null,
      running: item?.running === true,
      dataRunning: item?.dataRunning ?? null,
      ariaBusy: item?.ariaBusy ?? null,
      sessionId: item?.sessionId ?? null,
      sessionIdPrefix: item?.sessionIdPrefix ?? (item?.sessionId ? String(item.sessionId).slice(0, 12) : null),
      fallbackTitle: item?.fallbackTitle === true,
      titleHash: sha256Text(titleText),
      titlePreview: truncate(titleText, 160),
      titleBytes: Buffer.byteLength(titleText),
      rect: item?.rect ?? null,
    };
  }) : [];
  const fallbackItems = items.filter((item) => item.fallbackTitle).slice(0, 12);
  const visibleCount = Number(value.visibleCount ?? items.length);
  const fallbackTitleCount = Number(value.fallbackTitleCount ?? fallbackItems.length);
  return {
    visibleCount: Number.isFinite(visibleCount) ? visibleCount : items.length,
    fallbackTitleCount: Number.isFinite(fallbackTitleCount) ? fallbackTitleCount : fallbackItems.length,
    fallbackTitleRatio: Number.isFinite(Number(value.fallbackTitleRatio)) ? Number(value.fallbackTitleRatio) : (items.length > 0 ? Number((fallbackItems.length / items.length).toFixed(4)) : 0),
    activeItem: items.find((item) => item.active) || null,
    items,
    fallbackItems,
    valuesRedacted: true,
  };
}

function reserveScreenshot(reason) {
  if (screenshotCount >= maxScreenshots) {
    lastScreenshotAtMs = Date.now();
    return {
      seq: null,
      sampleSeq,
      ts: new Date().toISOString(),
      kind: "screenshot-skipped",
      reason,
      skipped: true,
      skipReason: "screenshot-cap-reached",
      screenshotCount,
      maxScreenshots,
      pageId,
      currentUrl: currentPageUrl(),
      valuesRedacted: true,
    };
  }
  screenshotCount += 1;
  return null;
}

async function captureScreenshot(reason, imageType = "png") {
  if (!page || page.isClosed()) throw new Error("page is not available for screenshot");
  const skipped = reserveScreenshot(reason);
  if (skipped !== null) return skipped;
  if (screenshotCaptureState && screenshotCaptureState.settled !== true) {
    const ageMs = Date.now() - Number(screenshotCaptureState.startedAtMs || Date.now());
    const error = new Error("screenshot capture already in progress");
    error.details = {
      reason,
      currentUrl: currentPageUrl(),
      pageId,
      activeReason: screenshotCaptureState.reason,
      activeStartedAt: screenshotCaptureState.startedAt,
      activeAgeMs: ageMs,
      activeTimedOut: screenshotCaptureState.timedOut === true,
      timeoutMs: screenshotCaptureTimeoutMs,
      valuesRedacted: true,
    };
    lastScreenshotAtMs = Date.now();
    throw error;
  }
  artifactSeq += 1;
  const safeReason = safeId(String(reason || "manual")).slice(0, 40) || "manual";
  const type = imageType === "jpeg" || imageType === "jpg" ? "jpeg" : "png";
  const ext = type === "jpeg" ? "jpg" : "png";
  const file = path.join(dirs.screenshots, String(sampleSeq).padStart(6, "0") + "_" + String(artifactSeq).padStart(4, "0") + "_" + safeReason + "." + ext);
  const timeoutMs = screenshotCaptureTimeoutMs;
  const options = type === "jpeg"
    ? { path: file, type: "jpeg", quality: 70, fullPage: false, animations: "disabled", timeout: timeoutMs }
    : { path: file, type: "png", fullPage: false, animations: "disabled", timeout: timeoutMs };
  const state = { reason, startedAtMs: Date.now(), startedAt: new Date().toISOString(), timeoutMs, settled: false, timedOut: false };
  screenshotCaptureState = state;
  const screenshotPromise = page.screenshot(options)
    .then((value) => {
      state.settled = true;
      return value;
    })
    .catch((error) => {
      state.settled = true;
      throw error;
    })
    .finally(() => {
      if (screenshotCaptureState === state) screenshotCaptureState = null;
    });
  try {
    await withHardTimeout(screenshotPromise, timeoutMs + 1000, "captureScreenshot " + safeReason + " exceeded " + timeoutMs + "ms");
    const meta = await fileMeta(file);
    const artifact = { seq: artifactSeq, sampleSeq, ts: new Date().toISOString(), kind: "screenshot", reason, path: file, type, byteCount: meta.byteCount, sha256: meta.sha256, pageId, currentUrl: currentPageUrl(), timeoutMs };
    await appendJsonl(files.artifacts, artifact);
    lastScreenshotAtMs = Date.now();
    return artifact;
  } catch (error) {
    if (String(error?.message || "").includes("exceeded " + timeoutMs + "ms")) state.timedOut = true;
    lastScreenshotAtMs = Date.now();
    const wrapped = error instanceof Error ? error : new Error(String(error));
    wrapped.details = {
      ...(wrapped.details || {}),
      reason,
      currentUrl: currentPageUrl(),
      pageId,
      timeoutMs,
      file,
      valuesRedacted: true,
    };
    throw wrapped;
  }
}

async function captureCommandScreenshot(command) {
  const shouldWaitProject = command.waitProjectManagementReady === true;
  const readiness = shouldWaitProject ? await waitForProjectManagementCommandReady({ timeoutMs: 15000 }) : null;
  if (readiness && readiness.ok !== true) {
    const error = new Error("screenshot project-management readiness wait failed: " + (readiness.reason || "not-ready"));
    error.details = { readiness, currentUrl: currentPageUrl(), pageId, valuesRedacted: true };
    throw error;
  }
  const artifact = await captureScreenshot(command.reason || command.label || "manual", command.imageType || "png");
  return { ...artifact, readiness, valuesRedacted: true };
}

function eventRecord(type, data) {
  const clean = sanitize(data) || {};
  return { ts: new Date().toISOString(), type, jobId, pageId: clean.pageId ?? pageId, pageRole: clean.pageRole ?? "control", sampleSeq, commandId: activeCommandId, ...clean };
}

async function summarizeWorkbenchResponseBody(response, request) {
  const method = String(request.method() || "GET").toUpperCase();
  const path = safeUrlPath(response.url()) || "";
  const resourceType = String(request.resourceType() || "");
  const status = Number(response.status());
  if (!shouldSummarizeWorkbenchResponseBody({ method, path, resourceType, status })) return { bodyRead: false };
  const headers = response.headers();
  const contentType = String(headers["content-type"] || headers["Content-Type"] || "");
  if (!/json/iu.test(contentType)) return { bodyRead: false, bodyReadSkipped: "non-json", valuesRedacted: true };
  const maxBytes = responseBodyMaxBytes;
  const body = await boundedResponseText(response, maxBytes);
  if (body.ok !== true || typeof body.value !== "string") return { bodyRead: false, bodyReadSkipped: body.error?.reason || "bounded-response-read-failed", bodyContract: body.bodyContract, valuesRedacted: true };
  const text = body.value;
  const byteCount = Buffer.byteLength(text);
  if (byteCount > maxBytes) return { bodyRead: true, bodyReadSkipped: "body-too-large", bodyByteCount: byteCount, bodyHash: sha256Text(text), valuesRedacted: true };
  let parsed = null;
  try {
    parsed = JSON.parse(text);
  } catch (error) {
    return { bodyRead: true, bodyReadSkipped: "json-parse-error", bodyByteCount: byteCount, bodyHash: sha256Text(text), bodyParseError: errorSummary(error), valuesRedacted: true };
  }
  return {
    bodyRead: true,
    bodyByteCount: byteCount,
    bodyHash: sha256Text(text),
    bodySummary: summarizeWorkbenchJsonBody(parsed, path),
    valuesRedacted: true
  };
}

function shouldSummarizeWorkbenchResponseBody({ method, path, resourceType, status }) {
  if (method !== "GET" && method !== "POST") return false;
  if (!Number.isFinite(status) || status < 200 || status >= 300) return false;
  if (resourceType === "eventsource" || path === "/v1/workbench/events") return false;
  return path === "/v1/agent/chat"
    || path === "/v1/agent/chat/steer"
    || path === "/v1/workbench/sessions"
    || /^\/v1\/workbench\/sessions\/[^/]+\/messages$/u.test(path)
    || /^\/v1\/workbench\/turns\/[^/]+$/u.test(path)
    || /^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(path);
}

function summarizeWorkbenchJsonBody(value, path) {
  const traceIds = new Set();
  const sessionIds = new Set();
  const terminalTraceIds = new Set();
  const statusCounts = {};
  const counters = {
    objectCount: 0,
    arrayCount: 0,
    traceEventLikeCount: 0,
    messageLikeCount: 0,
    turnLikeCount: 0,
    terminalStatusCount: 0,
    runningStatusCount: 0,
    terminalTextCount: 0,
    finalTextFieldCount: 0,
    finalTextByteCount: 0
  };

  const visit = (node, key = "", parent = null, depth = 0) => {
    if (depth > 32 || node === null || node === undefined) return;
    if (typeof node === "string") {
      collectWorkbenchIdsFromText(node, traceIds, sessionIds);
      const normalizedStatus = normalizeWorkbenchStatus(key, node);
      if (normalizedStatus) {
        statusCounts[normalizedStatus] = (statusCounts[normalizedStatus] || 0) + 1;
        if (isWorkbenchTerminalStatus(normalizedStatus)) counters.terminalStatusCount += 1;
        if (isWorkbenchRunningStatus(normalizedStatus)) counters.runningStatusCount += 1;
      }
      if (isWorkbenchTerminalText(node)) {
        counters.terminalTextCount += 1;
        for (const traceId of workbenchTraceIdsFromRecord(parent)) terminalTraceIds.add(traceId);
      }
      if (isLikelyWorkbenchFinalTextField(key, parent, node)) {
        counters.finalTextFieldCount += 1;
        counters.finalTextByteCount += Buffer.byteLength(node);
        for (const traceId of workbenchTraceIdsFromRecord(parent)) terminalTraceIds.add(traceId);
      }
      return;
    }
    if (typeof node !== "object") return;
    if (Array.isArray(node)) {
      counters.arrayCount += 1;
      for (const item of node) visit(item, key, parent, depth + 1);
      return;
    }
    counters.objectCount += 1;
    const record = node;
    const recordTraceIds = workbenchTraceIdsFromRecord(record);
    for (const raw of [record.traceId, record.trace_id, record.id, record.turnId, record.messageId]) {
      if (typeof raw === "string") collectWorkbenchIdsFromText(raw, traceIds, sessionIds);
    }
    for (const raw of [record.sessionId, record.session_id]) {
      if (typeof raw === "string") collectWorkbenchIdsFromText(raw, traceIds, sessionIds);
    }
    const statusValue = record.status ?? record.state ?? record.phase ?? record.result ?? record.lifecycle;
    if (typeof statusValue === "string") {
      const normalizedStatus = normalizeWorkbenchStatus("status", statusValue);
      if (normalizedStatus) {
        statusCounts[normalizedStatus] = (statusCounts[normalizedStatus] || 0) + 1;
        if (isWorkbenchTerminalStatus(normalizedStatus)) {
          counters.terminalStatusCount += 1;
          for (const traceId of recordTraceIds) terminalTraceIds.add(traceId);
        }
        if (isWorkbenchRunningStatus(normalizedStatus)) counters.runningStatusCount += 1;
      }
    }
    if (record.traceId || record.trace_id || record.turnId || record.turn_id) counters.turnLikeCount += 1;
    if (record.messageId || record.message_id || record.role || record.author || record.content || record.text || record.finalResponse) counters.messageLikeCount += 1;
    if (record.projectedSeq !== undefined || record.sourceSeq !== undefined || record.eventSeq !== undefined || record.eventKind !== undefined || record.eventTimestamp !== undefined) counters.traceEventLikeCount += 1;
    for (const [childKey, childValue] of Object.entries(record)) visit(childValue, childKey, record, depth + 1);
  };
  visit(value);
  return {
    pathKind: workbenchBodyPathKind(path),
    traceIds: Array.from(traceIds).sort().slice(0, 12),
    terminalTraceIds: Array.from(terminalTraceIds).sort().slice(0, 12),
    sessionIds: Array.from(sessionIds).sort().slice(0, 12),
    statusCounts,
    ...counters,
    terminalEvidenceCount: counters.terminalStatusCount + counters.terminalTextCount,
    valuesRedacted: true
  };
}

function collectWorkbenchIdsFromText(value, traceIds, sessionIds) {
  const text = String(value || "");
  for (const match of text.matchAll(/\btrc_[A-Za-z0-9_-]+\b/gu)) traceIds.add(match[0]);
  for (const match of text.matchAll(/\bses_[A-Za-z0-9_-]+\b/gu)) sessionIds.add(match[0]);
}

function workbenchTraceIdsFromRecord(record) {
  if (!record || typeof record !== "object") return [];
  const values = [record.traceId, record.trace_id, record.turnId, record.turn_id, record.messageId, record.message_id, record.id];
  const ids = new Set();
  for (const raw of values) {
    if (typeof raw !== "string") continue;
    for (const match of raw.matchAll(/\btrc_[A-Za-z0-9_-]+\b/gu)) ids.add(match[0]);
  }
  return Array.from(ids).sort();
}

function normalizeWorkbenchStatus(key, value) {
  if (!/status|state|phase|result|lifecycle/iu.test(String(key || ""))) return null;
  const text = String(value || "").toLowerCase().replace(/[^a-z0-9_-]+/gu, "-").replace(/^-+|-+$/gu, "");
  if (!text) return null;
  if (/^(completed|complete|succeeded|success|finished|done|terminal|sealed)$/u.test(text)) return "completed";
  if (/^(failed|failure|error|errored)$/u.test(text)) return "failed";
  if (/^(canceled|cancelled|aborted|cancel)$/u.test(text)) return "canceled";
  if (/^(running|active|in-progress|in_progress|processing|streaming|executing)$/u.test(text)) return "running";
  if (/^(queued|pending|admitted|created|waiting)$/u.test(text)) return "pending";
  return text.slice(0, 80);
}

function isWorkbenchTerminalStatus(value) {
  return value === "completed" || value === "failed" || value === "canceled";
}

function isWorkbenchRunningStatus(value) {
  return value === "running" || value === "pending";
}

function isWorkbenchTerminalText(value) {
  return /轮次完成|轮次失败|轮次取消|已记录|final response|sealed final response|turn completed|turn failed|turn canceled|terminal result|\bcompleted\b|\bfailed\b|\bcanceled\b|\bcancelled\b|\bterminal\b|\bdone\b/iu.test(String(value || ""));
}

function isLikelyWorkbenchFinalTextField(key, parent, value) {
  const text = String(value || "").trim();
  if (!text) return false;
  const field = String(key || "");
  if (!/final|assistant|response|content|markdown|text|message|output|result/iu.test(field)) return false;
  const parentStatus = normalizeWorkbenchStatus("status", parent?.status ?? parent?.state ?? parent?.phase ?? parent?.result ?? "");
  const parentRole = String(parent?.role ?? parent?.author ?? parent?.dataRole ?? "").toLowerCase();
  return isWorkbenchTerminalStatus(parentStatus) || /assistant|agent|code/iu.test(parentRole) || /final|response|result/iu.test(field);
}

function workbenchBodyPathKind(path) {
  if (path === "/v1/agent/chat" || path === "/v1/agent/chat/steer") return "agent-chat-submit";
  if (path === "/v1/workbench/sessions") return "workbench-sessions";
  if (/^\/v1\/workbench\/sessions\/[^/]+\/messages$/u.test(path)) return "workbench-session-messages";
  if (/^\/v1\/workbench\/turns\/[^/]+$/u.test(path)) return "workbench-turn";
  if (/^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(path)) return "workbench-trace-events";
  return "workbench";
}

function controlRecord(command, phase, detail) {
  return {
    ts: new Date().toISOString(),
    seq: commandSeq,
    phase,
    commandId: command.id,
    type: command.type,
    source: command.source || "file",
    input: commandInputSummary(command),
    beforeUrl: command.beforeUrl || null,
    afterUrl: currentPageUrl(),
    pageId,
    detail: sanitize(detail),
  };
}

function commandInputSummary(command) {
  const text = typeof command.text === "string" ? command.text : null;
  const opaque = (value) => {
    const raw = typeof value === "string" ? value : null;
    if (!raw) return null;
    return {
      hash: sha256Text(raw),
      preview: raw.length <= 18 ? raw : raw.slice(0, 10) + "..." + raw.slice(-5),
      bytes: Buffer.byteLength(raw),
      valuesRedacted: true
    };
  };
  return {
    type: command.type,
    path: command.path || null,
    url: command.url ? safeUrl(command.url) : null,
    sessionId: command.sessionId || command.value || null,
    provider: command.provider || null,
    afterRound: Number.isInteger(Number(command.afterRound)) ? Number(command.afterRound) : null,
    severity: command.severity || null,
    alternateSessionStrategy: command.alternateSessionStrategy || null,
    expectedSentinelRange: command.expectedSentinelRange || null,
    expectedActionWaitMs: command.expectedActionWaitMs === null || command.expectedActionWaitMs === undefined || command.expectedActionWaitMs === "" ? null : Number(command.expectedActionWaitMs),
    durationMs: command.durationMs === null || command.durationMs === undefined || command.durationMs === "" ? null : Number(command.durationMs),
    requireComposerReady: command.requireComposerReady === true,
    waitProjectManagementReady: command.waitProjectManagementReady === true,
    findingId: command.findingId || null,
    blocking: command.blocking === true ? true : command.blocking === false ? false : null,
    sourceId: opaque(command.sourceId),
    fileRef: opaque(command.fileRef),
    filename: command.filename ? truncate(command.filename, 200) : null,
    taskRef: opaque(command.taskRef),
    taskId: command.taskId || null,
    field: command.field || null,
    link: command.link ? truncate(command.link, 200) : null,
    titleHash: command.title ? sha256Text(command.title) : null,
    titleBytes: command.title ? Buffer.byteLength(command.title) : null,
    bodyHash: command.body ? sha256Text(command.body) : null,
    bodyBytes: command.body ? Buffer.byteLength(command.body) : null,
    status: command.status || null,
    hwpodId: opaque(command.hwpodId),
    nodeId: opaque(command.nodeId),
    workspaceRoot: opaque(command.workspaceRoot),
    root: opaque(command.root),
    label: command.label ? truncate(command.label, 200) : null,
    textHash: text === null ? null : sha256Text(text),
    textBytes: text === null ? null : Buffer.byteLength(text),
    textPreview: null,
    valuesRedacted: true,
  };
}
`;
}
