// SPEC: PJ2026-01040111 long-running Workbench observation.
// Responsibility: Analyzer request-rate, prompt network, runtime console/network alert, and diagnostic grouping source fragment.

export function nodeWebObserveAnalyzerRequestRuntimeSource(): string {
  return String.raw`function buildRequestRateReport(network) {
  const bucketMs = Math.max(1000, Number(alertThresholds.requestRateBucketMs || 0));
  const naturalApiRequests = (Array.isArray(network) ? network : [])
    .filter((item) => item?.observerInitiated !== true && item?.type === "request")
    .map((item) => requestRateEvent(item))
    .filter((item) => item !== null)
    .sort((a, b) => a.tsMs - b.tsMs);
  if (naturalApiRequests.length === 0) {
    return {
      summary: {
        bucketMs,
        bucketSeconds: Number((bucketMs / 1000).toFixed(3)),
        requestCount: 0,
        bucketCount: 0,
        pageCount: 0,
        apiPathCount: 0,
        totalRedPerMinute: alertThresholds.requestRateTotalRedPerMinute,
        pageRedPerMinute: alertThresholds.requestRatePageRedPerMinute,
        apiPathRedPerMinute: alertThresholds.requestRateApiPathRedPerMinute,
        totalPeakPerMinute: 0,
        pagePeakPerMinute: 0,
        apiPathPeakPerMinute: 0,
        overThresholdPeakCount: 0,
        valuesRedacted: true,
      },
      buckets: [],
      pageCurves: [],
      apiPathCurves: [],
      peaks: [],
      valuesRedacted: true,
    };
  }
  const firstBucketMs = Math.floor(naturalApiRequests[0].tsMs / bucketMs) * bucketMs;
  const lastBucketMs = Math.floor(naturalApiRequests[naturalApiRequests.length - 1].tsMs / bucketMs) * bucketMs;
  const makeBucket = (bucketStartMs, extra = {}) => ({
    bucketStartMs,
    bucketEndMs: bucketStartMs + bucketMs,
    startAt: new Date(bucketStartMs).toISOString(),
    endAt: new Date(bucketStartMs + bucketMs).toISOString(),
    count: 0,
    requestPerMinute: 0,
    ...extra,
    valuesRedacted: true,
  });
  const totalBuckets = new Map();
  const pageBuckets = new Map();
  const apiPathBuckets = new Map();
  const pageTotals = new Map();
  const apiPathTotals = new Map();
  const allBucketStarts = [];
  for (let bucketStartMs = firstBucketMs; bucketStartMs <= lastBucketMs; bucketStartMs += bucketMs) {
    totalBuckets.set(bucketStartMs, makeBucket(bucketStartMs));
    allBucketStarts.push(bucketStartMs);
  }
  for (const item of naturalApiRequests) {
    const bucketStartMs = Math.floor(item.tsMs / bucketMs) * bucketMs;
    const totalBucket = totalBuckets.get(bucketStartMs) || makeBucket(bucketStartMs);
    totalBucket.count += 1;
    totalBuckets.set(bucketStartMs, totalBucket);

    const pageKey = item.pageKey;
    const pageMapKey = pageKey + "|" + bucketStartMs;
    const pageBucket = pageBuckets.get(pageMapKey) || makeBucket(bucketStartMs, {
      pageKey,
      pageRole: item.pageRole,
      pageId: item.pageId,
      pageEpoch: item.pageEpoch,
      path: item.framePath,
    });
    pageBucket.count += 1;
    pageBuckets.set(pageMapKey, pageBucket);
    const pageTotal = pageTotals.get(pageKey) || { pageKey, pageRole: item.pageRole, pageId: item.pageId, pageEpoch: item.pageEpoch, path: item.framePath, count: 0 };
    pageTotal.count += 1;
    pageTotals.set(pageKey, pageTotal);

    const apiKey = item.method + " " + item.apiPath;
    const apiMapKey = apiKey + "|" + bucketStartMs;
    const apiBucket = apiPathBuckets.get(apiMapKey) || makeBucket(bucketStartMs, {
      apiKey,
      method: item.method,
      path: item.apiPath,
      pageKey,
      pageRole: item.pageRole,
      pageId: item.pageId,
      pageEpoch: item.pageEpoch,
    });
    apiBucket.count += 1;
    apiPathBuckets.set(apiMapKey, apiBucket);
    const apiTotal = apiPathTotals.get(apiKey) || { apiKey, method: item.method, path: item.apiPath, count: 0 };
    apiTotal.count += 1;
    apiPathTotals.set(apiKey, apiTotal);
  }
  const finalizeBucket = (bucket) => ({
    ...bucket,
    requestPerMinute: Number((Number(bucket.count || 0) * 60000 / bucketMs).toFixed(2)),
  });
  const buckets = Array.from(totalBuckets.values()).map(finalizeBucket);
  const pageBucketRows = Array.from(pageBuckets.values()).map(finalizeBucket);
  const apiPathBucketRows = Array.from(apiPathBuckets.values()).map(finalizeBucket);
  const peakForRows = (rows, thresholdPerMinute, scope, extra = {}) => rows
    .filter((row) => Number(row.requestPerMinute ?? 0) >= Number(thresholdPerMinute || Infinity))
    .map((row) => ({
      scope,
      thresholdPerMinute,
      overThreshold: true,
      bucketMs,
      bucketStartMs: row.bucketStartMs,
      bucketEndMs: row.bucketEndMs,
      startAt: row.startAt,
      endAt: row.endAt,
      count: row.count,
      requestPerMinute: row.requestPerMinute,
      ...extra(row),
      valuesRedacted: true,
    }));
  const totalPeaks = peakForRows(buckets, alertThresholds.requestRateTotalRedPerMinute, "total", () => ({}));
  const pagePeaks = peakForRows(pageBucketRows, alertThresholds.requestRatePageRedPerMinute, "page", (row) => ({
    pageKey: row.pageKey,
    pageRole: row.pageRole,
    pageId: row.pageId,
    pageEpoch: row.pageEpoch,
    path: row.path,
  }));
  const apiPathPeaks = peakForRows(apiPathBucketRows, alertThresholds.requestRateApiPathRedPerMinute, "apiPath", (row) => ({
    apiKey: row.apiKey,
    method: row.method,
    path: row.path,
    pageKey: row.pageKey,
    pageRole: row.pageRole,
    pageId: row.pageId,
    pageEpoch: row.pageEpoch,
  }));
  const pageCurves = Array.from(pageTotals.values())
    .map((page) => {
      const rows = pageBucketRows
        .filter((row) => row.pageKey === page.pageKey)
        .sort((a, b) => a.bucketStartMs - b.bucketStartMs);
      const peak = maxByNumber(rows, (row) => row.requestPerMinute);
      return {
        ...page,
        bucketCount: rows.length,
        peakRequestPerMinute: peak?.requestPerMinute ?? 0,
        peakBucket: peak ?? null,
        buckets: rows.slice(-60),
        valuesRedacted: true,
      };
    })
    .sort((a, b) => Number(b.peakRequestPerMinute ?? 0) - Number(a.peakRequestPerMinute ?? 0) || Number(b.count ?? 0) - Number(a.count ?? 0));
  const apiPathCurves = Array.from(apiPathTotals.values())
    .map((api) => {
      const rows = apiPathBucketRows
        .filter((row) => row.apiKey === api.apiKey)
        .sort((a, b) => a.bucketStartMs - b.bucketStartMs);
      const peak = maxByNumber(rows, (row) => row.requestPerMinute);
      return {
        ...api,
        bucketCount: rows.length,
        peakRequestPerMinute: peak?.requestPerMinute ?? 0,
        peakBucket: peak ?? null,
        buckets: rows.slice(-60),
        valuesRedacted: true,
      };
    })
    .sort((a, b) => Number(b.peakRequestPerMinute ?? 0) - Number(a.peakRequestPerMinute ?? 0) || Number(b.count ?? 0) - Number(a.count ?? 0));
  const totalPeak = maxByNumber(buckets, (row) => row.requestPerMinute);
  const pagePeak = pageCurves[0] ?? null;
  const apiPathPeak = apiPathCurves[0] ?? null;
  const peaks = [...totalPeaks, ...pagePeaks, ...apiPathPeaks]
    .sort((a, b) => Number(b.requestPerMinute ?? 0) - Number(a.requestPerMinute ?? 0) || String(a.scope).localeCompare(String(b.scope)));
  return {
    summary: {
      bucketMs,
      bucketSeconds: Number((bucketMs / 1000).toFixed(3)),
      requestCount: naturalApiRequests.length,
      bucketCount: buckets.length,
      pageCount: pageCurves.length,
      apiPathCount: apiPathCurves.length,
      firstAt: new Date(firstBucketMs).toISOString(),
      lastAt: new Date(lastBucketMs + bucketMs).toISOString(),
      totalRedPerMinute: alertThresholds.requestRateTotalRedPerMinute,
      pageRedPerMinute: alertThresholds.requestRatePageRedPerMinute,
      apiPathRedPerMinute: alertThresholds.requestRateApiPathRedPerMinute,
      totalPeakPerMinute: totalPeak?.requestPerMinute ?? 0,
      totalPeakCount: totalPeak?.count ?? 0,
      totalPeakAt: totalPeak?.startAt ?? null,
      pagePeakPerMinute: pagePeak?.peakRequestPerMinute ?? 0,
      pagePeakKey: pagePeak?.pageKey ?? null,
      pagePeakPath: pagePeak?.path ?? null,
      apiPathPeakPerMinute: apiPathPeak?.peakRequestPerMinute ?? 0,
      apiPathPeakKey: apiPathPeak?.apiKey ?? null,
      overThresholdPeakCount: peaks.length,
      valuesRedacted: true,
    },
    buckets: buckets.slice(-120),
    pageCurves: pageCurves.slice(0, 20),
    apiPathCurves: apiPathCurves.slice(0, 40),
    peaks: peaks.slice(0, 80),
    valuesRedacted: true,
  };
}

function requestRateEvent(item) {
  const tsMs = Date.parse(String(item?.ts || ""));
  const apiPath = normalizeApiRatePath(urlPath(item?.url));
  if (!Number.isFinite(tsMs) || !isApiLikePath(apiPath)) return null;
  const framePath = normalizeRoutePath(urlPath(item?.frameUrl || item?.url));
  const pageRole = String(item?.pageRole || "unknown");
  const pageId = String(item?.pageId || "unknown");
  const pageEpoch = Number.isFinite(Number(item?.pageEpoch)) ? Number(item.pageEpoch) : null;
  return {
    ts: item.ts,
    tsMs,
    method: String(item?.method || "GET").toUpperCase(),
    apiPath,
    framePath,
    pageRole,
    pageId,
    pageEpoch,
    pageKey: [pageRole, pageId, pageEpoch ?? "-", framePath || "-"].join("|"),
    valuesRedacted: true,
  };
}

function normalizeApiRatePath(value) {
  return normalizeRoutePath(value)
    .replace(/\/sessions\/[^/?#]+/gu, "/sessions/:id")
    .replace(/\/turns\/[^/?#]+/gu, "/turns/:id")
    .replace(/\/traces\/[^/?#]+/gu, "/traces/:id")
    .replace(/\/messages\/[^/?#]+/gu, "/messages/:id")
    .replace(/\/files\/[^/?#]+/gu, "/files/:id")
    .replace(/\/tasks\/[^/?#]+/gu, "/tasks/:id")
    .replace(/\/runs\/[^/?#]+/gu, "/runs/:id")
    .replace(/\/[0-9a-f]{8,}(?=\/|$)/giu, "/:id")
    .replace(/\/[A-Za-z0-9_-]{24,}(?=\/|$)/gu, "/:id");
}

function normalizeRoutePath(value) {
  const raw = String(value || "");
  if (!raw || raw === "-") return "-";
  return raw.replace(/\/+/gu, "/").replace(/\/$/u, "") || "/";
}

function compactRequestRateForOutput(report) {
  if (!report || typeof report !== "object") return null;
  return {
    summary: report.summary ?? null,
    buckets: Array.isArray(report.buckets) ? report.buckets.slice(-12) : [],
    pageCurves: Array.isArray(report.pageCurves) ? report.pageCurves.slice(0, 8).map((item) => ({
      pageKey: item.pageKey ?? null,
      pageRole: item.pageRole ?? null,
      pageId: item.pageId ?? null,
      pageEpoch: item.pageEpoch ?? null,
      path: item.path ?? null,
      count: item.count ?? null,
      peakRequestPerMinute: item.peakRequestPerMinute ?? null,
      peakBucket: item.peakBucket ?? null,
      buckets: Array.isArray(item.buckets) ? item.buckets.slice(-12) : [],
      valuesRedacted: true,
    })) : [],
    apiPathCurves: Array.isArray(report.apiPathCurves) ? report.apiPathCurves.slice(0, 12).map((item) => ({
      apiKey: item.apiKey ?? null,
      method: item.method ?? null,
      path: item.path ?? null,
      count: item.count ?? null,
      peakRequestPerMinute: item.peakRequestPerMinute ?? null,
      peakBucket: item.peakBucket ?? null,
      buckets: Array.isArray(item.buckets) ? item.buckets.slice(-12) : [],
      valuesRedacted: true,
    })) : [],
    peaks: Array.isArray(report.peaks) ? report.peaks.slice(0, 12) : [],
    valuesRedacted: true,
  };
}

function buildPromptNetworkReport(control, network) {
  const promptsById = new Map();
  for (const item of control) {
    if (item?.type !== "sendPrompt" || !item.commandId) continue;
    const existing = promptsById.get(item.commandId) || {
      commandId: item.commandId,
      promptIndex: promptsById.size + 1,
      promptTextHash: item.input?.textHash ?? null,
      promptTextBytes: item.input?.textBytes ?? null,
      startedAt: null,
      completedAt: null,
      failedAt: null,
      phase: null
    };
    if (!existing.promptTextHash && item.input?.textHash) existing.promptTextHash = item.input.textHash;
    if (!existing.promptTextBytes && item.input?.textBytes) existing.promptTextBytes = item.input.textBytes;
    if (item.phase === "started") existing.startedAt = item.ts ?? existing.startedAt;
    if (item.phase === "completed") existing.completedAt = item.ts ?? existing.completedAt;
    if (item.phase === "failed") existing.failedAt = item.ts ?? existing.failedAt;
    existing.phase = item.phase ?? existing.phase;
    promptsById.set(item.commandId, existing);
  }
  const prompts = Array.from(promptsById.values()).sort((a, b) => Date.parse(a.startedAt || a.completedAt || a.failedAt || "") - Date.parse(b.startedAt || b.completedAt || b.failedAt || ""));
  prompts.forEach((item, index) => { item.promptIndex = index + 1; });
  const chatEvents = network
    .filter((item) => String(item?.method || "").toUpperCase() === "POST" && promptSubmitModeForUrl(item?.url) !== null)
    .map((item) => {
      const failureText = item.failureKind ?? item.failure ?? item.errorText ?? null;
      const urlPathValue = urlPath(item.url);
      return {
        ts: item.ts ?? null,
        tsMs: Date.parse(item.ts),
        type: item.type ?? null,
        status: Number.isFinite(Number(item.status)) ? Number(item.status) : null,
        commandId: item.commandId ?? null,
        urlPath: urlPathValue,
        submitMode: promptSubmitModeForUrl(item.url),
        failureKind: failureText ? String(failureText) : null,
        errorTextHash: failureText ? sha256(failureText) : null
      };
    })
    .filter((item) => Number.isFinite(item.tsMs))
    .sort((a, b) => a.tsMs - b.tsMs);
  const rounds = prompts.map((prompt) => {
    const startMs = Date.parse(prompt.startedAt || prompt.completedAt || prompt.failedAt || "");
    const endAnchorMs = Date.parse(prompt.completedAt || prompt.failedAt || prompt.startedAt || "");
    const fromMs = Number.isFinite(startMs) ? startMs - 3000 : Number.NEGATIVE_INFINITY;
    const toMs = Number.isFinite(endAnchorMs) ? endAnchorMs + 30000 : Number.POSITIVE_INFINITY;
    const events = chatEvents.filter((event) => {
      if (event.commandId && prompt.commandId && event.commandId === prompt.commandId) return true;
      return event.tsMs >= fromMs && event.tsMs <= toMs;
    });
    const responses = events.filter((event) => event.type === "response");
    const failures = events.filter((event) => event.type === "requestfailed");
    const responseStatuses = responses.map((event) => event.status).filter((status) => status !== null);
    const submitModes = Array.from(new Set(events.map((event) => event.submitMode).filter(Boolean))).sort();
    const chatPostOk = responseStatuses.some((status) => status >= 200 && status < 300);
    const failureKind = chatPostOk
      ? null
      : failures.length > 0
        ? "requestfailed"
        : responseStatuses.length === 0
          ? "missing-response"
          : "http-status";
    return {
      promptIndex: prompt.promptIndex,
      promptCommandId: prompt.commandId,
      promptTextHash: prompt.promptTextHash,
      promptTextBytes: prompt.promptTextBytes,
      startedAt: prompt.startedAt,
      completedAt: prompt.completedAt,
      failedAt: prompt.failedAt,
      chatPostOk,
      failureKind,
      requestCount: events.filter((event) => event.type === "request").length,
      responseCount: responses.length,
      requestFailedCount: failures.length,
      responseStatuses,
      submitModes,
      steerUsed: submitModes.includes("steer"),
      firstChatEventAt: events[0]?.ts ?? null,
      lastChatEventAt: events[events.length - 1]?.ts ?? null,
      events: events.slice(0, 12).map((event) => ({ ts: event.ts, type: event.type, status: event.status, urlPath: event.urlPath, submitMode: event.submitMode, failureKind: event.failureKind, errorTextHash: event.errorTextHash }))
    };
  });
  return {
    summary: {
      promptCount: rounds.length,
      chatPostOk: rounds.filter((item) => item.chatPostOk === true).length,
      chatPostFailed: rounds.filter((item) => item.chatPostOk === false).length,
      chatPostMissing: rounds.filter((item) => item.failureKind === "missing-response").length
    },
    rounds
  };
}

function promptSubmitModeForUrl(value) {
  const pathValue = urlPath(value);
  if (pathValue === "/v1/agent/chat") return "chat";
  if (pathValue === "/v1/agent/chat/steer") return "steer";
  return null;
}

function parseDomDiagnosticSummary(text) {
  const value = String(text || "");
  const traceMatch = value.match(/\b(?:trace_id=)?(trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu);
  const httpStatusMatch = value.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu);
  const idleMatch = value.match(/\bidle\s+(\d+)s\b/iu);
  const waitingForMatch = value.match(/\bwaitingFor=([^\s；;，,)]+)/iu);
  const lastEventLabelMatch = value.match(/\blastEventLabel=([^\s；;，,)]+)/iu);
  const diagnosticCode = httpStatusMatch
    ? "http-" + httpStatusMatch[1]
    : /turn\s*超过|无新活动/iu.test(value)
      ? "turn-idle-no-activity"
      : /Failed to fetch/iu.test(value)
        ? "failed-to-fetch"
        : "diagnostic";
  return {
    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
  };
}

function isDomDiagnosticSampleText(text) {
  const value = String(text || "").replace(/\s+/g, " ").trim();
  if (!value) return false;
  const strongDiagnostic = [
    /\bHTTP\s+[45][0-9]{2}\b(?:[\s\S]{0,120}\btrace_id=|\b)/iu,
    /\btrace_id=(?:trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu,
    /workbench\s+turn\s*超过\s*\d+ms\s*无新活动/iu,
    /\bturn\s*超过\b[\s\S]{0,120}\b无新活动\b/iu,
    /\bprojection-resume:sync-failed\b/iu,
    /\bAgentRun\s+GET\b[\s\S]*\/result\b[\s\S]*timed out after\s+\d+ms\b/iu,
    /\bFailed to fetch\b/iu,
    /\bFailed to load resource\b[\s\S]{0,180}\bstatus of\s+[45][0-9]{2}\b/iu,
    /\bserver responded with a status of\s+[45][0-9]{2}\b/iu,
    /Code Agent\b[\s\S]{0,120}(?:无法连接上游|请求已结束)/iu
  ].some((pattern) => pattern.test(value));
  if (!strongDiagnostic) return false;
  const looksLikeToolStdout = /\b(?:stdout|stderr):/iu.test(value)
    && /(?:\becho\s+["']?===|\bnode\s+|\.tspy\b|tspy\/|===\s*[A-Za-z0-9_.-]+\s*===)/iu.test(value);
  if (!looksLikeToolStdout) return true;
  return /\b(?:trace_id=|HTTP\s+[45][0-9]{2}|workbench\s+turn\s*超过|projection-resume:sync-failed|Failed to fetch|Failed to load resource|server responded with a status of\s+[45][0-9]{2}|Code Agent\b[\s\S]{0,120}(?:无法连接上游|请求已结束))\b/iu.test(value);
}

function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) {
  const promptTimes = control
    .filter((item) => item.type === "sendPrompt" && item.phase === "completed")
    .map((item) => Date.parse(item.ts))
    .filter(Number.isFinite)
    .sort((a, b) => a - b);
  const observerRefreshTimes = control
    .filter((item) => item.type === "observer-periodic-refresh")
    .map((item) => Date.parse(item.ts))
    .filter(Number.isFinite)
    .sort((a, b) => a - b);
  const naturalNetwork = network.filter((item) => item?.observerInitiated !== true);
  const httpErrors = naturalNetwork
    .filter((item) => item?.type === "response" && Number(item.status) >= 400)
    .map((item) => networkAlertEvent(item, promptTimes));
  const pageResponseErrors = httpErrors.filter((item) => Number(item.status) >= 500);
  const nonBlockingHttpErrors = httpErrors.filter((item) => Number(item.status) < 500);
  const requestFailed = naturalNetwork
    .filter((item) => item?.type === "requestfailed")
    .map((item) => networkAlertEvent(item, promptTimes));
  const workbenchSessionListReadCount = naturalNetwork
    .filter((item) => urlPath(item?.url) === "/v1/workbench/sessions")
    .length;
  const workbenchTraceEventsReadCount = naturalNetwork
    .filter((item) => /^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(urlPath(item?.url)))
    .length;
  const webPerformanceBeaconFailureCount = naturalNetwork
    .filter((item) => urlPath(item?.url) === "/v1/web-performance" && (item?.type === "requestfailed" || Number(item?.status) >= 400))
    .length;
  const workbenchEventSourceFailureCount = naturalNetwork
    .filter((item) => urlPath(item?.url) === "/v1/workbench/events" && (item?.type === "requestfailed" || Number(item?.status) >= 400))
    .length;
  const significantRequestFailed = requestFailed.filter(
    (item) => !isBenignLongLivedStreamClosureAlert(item) && !isObserverRefreshClosureAlert(item, observerRefreshTimes),
  );
  const webPerformanceDiagnostics = extractWebPerformanceRuntimeDiagnostics(naturalNetwork, promptTimes);
  const webPerformancePayloadStates = summarizeWebPerformancePayloadStates(naturalNetwork);
  const webPerformanceDiagnosticGroups = groupWebPerformanceRuntimeDiagnostics(webPerformanceDiagnostics);
  const domDiagnostics = [];
  const executionErrors = [];
  const baselineExecutionErrors = [];
  const canarySessionIds = sessionInvariantCanarySessionIds(control);
  const firstPromptMs = promptTimes.length > 0 ? promptTimes[0] : Infinity;
  const firstSeenExecutionErrorMs = new Map();
  for (const sample of samples) {
    const tsMs = Date.parse(sample?.ts);
    const promptIndex = Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0;
    if (Array.isArray(sample?.diagnostics)) {
      for (const diagnostic of sample.diagnostics.slice(0, 12)) {
        const text = diagnostic?.textPreview || diagnostic?.text || "";
        if (!String(text).trim()) continue;
        const parsedDiagnostic = parseDomDiagnosticSummary(text);
        domDiagnostics.push({
          seq: sample.seq ?? null,
          ts: sample.ts ?? null,
          promptIndex,
          source: "diagnostic-node",
          className: diagnostic.className ?? null,
          diagnosticCode: diagnostic.diagnosticCode ?? parsedDiagnostic.diagnosticCode,
          traceId: diagnostic.traceId ?? parsedDiagnostic.traceId,
          httpStatus: diagnostic.httpStatus ?? parsedDiagnostic.httpStatus,
          idleSeconds: diagnostic.idleSeconds ?? parsedDiagnostic.idleSeconds,
          waitingFor: diagnostic.waitingFor ?? parsedDiagnostic.waitingFor,
          lastEventLabel: diagnostic.lastEventLabel ?? parsedDiagnostic.lastEventLabel,
          compact: diagnostic.compact ?? null,
          expanded: diagnostic.expanded ?? null,
          routeSessionId: sample.routeSessionId ?? null,
          activeSessionId: sample.activeSessionId ?? null,
          textHash: diagnostic.textHash || sha256(text),
          preview: limitText(text, 260)
        });
      }
    }
    const texts = sampleTexts(sample).filter(isDomDiagnosticSampleText);
    for (const text of texts.slice(0, 4)) {
      const parsedDiagnostic = parseDomDiagnosticSummary(text);
      domDiagnostics.push({
        seq: sample.seq ?? null,
        ts: sample.ts ?? null,
        promptIndex,
        source: "sample-text",
        diagnosticCode: parsedDiagnostic.diagnosticCode,
        traceId: parsedDiagnostic.traceId,
        httpStatus: parsedDiagnostic.httpStatus,
        idleSeconds: parsedDiagnostic.idleSeconds,
        waitingFor: parsedDiagnostic.waitingFor,
        lastEventLabel: parsedDiagnostic.lastEventLabel,
        routeSessionId: sample.routeSessionId ?? null,
        activeSessionId: sample.activeSessionId ?? null,
        textHash: sha256(text),
        preview: limitText(text, 220)
      });
    }
    const seenExecutionErrors = new Set();
    for (const candidate of sampleExecutionErrorCandidates(sample)) {
      const parsed = parseExecutionErrorText(candidate.text);
      if (!parsed) continue;
      const textHash = sha256(candidate.text);
      const dedupeKey = [candidate.source, candidate.traceId || "-", parsed.backend || "-", parsed.code || "-", parsed.status || "-", textHash].join("|");
      if (seenExecutionErrors.has(dedupeKey)) continue;
      seenExecutionErrors.add(dedupeKey);
      const firstSeenMs = firstSeenExecutionErrorMs.has(dedupeKey) ? firstSeenExecutionErrorMs.get(dedupeKey) : tsMs;
      if (!firstSeenExecutionErrorMs.has(dedupeKey) && Number.isFinite(tsMs)) firstSeenExecutionErrorMs.set(dedupeKey, tsMs);
      const baseline = Number.isFinite(firstSeenMs) && firstSeenMs < firstPromptMs;
      const event = {
        seq: sample.seq ?? null,
        ts: sample.ts ?? null,
        promptIndex,
        baseline,
        firstSeenAt: Number.isFinite(firstSeenMs) ? new Date(firstSeenMs).toISOString() : null,
        source: candidate.source,
        backend: parsed.backend,
        status: parsed.status,
        code: parsed.code,
        rawCode: parsed.rawCode,
        totalSeconds: parsed.totalSeconds,
        traceId: candidate.traceId || parsed.traceId || null,
        messageId: candidate.messageId || null,
        routeSessionId: sample.routeSessionId ?? null,
        activeSessionId: sample.activeSessionId ?? null,
        textHash,
        preview: limitText(candidate.text, 260)
      };
      const eventSessions = [event.routeSessionId, event.activeSessionId].filter(Boolean);
      const nonCanarySession = canarySessionIds.size > 0 && !eventSessions.some((sessionId) => canarySessionIds.has(sessionId));
      if (baseline || nonCanarySession) baselineExecutionErrors.push({ ...event, baseline: true, baselineReason: nonCanarySession ? "non-canary-session" : "pre-first-prompt" });
      else executionErrors.push(event);
      domDiagnostics.push({
        seq: sample.seq ?? null,
        ts: sample.ts ?? null,
        promptIndex,
        source: "execution-row",
        diagnosticCode: parsed.rawCode || parsed.code || "execution-error",
        traceId: candidate.traceId || parsed.traceId || null,
        routeSessionId: sample.routeSessionId ?? null,
        activeSessionId: sample.activeSessionId ?? null,
        textHash,
        preview: limitText(candidate.text, 220)
      });
    }
  }
  const consoleAlerts = consoleEvents
    .filter((item) => /error|warning|warn|assert/iu.test(String(item?.type || "")) || isDiagnosticText(item?.text))
    .map((item) => consoleAlertEvent(item, promptTimes));
  const significantConsoleAlerts = consoleAlerts.filter((item) => !isBenignLongLivedStreamClosureAlert(item) && !isObserverRefreshClosureAlert(item, observerRefreshTimes));
  const pageErrors = errors.map((item) => ({
    ts: item.ts ?? null,
    promptIndex: promptIndexForTs(promptTimes, item.ts),
    type: item.type ?? null,
    pageRole: item.pageRole ?? item.error?.details?.pageRole ?? null,
    pageId: item.pageId ?? item.error?.details?.pageId ?? null,
    routeSessionId: item.routeSessionId ?? item.error?.details?.routeSessionId ?? null,
    activeSessionId: item.activeSessionId ?? item.error?.details?.activeSessionId ?? null,
    commandId: item.commandId ?? item.error?.details?.commandId ?? null,
    sampleSeq: item.sampleSeq ?? item.error?.details?.sampleSeq ?? null,
    timeoutMs: timeoutMsFromMessage(item.error?.message || item.message || item.error || ""),
    errorName: item.error?.name ?? item.name ?? null,
    messageHash: item.error?.message ? sha256(item.error.message) : item.message ? sha256(item.message) : null,
    preview: limitText(item.error?.message || item.message || item.error || "", 220)
  }));
  return {
    summary: {
      httpErrorCount: httpErrors.length,
      pageResponseErrorCount: pageResponseErrors.length,
      nonBlockingHttpErrorCount: nonBlockingHttpErrors.length,
      requestFailedCount: requestFailed.length,
      significantRequestFailedCount: significantRequestFailed.length,
      workbenchSessionListReadCount,
      workbenchTraceEventsReadCount,
      webPerformanceBeaconFailureCount,
      webPerformancePayloadRequestCount: webPerformancePayloadStates.total,
      webPerformancePayloadParsedCount: webPerformancePayloadStates.parsed,
      webPerformancePayloadParseIssueCount: webPerformancePayloadStates.parseIssue,
      webPerformanceRuntimeDiagnosticCount: webPerformanceDiagnostics.length,
      webPerformanceRuntimeDiagnosticGroupCount: webPerformanceDiagnosticGroups.length,
      workbenchEventSourceFailureCount,
      benignLongLivedStreamClosureCount: requestFailed.length - significantRequestFailed.length,
      domDiagnosticSampleCount: domDiagnostics.length,
      domDiagnosticGroupCount: groupDomDiagnostics(domDiagnostics).length,
      executionErrorCount: executionErrors.length,
      baselineExecutionErrorCount: baselineExecutionErrors.length,
      consoleAlertCount: consoleAlerts.length,
      significantConsoleAlertCount: significantConsoleAlerts.length,
      pageErrorCount: pageErrors.length,
      networkErrorGroupCount: groupNetworkAlerts(httpErrors).length,
      pageResponseErrorGroupCount: groupNetworkAlerts(pageResponseErrors).length,
      requestFailedGroupCount: groupNetworkAlerts(requestFailed).length,
      significantRequestFailedGroupCount: groupNetworkAlerts(significantRequestFailed).length,
      executionErrorGroupCount: groupExecutionErrors(executionErrors).length,
      baselineExecutionErrorGroupCount: groupExecutionErrors(baselineExecutionErrors).length,
      consoleAlertGroupCount: groupConsoleAlerts(consoleAlerts).length,
      significantConsoleAlertGroupCount: groupConsoleAlerts(significantConsoleAlerts).length
    },
    networkHttpErrorsByPath: groupNetworkAlerts(httpErrors),
    networkPageResponseErrorsByPath: groupNetworkAlerts(pageResponseErrors),
    networkNonBlockingHttpErrorsByPath: groupNetworkAlerts(nonBlockingHttpErrors),
    networkRequestFailedByPath: groupNetworkAlerts(requestFailed),
    networkSignificantRequestFailedByPath: groupNetworkAlerts(significantRequestFailed),
    webPerformancePayloadStates,
    webPerformanceRuntimeDiagnostics: webPerformanceDiagnostics.slice(0, 120),
    webPerformanceRuntimeDiagnosticsByCode: webPerformanceDiagnosticGroups,
    domDiagnostics: domDiagnostics.slice(-80),
    domDiagnosticsByText: groupDomDiagnostics(domDiagnostics),
    domDiagnosticsByFingerprint: groupDomDiagnostics(domDiagnostics).slice(0, 80),
    runtimeExecutionErrors: executionErrors.slice(0, 120),
    runtimeExecutionErrorsByCode: groupExecutionErrors(executionErrors),
    baselineRuntimeExecutionErrors: baselineExecutionErrors.slice(0, 80),
    baselineRuntimeExecutionErrorsByCode: groupExecutionErrors(baselineExecutionErrors),
    consoleAlerts: consoleAlerts.slice(0, 80),
    consoleAlertsByPath: groupConsoleAlerts(consoleAlerts),
    significantConsoleAlerts: significantConsoleAlerts.slice(0, 80),
    significantConsoleAlertsByPath: groupConsoleAlerts(significantConsoleAlerts),
    pageErrors: pageErrors.slice(0, 40)
  };
}

function groupDomDiagnostics(events) {
  const groups = new Map();
  for (const item of events || []) {
    const preview = String(item?.preview || "").trim();
    if (!isReportableDomDiagnostic(item, preview)) continue;
    const normalizedPreview = normalizeDiagnosticPreview(preview);
    const key = [
      item?.diagnosticCode || "",
      normalizedPreview
    ].join("|");
    const existing = groups.get(key) || {
      source: item?.source || null,
      sources: new Set(),
      diagnosticCode: item?.diagnosticCode || null,
      textHash: item?.textHash || null,
      normalizedPreview,
      preview,
      count: 0,
      firstAt: item?.ts || null,
      lastAt: item?.ts || null,
      promptIndexes: new Set(),
      traceIds: new Set(),
      sampleSeqs: []
    };
    if (item?.source) existing.sources.add(String(item.source));
    existing.count += 1;
    existing.firstAt = minIso(existing.firstAt, item?.ts || null);
    existing.lastAt = maxIso(existing.lastAt, item?.ts || null);
    if (Number.isFinite(Number(item?.promptIndex))) existing.promptIndexes.add(Number(item.promptIndex));
    for (const traceId of extractDiagnosticTraceIds(item, preview)) existing.traceIds.add(traceId);
    if (existing.sampleSeqs.length < 12 && item?.seq !== undefined && item?.seq !== null) existing.sampleSeqs.push(item.seq);
    groups.set(key, existing);
  }
  return Array.from(groups.values())
    .map((item) => ({
      source: item.source,
      sources: Array.from(item.sources).sort(),
      diagnosticCode: item.diagnosticCode,
      textHash: item.textHash,
      normalizedPreview: item.normalizedPreview,
      preview: item.preview,
      count: item.count,
      firstAt: item.firstAt,
      lastAt: item.lastAt,
      promptIndexes: Array.from(item.promptIndexes).sort((a, b) => a - b),
      traceIds: Array.from(item.traceIds).sort(),
      sampleSeqs: item.sampleSeqs
    }))
    .sort((a, b) => (b.count - a.count) || String(a.firstAt || "").localeCompare(String(b.firstAt || "")));
}

function extractWebPerformanceRuntimeDiagnostics(network, promptTimes) {
  const rows = [];
  for (const item of Array.isArray(network) ? network : []) {
    const payload = item?.webPerformancePayload && typeof item.webPerformancePayload === "object" ? item.webPerformancePayload : null;
    if (!payload || payload.parseStatus !== "parsed" || payload.schemaVersion !== "hwlab-web-performance-v2") continue;
    const events = Array.isArray(payload.events) ? payload.events : [];
    for (const event of events) {
      if (!event || typeof event !== "object") continue;
      const diagnosticCode = limitText(event.diagnosticCode || event.code || "", 120);
      const reason = limitText(event.rootCause || event.reason || "", 120);
      const eventType = limitText(event.eventType || event.type || event.kind || "", 120);
      if (!diagnosticCode && !reason && !eventType) continue;
      rows.push({
        ts: item.ts ?? event.ts ?? null,
        promptIndex: promptIndexForTs(promptTimes, item.ts ?? event.ts),
        pageRole: item.pageRole ?? null,
        pageId: item.pageId ?? null,
        commandId: item.commandId ?? null,
        method: item.method ?? null,
        urlPath: urlPath(item.url),
        schemaVersion: payload.schemaVersion,
        captureStatus: payload.captureStatus ?? null,
        parseStatus: payload.parseStatus ?? null,
        byteCount: numberOrNull(payload.byteCount),
        bodyHash: payload.bodyHash ?? null,
        eventType,
        diagnosticCode,
        reason,
        module: limitText(event.module || "", 120),
        recoveryAction: limitText(event.recoveryAction || "", 160),
        contractVersion: limitText(event.contractVersion || "", 80),
        realtimeAuthority: limitText(event.realtimeAuthority || "", 120),
        sinceOutboxSeq: numberOrNull(event.sinceOutboxSeq),
        syncCursorOutboxSeq: numberOrNull(event.syncCursorOutboxSeq),
        entityFamilyCount: numberOrNull(event.entityFamilyCount),
        entityFamilies: limitText(event.entityFamilies || "", 160),
        maxEntityVersion: numberOrNull(event.maxEntityVersion),
        projectionRevision: limitText(event.projectionRevision || "", 120),
        terminalSeal: typeof event.terminalSeal === "boolean" ? event.terminalSeal : null,
        detailProjection: typeof event.detailProjection === "boolean" ? event.detailProjection : null,
        traceId: event.traceId ?? null,
        sessionIdHash: event.sessionIdHash ?? null,
        eventIdHash: event.eventIdHash ?? null,
        eventCount: numberOrNull(event.eventCount),
        deliveredCount: numberOrNull(event.deliveredCount),
        chunkCount: numberOrNull(event.chunkCount),
        flushDurationMs: numberOrNull(event.flushDurationMs),
        droppedCount: numberOrNull(event.droppedCount),
        maxItemsPerChunk: numberOrNull(event.maxItemsPerChunk),
        maxChunkMs: numberOrNull(event.maxChunkMs),
        replacedByKey: typeof event.replacedByKey === "boolean" || typeof event.replacedByKey === "number" ? event.replacedByKey : null,
        replacedByKeyHash: event.replacedByKeyHash ?? null,
        valuesRedacted: true,
      });
    }
  }
  return rows.sort((left, right) => String(left.ts || "").localeCompare(String(right.ts || ""))).slice(-200);
}

function summarizeWebPerformancePayloadStates(network) {
  const stateCounts = new Map();
  let total = 0;
  let parsed = 0;
  let parseIssue = 0;
  let overLimit = 0;
  let invalidJson = 0;
  let missingBody = 0;
  let unsupportedSchema = 0;
  let capturedEventCount = 0;
  let storedEventCount = 0;
  for (const item of Array.isArray(network) ? network : []) {
    const payload = item?.webPerformancePayload && typeof item.webPerformancePayload === "object" ? item.webPerformancePayload : null;
    if (!payload) continue;
    total += 1;
    const state = String(payload.parseStatus || payload.captureStatus || "unknown");
    stateCounts.set(state, (stateCounts.get(state) || 0) + 1);
    if (payload.parseStatus === "parsed") parsed += 1;
    else parseIssue += 1;
    if (payload.parseStatus === "not-parsed-over-limit" || payload.captureStatus === "skipped-over-limit") overLimit += 1;
    if (payload.parseStatus === "invalid-json") invalidJson += 1;
    if (payload.parseStatus === "missing-body") missingBody += 1;
    if (payload.parseStatus === "unsupported-schema") unsupportedSchema += 1;
    if (Number.isFinite(Number(payload.eventCount))) capturedEventCount += Number(payload.eventCount);
    if (Number.isFinite(Number(payload.storedEventCount))) storedEventCount += Number(payload.storedEventCount);
  }
  return {
    total,
    parsed,
    parseIssue,
    overLimit,
    invalidJson,
    missingBody,
    unsupportedSchema,
    capturedEventCount,
    storedEventCount,
    states: Array.from(stateCounts.entries()).map(([state, count]) => ({ state, count })).sort((a, b) => b.count - a.count || a.state.localeCompare(b.state)),
    valuesRedacted: true,
  };
}

function groupWebPerformanceRuntimeDiagnostics(events) {
  const groups = new Map();
  for (const item of Array.isArray(events) ? events : []) {
    const key = [item.diagnosticCode || item.eventType || "-", item.reason || "-", item.module || "-"].join("|");
    const group = groups.get(key) || {
      diagnosticCode: item.diagnosticCode || null,
      reason: item.reason || null,
      module: item.module || null,
      eventType: item.eventType || null,
      count: 0,
      firstAt: item.ts || null,
      lastAt: item.ts || null,
      promptIndexes: new Set(),
      traceIds: new Set(),
      eventCount: 0,
      deliveredCount: 0,
      chunkCount: 0,
      droppedCount: 0,
      maxFlushDurationMs: null,
      maxItemsPerChunk: null,
      maxChunkMs: null,
      replacedByKeyCount: 0,
      examples: [],
    };
    group.count += 1;
    group.firstAt = minIso(group.firstAt, item.ts || null);
    group.lastAt = maxIso(group.lastAt, item.ts || null);
    if (Number.isFinite(Number(item.promptIndex))) group.promptIndexes.add(Number(item.promptIndex));
    if (item.traceId) group.traceIds.add(String(item.traceId));
    group.eventCount += Number(item.eventCount || 0);
    group.deliveredCount += Number(item.deliveredCount || 0);
    group.chunkCount += Number(item.chunkCount || 0);
    group.droppedCount += Number(item.droppedCount || 0);
    group.maxFlushDurationMs = maxRuntimeNumber(group.maxFlushDurationMs, item.flushDurationMs);
    group.maxItemsPerChunk = maxRuntimeNumber(group.maxItemsPerChunk, item.maxItemsPerChunk);
    group.maxChunkMs = maxRuntimeNumber(group.maxChunkMs, item.maxChunkMs);
    if (Number.isFinite(Number(item.replacedByKey))) group.replacedByKeyCount += Number(item.replacedByKey);
    else if (item.replacedByKey === true || item.replacedByKeyHash) group.replacedByKeyCount += 1;
    if (group.examples.length < 6) group.examples.push({
      ts: item.ts || null,
      traceId: item.traceId || null,
      eventCount: item.eventCount ?? null,
      recoveryAction: item.recoveryAction ?? null,
      sinceOutboxSeq: item.sinceOutboxSeq ?? null,
      syncCursorOutboxSeq: item.syncCursorOutboxSeq ?? null,
      contractVersion: item.contractVersion ?? null,
      realtimeAuthority: item.realtimeAuthority ?? null,
      entityFamilyCount: item.entityFamilyCount ?? null,
      entityFamilies: item.entityFamilies ?? null,
      maxEntityVersion: item.maxEntityVersion ?? null,
      projectionRevision: item.projectionRevision ?? null,
      terminalSeal: item.terminalSeal ?? null,
      detailProjection: item.detailProjection ?? null,
      deliveredCount: item.deliveredCount ?? null,
      chunkCount: item.chunkCount ?? null,
      flushDurationMs: item.flushDurationMs ?? null,
      droppedCount: item.droppedCount ?? null,
      maxItemsPerChunk: item.maxItemsPerChunk ?? null,
      maxChunkMs: item.maxChunkMs ?? null,
      replacedByKey: item.replacedByKey ?? null,
      replacedByKeyHash: item.replacedByKeyHash ?? null,
      valuesRedacted: true,
    });
    groups.set(key, group);
  }
  return Array.from(groups.values()).map((item) => ({
    diagnosticCode: item.diagnosticCode,
    reason: item.reason,
    module: item.module,
    eventType: item.eventType,
    count: item.count,
    firstAt: item.firstAt,
    lastAt: item.lastAt,
    promptIndexes: Array.from(item.promptIndexes).sort((a, b) => a - b),
    traceIds: Array.from(item.traceIds).sort().slice(0, 12),
    eventCount: item.eventCount,
    deliveredCount: item.deliveredCount,
    chunkCount: item.chunkCount,
    droppedCount: item.droppedCount,
    maxFlushDurationMs: item.maxFlushDurationMs,
    maxItemsPerChunk: item.maxItemsPerChunk,
    maxChunkMs: item.maxChunkMs,
    replacedByKeyCount: item.replacedByKeyCount,
    examples: item.examples,
    valuesRedacted: true,
  })).sort((left, right) => right.count - left.count || String(left.firstAt || "").localeCompare(String(right.firstAt || "")));
}

function maxRuntimeNumber(current, candidate) {
  const value = Number(candidate);
  if (!Number.isFinite(value)) return current ?? null;
  const existing = Number(current);
  return Number.isFinite(existing) ? Math.max(existing, value) : value;
}

function isReportableDomDiagnostic(item, preview) {
  if (item?.source === "diagnostic-node" || item?.source === "execution-row") return true;
  return /trace_id=|HTTP\s+\d{3}\b|Failed to load resource|ERR_[A-Z_]+|provider-unavailable|AgentRun error|超过\s*\d+\s*ms\s*无新活动|代理暂时无法连接上游|Trace 更新超时|加载失败/iu.test(String(preview || ""));
}

function normalizeDiagnosticPreview(text) {
  return String(text || "")
    .replace(/trace_id=[A-Za-z0-9_-]+/gu, "trace_id=:traceId")
    .replace(/\btrc_[A-Za-z0-9_-]+\b/gu, "trc_:traceId")
    .replace(/\bses_[A-Za-z0-9_-]+\b/gu, "ses_:sessionId")
    .replace(/\brun_[A-Za-z0-9_-]+\b/gu, "run_:runId")
    .replace(/\bcmd_[A-Za-z0-9_-]+\b/gu, "cmd_:commandId")
    .replace(/[!！]+$/gu, "")
    .replace(/\s+/gu, " ")
    .trim();
}

function extractDiagnosticTraceIds(item, preview) {
  const ids = new Set();
  if (item?.traceId) ids.add(String(item.traceId));
  const text = String(preview || "");
  for (const match of text.matchAll(/\btrc_[A-Za-z0-9_-]+\b/gu)) ids.add(match[0]);
  for (const match of text.matchAll(/trace_id=([A-Za-z0-9_-]+)/gu)) ids.add(match[1]);
  return ids;
}

function minIso(a, b) {
  if (!a) return b || null;
  if (!b) return a || null;
  return Date.parse(a) <= Date.parse(b) ? a : b;
}

function maxIso(a, b) {
  if (!a) return b || null;
  if (!b) return a || null;
  return Date.parse(a) >= Date.parse(b) ? a : b;
}

function sampleExecutionErrorCandidates(sample) {
  const candidates = [];
  const add = (source, items) => {
    if (!Array.isArray(items)) return;
    for (const item of items) {
      const text = String(item?.textPreview || item?.text || item?.preview || "").trim();
      if (!text) continue;
      if (!parseExecutionErrorText(text)) continue;
      candidates.push({
        source,
        text,
        traceId: item?.traceId ?? null,
        messageId: item?.messageId ?? null,
        status: item?.status ?? null
      });
    }
  };
  add("diagnostic-node", sample?.diagnostics);
  add("message", sample?.messages);
  add("trace-row", sample?.traceRows);
  add("turn", sample?.turns);
  const specific = candidates.filter((candidate) => {
    const parsed = parseExecutionErrorText(candidate.text);
    return parsed && parsed.code !== "error";
  });
  return specific.length > 0 ? specific : candidates;
}

function parseExecutionErrorText(text) {
  const value = String(text || "");
  const agentRunCodeMatch = value.match(/\bagentrun:error:([A-Za-z0-9_.:-]+)/u);
  const agentRunText = /\bAgentRun\s+error\b|\bagentrun:error:/iu.test(value);
  const providerUnavailable = /\bprovider[-_\s]*unavailable\b/iu.test(value);
  if (!agentRunCodeMatch && !agentRunText && !providerUnavailable) return null;
  const statusMatch = value.match(/\b(fail(?:ed)?|error|blocked|cancel(?:ed)?)\b/iu);
  const traceMatch = value.match(/\btrc_[A-Za-z0-9_-]+\b/u);
  const totalMatch = value.match(/\btotal\s*=\s*([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)\b/iu)
    || value.match(/总耗时\s*[:：]?\s*([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)/iu);
  const agentRunCode = cleanExecutionCode(agentRunCodeMatch?.[1] || "");
  const rawCode = agentRunCode ? "agentrun:error:" + agentRunCode : providerUnavailable ? "provider-unavailable" : "agentrun:error";
  return {
    backend: agentRunText || agentRunCodeMatch ? "agentrun" : "unknown",
    status: normalizeExecutionStatus(statusMatch?.[1] || "error"),
    code: agentRunCode || (providerUnavailable ? "provider-unavailable" : "error"),
    rawCode,
    totalSeconds: totalMatch ? parseClockDurationSeconds(totalMatch[1]) : null,
    traceId: traceMatch?.[0] || null
  };
}

function cleanExecutionCode(code) {
  const value = String(code || "").replace(/(?:AgentRun|Error|Failed).*$/u, "").replace(/[^A-Za-z0-9_.:-].*$/u, "");
  return value || null;
}

function normalizeExecutionStatus(status) {
  const value = String(status || "").toLowerCase();
  if (value === "failed") return "fail";
  if (value === "cancelled" || value === "canceled") return "cancel";
  return value || "error";
}

function parseClockDurationSeconds(value) {
  const parts = String(value || "").split(":").map((part) => Number(part));
  if (parts.length === 2 && parts.every(Number.isFinite)) return parts[0] * 60 + parts[1];
  if (parts.length === 3 && parts.every(Number.isFinite)) return parts[0] * 3600 + parts[1] * 60 + parts[2];
  return null;
}

function groupExecutionErrors(events) {
  const groups = new Map();
  for (const event of events) {
    const key = [event.backend || "-", event.status || "-", event.code || "-"].join(" ");
    const group = groups.get(key) || {
      backend: event.backend ?? null,
      status: event.status ?? null,
      code: event.code ?? null,
      rawCode: event.rawCode ?? null,
      count: 0,
      firstAt: event.ts,
      lastAt: event.ts,
      promptIndexes: [],
      traceIds: [],
      sources: []
    };
    group.count += 1;
    group.lastAt = event.ts;
    if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex);
    if (event.traceId && !group.traceIds.includes(event.traceId)) group.traceIds.push(event.traceId);
    if (event.source && !group.sources.includes(event.source)) group.sources.push(event.source);
    groups.set(key, group);
  }
  return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.code).localeCompare(String(b.code)));
}

function consoleAlertEvent(item, promptTimes) {
  const text = String(item?.text || "");
  const statusMatch = text.match(/\bstatus\s+of\s+([1-5][0-9]{2})\b/iu) || text.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu);
  const location = compactLocation(item.location);
  const traceMatch = (location?.urlPath || text).match(/\btrc_[A-Za-z0-9_-]+\b/u);
  return {
    ts: item.ts ?? null,
    promptIndex: promptIndexForTs(promptTimes, item.ts),
    type: item.type ?? null,
    status: statusMatch ? Number(statusMatch[1]) : null,
    urlPath: location?.urlPath || "-",
    traceId: traceMatch?.[0] || null,
    textHash: item.text ? sha256(item.text) : null,
    preview: limitText(text, 220),
    location
  };
}

function groupConsoleAlerts(events) {
  const groups = new Map();
  for (const event of events) {
    const key = [event.type || "-", event.status ?? "-", event.urlPath || "-"].join(" ");
    const group = groups.get(key) || {
      type: event.type ?? null,
      status: event.status ?? null,
      urlPath: event.urlPath || "-",
      count: 0,
      firstAt: event.ts,
      lastAt: event.ts,
      promptIndexes: [],
      traceIds: []
    };
    group.count += 1;
    group.lastAt = event.ts;
    if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex);
    if (event.traceId && !group.traceIds.includes(event.traceId)) group.traceIds.push(event.traceId);
    groups.set(key, group);
  }
  return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath)));
}

function isBenignLongLivedStreamClosureAlert(event) {
  if (event?.urlPath !== "/v1/workbench/events") return false;
  if (event.status !== null && event.status !== undefined && Number(event.status) > 0) return false;
  const text = String(event.failureKind || event.errorPreview || event.preview || "");
  return /ERR_NETWORK_CHANGED|ERR_ABORTED|net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|aborted|network changed/iu.test(text);
}

function isObserverRefreshClosureAlert(event, observerRefreshTimes) {
  const urlPath = String(event?.urlPath || "");
  if (!["/v1/workbench/events", "/v1/web-performance"].includes(urlPath) && !/^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(urlPath)) return false;
  if (event.status !== null && event.status !== undefined && Number(event.status) > 0) return false;
  const text = String(event.failureKind || event.errorPreview || event.preview || "");
  if (!/ERR_NETWORK_CHANGED|ERR_ABORTED|ERR_INCOMPLETE_CHUNKED_ENCODING|ERR_INVALID_CHUNKED_ENCODING|net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|aborted|network changed|incomplete chunked|invalid chunked/iu.test(text)) return false;
  const ts = Date.parse(String(event.ts || ""));
  return Number.isFinite(ts) && observerRefreshTimes.some((refreshTs) => Math.abs(ts - refreshTs) <= 8000);
}

function networkAlertEvent(item, promptTimes) {
  const failureText = item.failureKind ?? item.failure ?? item.errorText ?? null;
  return {
    ts: item.ts ?? null,
    promptIndex: promptIndexForTs(promptTimes, item.ts),
    method: String(item.method || "GET").toUpperCase(),
    status: Number.isFinite(Number(item.status)) ? Number(item.status) : null,
    type: item.type ?? null,
    urlPath: urlPath(item.url),
    urlHash: item.url ? sha256(item.url) : null,
    failureKind: failureText ? String(failureText) : null,
    errorTextHash: failureText ? sha256(failureText) : null,
    errorPreview: failureText ? limitText(failureText, 160) : null
  };
}

function groupNetworkAlerts(events) {
  const groups = new Map();
  for (const event of events) {
    const key = [event.method, event.urlPath, event.status ?? "-", event.type].join(" ");
    const group = groups.get(key) || {
      method: event.method,
      urlPath: event.urlPath,
      status: event.status,
      type: event.type,
      count: 0,
      firstAt: event.ts,
      lastAt: event.ts,
      promptIndexes: [],
      failureKinds: [],
      errorTextHashes: []
    };
    group.count += 1;
    group.lastAt = event.ts;
    if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex);
    if (event.failureKind && !group.failureKinds.includes(event.failureKind)) group.failureKinds.push(event.failureKind);
    if (event.errorTextHash && !group.errorTextHashes.includes(event.errorTextHash)) group.errorTextHashes.push(event.errorTextHash);
    groups.set(key, group);
  }
  return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath)));
}

function isDiagnosticText(text) {
  const value = String(text || "");
  return /Failed to (?:fetch|load resource)|request failed|net::ERR_[A-Z0-9_:-]+|server responded with a status of [45][0-9]{2}|HTTP\s+[45][0-9]{2}\b|trace_id=|workbench turn\s*超过|turn\s*超过|无新活动|idle\s+\d+s|waitingFor=|lastEventLabel=|无法连接上游|代理暂时无法连接上游|provider-unavailable|agentrun:error|AgentRun error|projection-resume|sync-failed|durable projection store|realtime-gap|Trace 更新超时|加载失败|请求失败|请求已失败/iu.test(value);
}
`;
}
