// SPEC: PJ2026-01040111 long-running Workbench observation. // Responsibility: Runner page sampling, screenshots, DOM digest, response body summaries, and command record source fragment. 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 taskItems = Array.from(document.querySelectorAll('[data-testid="mdtodo-task-tree"] [data-task-ref], [data-task-ref]')).filter(visible); const taskCandidates = Array.from(document.querySelectorAll('[data-testid="mdtodo-task-tree"] li, [data-testid="mdtodo-task-tree"] [role="treeitem"], [data-testid="mdtodo-task-tree"] [role="listitem"]')).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") || "unknown"; statusCounts[status] = (statusCounts[status] || 0) + 1; } const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-action="launch-workbench"]'); const bodyRendered = document.querySelector('[data-testid="mdtodo-body-rendered"]'); 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"], [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"]', '[data-task-ref], [role="treeitem"], [role="listitem"], li, button, input, select, .task-row-shell, .task-tools'), measurePaneGap("task-detail", '[data-testid="mdtodo-task-detail"]', '[data-testid="mdtodo-body-rendered"] > *, [data-testid="mdtodo-report-section"], [data-testid="mdtodo-workbench-launch"], [data-testid="mdtodo-delete-task"], [data-testid="mdtodo-task-detail-error"], .mdtodo-detail-header, .task-status-stack > *, .task-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: Math.max(0, taskCandidates.length - taskItems.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") || 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"]')), 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, }; } async function captureScreenshot(reason, imageType = "png") { if (!page || page.isClosed()) throw new Error("page is not available for screenshot"); 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 contentLength = Number(headers["content-length"] || headers["Content-Length"]); const maxBytes = 512 * 1024; if (Number.isFinite(contentLength) && contentLength > maxBytes) return { bodyRead: false, bodyReadSkipped: "content-length-too-large", bodyByteCount: contentLength, valuesRedacted: true }; const text = await response.text(); 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, }; } `; }