fix: detect workbench in-place projection stalls
This commit is contained in:
@@ -234,9 +234,10 @@ function attachPassiveListeners(targetPage, pageRole = "control", targetPageId =
|
||||
});
|
||||
targetPage.on("response", (response) => {
|
||||
const request = response.request();
|
||||
void appendJsonl(files.network, eventRecord("response", {
|
||||
const base = {
|
||||
pageRole,
|
||||
pageId: targetPageId,
|
||||
sampleSeq,
|
||||
observerInitiated: false,
|
||||
commandId: activeCommandId,
|
||||
method: request.method(),
|
||||
@@ -245,8 +246,20 @@ function attachPassiveListeners(targetPage, pageRole = "control", targetPageId =
|
||||
status: response.status(),
|
||||
statusText: response.statusText(),
|
||||
fromServiceWorker: response.fromServiceWorker(),
|
||||
bodyRead: false,
|
||||
}));
|
||||
};
|
||||
void (async () => {
|
||||
const bodyFields = await summarizeWorkbenchResponseBody(response, request);
|
||||
await appendJsonl(files.network, eventRecord("response", { ...base, ...bodyFields }));
|
||||
})().catch((error) => appendJsonl(files.errors, eventRecord("response-body-summary-error", {
|
||||
pageRole,
|
||||
pageId: targetPageId,
|
||||
sampleSeq,
|
||||
commandId: activeCommandId,
|
||||
method: request.method(),
|
||||
url: safeUrl(response.url()),
|
||||
error: errorSummary(error),
|
||||
valuesRedacted: true
|
||||
})));
|
||||
});
|
||||
targetPage.on("requestfailed", (request) => {
|
||||
void appendJsonl(files.network, eventRecord("requestfailed", {
|
||||
@@ -3871,6 +3884,171 @@ function eventRecord(type, 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 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;
|
||||
if (isLikelyWorkbenchFinalTextField(key, parent, node)) {
|
||||
counters.finalTextFieldCount += 1;
|
||||
counters.finalTextByteCount += Buffer.byteLength(node);
|
||||
}
|
||||
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;
|
||||
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;
|
||||
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),
|
||||
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 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(),
|
||||
|
||||
Reference in New Issue
Block a user