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
49 changes: 49 additions & 0 deletions actions/setup/js/parse_codex_log.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,42 @@ function extractMCPInitialization(lines) {
};
}

/**
* Extract error messages from Codex logs (e.g., model access blocked, cyber_policy_violation)
* @param {string[]} lines - Array of log lines
* @returns {{hasErrors: boolean, messages: string[], reconnectCount: number, maxReconnects: number}} Error info
*/
function extractCodexErrorMessages(lines) {
const messages = new Set();
let reconnectCount = 0;
let maxReconnects = 0;

for (const line of lines) {
// Match: ERROR: <message> (final error after all retries exhausted)
const errorMatch = line.match(/^ERROR:\s*(.+)$/);
if (errorMatch) {
messages.add(errorMatch[1].trim());
}

// Match: Reconnecting... N/M (error message) - reconnect attempts with error details
const reconnectMatch = line.match(/^Reconnecting\.\.\.\s+(\d+)\/(\d+)\s*\((.+)\)$/);
if (reconnectMatch) {
const attempt = parseInt(reconnectMatch[1]);
const total = parseInt(reconnectMatch[2]);
Comment on lines +145 to +146
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractCodexErrorMessages uses parseInt(...) without a radix. It’s safer and consistent to pass base 10 (or use Number(...)) even though the regex only matches digits.

Suggested change
const attempt = parseInt(reconnectMatch[1]);
const total = parseInt(reconnectMatch[2]);
const attempt = parseInt(reconnectMatch[1], 10);
const total = parseInt(reconnectMatch[2], 10);

Copilot uses AI. Check for mistakes.
if (attempt > reconnectCount) reconnectCount = attempt;
if (total > maxReconnects) maxReconnects = total;
messages.add(reconnectMatch[3].trim());
}
Comment on lines +135 to +150
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractCodexErrorMessages uses an end-anchored reconnect regex (...\((.+)\)$). If log lines have trailing whitespace or CRLF (\r), reconnect lines like Reconnecting... 1/5 (...)\r won't match and retries won’t be surfaced. Consider normalizing each line with trimEnd() before matching, or relaxing the pattern to allow optional whitespace/\r after the closing ) (e.g. \)\s*$).

Copilot uses AI. Check for mistakes.
}

return {
hasErrors: messages.size > 0,
messages: Array.from(messages),
reconnectCount,
maxReconnects,
};
}

/**
* Convert parsed Codex data to logEntries format for plain text rendering
* @param {Array<{type: string, content?: string, toolName?: string, params?: string, response?: string, statusIcon?: string}>} parsedData - Parsed Codex log data
Expand Down Expand Up @@ -255,6 +291,18 @@ function parseCodexLog(logContent) {
markdown += mcpInfo.markdown;
}

// Extract error messages (e.g., model access blocked, cyber_policy_violation)
const errorInfo = extractCodexErrorMessages(lines);
if (errorInfo.hasErrors) {
markdown += "## ⚠️ Errors\n\n";
for (const message of errorInfo.messages) {
markdown += `> ${message}\n\n`;
}
if (errorInfo.reconnectCount > 0) {
markdown += `> Reconnect attempts: ${errorInfo.reconnectCount}/${errorInfo.maxReconnects}\n\n`;
}
}
Comment on lines +294 to +304
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseCodexLog now surfaces ERROR: / Reconnecting... lines in an Errors section, but those same lines can still be treated as “thinking” content if they appear after a thinking marker (they’re not in the skip list). That can duplicate error text under ## 🤖 Reasoning. Consider skipping these patterns in the main parsing loop (e.g., extend the metadata skip condition to include line.startsWith("ERROR:") / line.startsWith("Reconnecting...")) once they’ve been extracted.

Copilot uses AI. Check for mistakes.

markdown += "## 🤖 Reasoning\n\n";

// Second pass: process full conversation flow with interleaved reasoning and tools
Expand Down Expand Up @@ -662,5 +710,6 @@ if (typeof module !== "undefined" && module.exports) {
formatCodexToolCall,
formatCodexBashCall,
extractMCPInitialization,
extractCodexErrorMessages,
};
}
116 changes: 116 additions & 0 deletions actions/setup/js/parse_codex_log.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -542,4 +542,120 @@ I will analyze the code`;
expect(result.markdown).toContain("## 🤖 Reasoning");
});
});

describe("extractCodexErrorMessages function", () => {
let extractCodexErrorMessages;

beforeEach(async () => {
const module = await import("./parse_codex_log.cjs");
extractCodexErrorMessages = module.extractCodexErrorMessages;
});
Comment on lines +549 to +552
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The outer beforeEach already imports ./parse_codex_log.cjs for every test; this nested beforeEach re-imports the same module again, doubling work for this describe block. Consider wiring extractCodexErrorMessages into the existing outer setup (add a let extractCodexErrorMessages; alongside the other helpers) and remove this nested import.

Copilot uses AI. Check for mistakes.

it("should extract ERROR: lines", () => {
const lines = [
"thinking",
"Some thinking content here",
"ERROR: stream disconnected before completion: This user's access to gpt-5.3-codex has been temporarily limited for potentially suspicious activity related to cybersecurity. Learn more about our safety mitigations: https://platform.openai.com/docs/guides/safety-checks/cybersecurity",
];

const result = extractCodexErrorMessages(lines);

expect(result.hasErrors).toBe(true);
expect(result.messages).toHaveLength(1);
expect(result.messages[0]).toContain("stream disconnected before completion");
expect(result.messages[0]).toContain("cyber");
});

it("should extract Reconnecting messages with error details", () => {
const lines = [
"Reconnecting... 1/5 (stream disconnected before completion: This user's access to gpt-5.3-codex has been temporarily limited for potentially suspicious activity related to cybersecurity. Learn more about our safety mitigations: https://platform.openai.com/docs/guides/safety-checks/cybersecurity)",
"Reconnecting... 2/5 (stream disconnected before completion: This user's access to gpt-5.3-codex has been temporarily limited for potentially suspicious activity related to cybersecurity. Learn more about our safety mitigations: https://platform.openai.com/docs/guides/safety-checks/cybersecurity)",
];

const result = extractCodexErrorMessages(lines);

expect(result.hasErrors).toBe(true);
expect(result.messages).toHaveLength(1); // De-duplicated
expect(result.reconnectCount).toBe(2);
expect(result.maxReconnects).toBe(5);
});

it("should de-duplicate identical error messages from retries", () => {
const errorMsg = "stream disconnected before completion: This user's access to gpt-5.3-codex has been temporarily limited";
const lines = [`Reconnecting... 1/5 (${errorMsg})`, `Reconnecting... 2/5 (${errorMsg})`, `Reconnecting... 3/5 (${errorMsg})`, `ERROR: ${errorMsg}`];

const result = extractCodexErrorMessages(lines);

expect(result.hasErrors).toBe(true);
expect(result.messages).toHaveLength(1);
expect(result.reconnectCount).toBe(3);
});

it("should return no errors for normal log lines", () => {
const lines = ["thinking", "I will list the open pull requests", "tool github.list_pull_requests({})"];

const result = extractCodexErrorMessages(lines);

expect(result.hasErrors).toBe(false);
expect(result.messages).toHaveLength(0);
expect(result.reconnectCount).toBe(0);
expect(result.maxReconnects).toBe(0);
});

it("should handle empty lines array", () => {
const result = extractCodexErrorMessages([]);

expect(result.hasErrors).toBe(false);
expect(result.messages).toHaveLength(0);
});
});

describe("parseCodexLog with model access errors", () => {
it("should include Errors section when ERROR: line is present", () => {
const logContent = `2026-02-27T14:06:41.886993Z INFO session_loop: codex_core::codex: Turn error: stream disconnected before completion: This user's access to gpt-5.3-codex has been temporarily limited for potentially suspicious activity related to cybersecurity. Learn more about our safety mitigations: https://platform.openai.com/docs/guides/safety-checks/cybersecurity
ERROR: stream disconnected before completion: This user's access to gpt-5.3-codex has been temporarily limited for potentially suspicious activity related to cybersecurity. Learn more about our safety mitigations: https://platform.openai.com/docs/guides/safety-checks/cybersecurity`;

const result = parseCodexLog(logContent);

expect(result.markdown).toContain("## ⚠️ Errors");
expect(result.markdown).toContain("stream disconnected before completion");
expect(result.markdown).toContain("cybersecurity");
});

it("should include Errors section with reconnect count when retries occurred", () => {
const logContent = `Reconnecting... 1/5 (stream disconnected before completion: This user's access to gpt-5.3-codex has been temporarily limited for potentially suspicious activity related to cybersecurity)
Reconnecting... 2/5 (stream disconnected before completion: This user's access to gpt-5.3-codex has been temporarily limited for potentially suspicious activity related to cybersecurity)
Reconnecting... 3/5 (stream disconnected before completion: This user's access to gpt-5.3-codex has been temporarily limited for potentially suspicious activity related to cybersecurity)
ERROR: stream disconnected before completion: This user's access to gpt-5.3-codex has been temporarily limited for potentially suspicious activity related to cybersecurity`;

const result = parseCodexLog(logContent);

expect(result.markdown).toContain("## ⚠️ Errors");
expect(result.markdown).toContain("Reconnect attempts: 3/5");
});

it("should not include Errors section for normal logs", () => {
const logContent = `thinking
I will list the open pull requests
tool github.list_pull_requests({"state":"open"})
github.list_pull_requests(...) success in 123ms:
{"items": []}`;

const result = parseCodexLog(logContent);

expect(result.markdown).not.toContain("## ⚠️ Errors");
});

it("should place Errors section before Reasoning section", () => {
const logContent = `ERROR: This user's access to gpt-5.3-codex has been temporarily limited`;

const result = parseCodexLog(logContent);

const errorsIndex = result.markdown.indexOf("## ⚠️ Errors");
const reasoningIndex = result.markdown.indexOf("## 🤖 Reasoning");
expect(errorsIndex).toBeGreaterThan(-1);
expect(reasoningIndex).toBeGreaterThan(-1);
expect(errorsIndex).toBeLessThan(reasoningIndex);
});
});
});
Loading