fix: detect workbench in-place projection stalls

This commit is contained in:
Codex
2026-06-27 08:08:12 +00:00
parent 35cb49f319
commit 2a3d0a4657
2 changed files with 419 additions and 3 deletions
@@ -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(),