Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions scripts/lib/agent-debug-trace.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { homedir } from "node:os";
import { join, resolve } from "node:path";

const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
const DEFAULT_REASONING_COALESCE_BYTES = 16 * 1024;
const TRACE_KIND = "github-delivery/agent-debug-trace-event";
const PROVIDERS = new Set(["codex", "grok", "cursor"]);
const ALLOWED_EVENT_TYPES = new Set([
Expand All @@ -21,6 +22,8 @@ const ALLOWED_EVENT_TYPES = new Set([
"turn_started",
"turn_completed",
]);
const ALLOWED_OUTCOMES = new Set(["succeeded", "failed", "cancelled"]);
const ERROR_KIND_RE = /^[a-z0-9][a-z0-9_.-]{0,63}$/i;

function cleanString(value) {
return typeof value === "string" && value.length > 0 ? value : null;
Expand All @@ -32,6 +35,20 @@ function checkedProvider(value) {
return provider;
}

function safeDuration(value) {
return Number.isFinite(value) && value >= 0 ? Math.round(value) : null;
}

function safeOutcome(value) {
const outcome = String(value || "").trim().toLowerCase();
return ALLOWED_OUTCOMES.has(outcome) ? outcome : null;
}

function safeErrorKind(value) {
const kind = String(value || "").trim();
return ERROR_KIND_RE.test(kind) ? kind : null;
}

function sanitizeEvent(event, provider, traceKind = TRACE_KIND, timestamp = new Date()) {
if (!event || typeof event !== "object") return null;
const type = cleanString(event.type);
Expand All @@ -54,6 +71,15 @@ function sanitizeEvent(event, provider, traceKind = TRACE_KIND, timestamp = new
sanitized.text = typeof event.text === "string" ? event.text : "";
}

if (type === "item_completed") {
const outcome = safeOutcome(event.outcome);
const durationMs = safeDuration(event.durationMs);
const errorKind = safeErrorKind(event.errorKind);
if (outcome) sanitized.outcome = outcome;
if (durationMs !== null) sanitized.durationMs = durationMs;
if (errorKind) sanitized.errorKind = errorKind;
}

const decision = cleanString(event.watchdogDecision);
if (decision) sanitized.watchdogDecision = decision;
if (typeof event.interrupted === "boolean") sanitized.interrupted = event.interrupted;
Expand Down Expand Up @@ -118,6 +144,10 @@ function byteLimit(value) {
return Number.isInteger(value) && value > 0 ? value : DEFAULT_MAX_BYTES;
}

function reasoningIdentity(event) {
return [event.threadId || "", event.turnId || "", event.itemId || ""].join("\0");
}

function disabledRecorder() {
return {
enabled: false,
Expand All @@ -140,11 +170,13 @@ export function createAgentDebugTraceRecorder({
pid = process.pid,
maxBytes = DEFAULT_MAX_BYTES,
traceKind = TRACE_KIND,
reasoningCoalesceBytes = DEFAULT_REASONING_COALESCE_BYTES,
} = {}) {
if (!debugTraceEnabled(env)) return disabledRecorder();

const normalizedProvider = checkedProvider(provider);
const limit = byteLimit(maxBytes);
const coalesceLimit = byteLimit(reasoningCoalesceBytes);
const root = traceRoot(env, stateDir);
const directory = join(root, "debug-traces");
ensurePrivateDirectory(root, "debug trace state directory");
Expand All @@ -155,11 +187,10 @@ export function createAgentDebugTraceRecorder({

let fd = opened.fd;
let bytesWritten = 0;
let pendingReasoning = null;

function record(event) {
function writeSanitized(sanitized) {
if (fd === null) return false;
const sanitized = sanitizeEvent(event, normalizedProvider, traceKind, now());
if (!sanitized) return false;
const line = `${JSON.stringify(sanitized)}\n`;
const bytes = Buffer.byteLength(line);
if (bytesWritten + bytes > limit) return false;
Expand All @@ -168,8 +199,40 @@ export function createAgentDebugTraceRecorder({
return true;
}

function flushReasoning() {
if (!pendingReasoning) return true;
const pending = pendingReasoning;
pendingReasoning = null;
return writeSanitized(pending);
}

function record(event) {
if (fd === null) return false;
const sanitized = sanitizeEvent(event, normalizedProvider, traceKind, now());
if (!sanitized) return false;

if (sanitized.type === "reasoning_summary_delta") {
const identity = reasoningIdentity(sanitized);
if (pendingReasoning && reasoningIdentity(pendingReasoning) === identity) {
const combined = `${pendingReasoning.text}${sanitized.text}`;
if (Buffer.byteLength(combined, "utf8") <= coalesceLimit) {
pendingReasoning.text = combined;
pendingReasoning.deltaCount += 1;
return true;
}
}
flushReasoning();
pendingReasoning = { ...sanitized, deltaCount: 1 };
return true;
}

flushReasoning();
return writeSanitized(sanitized);
}

function close() {
if (fd === null) return;
flushReasoning();
closeSync(fd);
fd = null;
}
Expand Down
22 changes: 22 additions & 0 deletions scripts/lib/codex-app-server-watchdog-proxy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,27 @@ function messageThreadId(message) {
return message?.params?.threadId || null;
}

function completionDiagnostics(message) {
const item = message?.params?.item || {};
const rawStatus = String(item?.status || message?.params?.status || "completed").toLowerCase();
const outcome = ["failed", "error"].includes(rawStatus)
? "failed"
: ["cancelled", "canceled"].includes(rawStatus)
? "cancelled"
: "succeeded";
const rawDuration = Number.isFinite(item?.durationMs)
? item.durationMs
: Number.isFinite(message?.params?.durationMs)
? message.params.durationMs
: item?.duration_ms;
const durationMs = Number.isFinite(rawDuration) && rawDuration >= 0 ? Math.round(rawDuration) : null;
return {
outcome,
...(durationMs !== null ? { durationMs } : {}),
...(outcome === "failed" ? { errorKind: "tool_failed" } : {}),
};
}

function emitTelemetry(options, message, outcome = null) {
if (typeof options.onTelemetry !== "function" || !message?.method) return;
const event = {
Expand Down Expand Up @@ -61,6 +82,7 @@ function debugTraceEvent(message, outcome = null) {
type: method === "item/started" ? "item_started" : "item_completed",
itemId: message?.params?.item?.id || message?.params?.itemId || null,
itemType: message?.params?.item?.type || null,
...(method === "item/completed" ? completionDiagnostics(message) : {}),
};
}

Expand Down
18 changes: 18 additions & 0 deletions scripts/lib/cursor-debug-trace.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ function text(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}

function durationMs(event) {
const value = Number.isFinite(event?.durationMs) ? event.durationMs : event?.duration_ms;
return Number.isFinite(value) && value >= 0 ? Math.round(value) : null;
}

function terminalDiagnostics(event, status) {
const outcome = ["completed", "complete"].includes(status) ? "succeeded" : status;
const duration = durationMs(event);
return {
outcome,
...(duration !== null ? { durationMs: duration } : {}),
...(status === "failed" ? { errorKind: "tool_failed" } : {}),
};
}

function commonCursorIds(event) {
const threadId = text(event?.conversation_id) || text(event?.session_id);
const turnId = text(event?.generation_id);
Expand Down Expand Up @@ -78,6 +93,7 @@ export function normalizeCursorCliDebugTraceEvent(event) {
type: "item_completed",
...ids,
...toolIdentity(event),
...terminalDiagnostics(event, subtype),
};
}

Expand Down Expand Up @@ -116,11 +132,13 @@ export function normalizeCursorHookDebugTraceEvent(event) {
}

if (hook === "postToolUse" || hook === "postToolUseFailure") {
const status = hook === "postToolUseFailure" ? "failed" : "completed";
return {
provider: "cursor",
type: "item_completed",
...ids,
...toolIdentity(event),
...terminalDiagnostics(event, status),
};
}

Expand Down
16 changes: 16 additions & 0 deletions scripts/lib/grok-debug-trace.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ function text(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}

function durationMs(event) {
const value = Number.isFinite(event?.durationMs) ? event.durationMs : event?.duration_ms;
return Number.isFinite(value) && value >= 0 ? Math.round(value) : null;
}

function terminalDiagnostics(event, status) {
const outcome = status === "completed" ? "succeeded" : status;
const duration = durationMs(event);
return {
outcome,
...(duration !== null ? { durationMs: duration } : {}),
...(status === "failed" ? { errorKind: "tool_failed" } : {}),
};
}

function hasOwnedOutputFormat(args) {
return args.some((arg) => arg === "--output-format" || String(arg).startsWith("--output-format="));
}
Expand Down Expand Up @@ -56,6 +71,7 @@ export function normalizeGrokDebugTraceEvent(event) {
type: "item_completed",
...(text(event.toolCallId) ? { itemId: event.toolCallId } : {}),
...(text(event.toolName) ? { itemType: event.toolName } : {}),
...terminalDiagnostics(event, status),
};
}

Expand Down
Loading
Loading