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
43 changes: 43 additions & 0 deletions actions/setup/js/parse_codex_log.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,17 @@ function convertToLogEntries(parsedData) {
return logEntries;
}

/**
* Extract the model name from Codex log header lines.
* Codex logs include a line like "model: o4-mini" near the top.
* @param {string} logContent - The raw log content
* @returns {string|null} The model name, or null if not found
*/
function extractCodexModel(logContent) {
const match = logContent.match(/^model:\s*(.+)$/m);
return match ? match[1].trim() : null;
}

/**
* Parse codex log content and format as markdown
* @param {string} logContent - The raw log content to parse
Expand Down Expand Up @@ -598,6 +609,37 @@ function parseCodexLog(logContent) {
// Convert parsed data to logEntries format
const logEntries = convertToLogEntries(parsedData);

// Always prepend a system init entry so the session preview is shown even for
// failed or sparse runs (matches behaviour of Claude, Copilot, and Gemini parsers).
const model = extractCodexModel(logContent);
logEntries.unshift({
type: "system",
subtype: "init",
model: model || undefined,
});

// When there are no tool calls or thinking entries, surface error messages in the
// preview so users can see why the session failed.
const hasConversationEntries = logEntries.some(e => e.type !== "system");
if (!hasConversationEntries && errorInfo.hasErrors) {
for (const message of errorInfo.messages) {
logEntries.push({
type: "assistant",
message: {
content: [{ type: "text", text: message }],
},
});
}
if (errorInfo.reconnectCount > 0) {
logEntries.push({
type: "assistant",
message: {
content: [{ type: "text", text: `Reconnect attempts: ${errorInfo.reconnectCount}/${errorInfo.maxReconnects}` }],
},
});
}
}

// Check for MCP failures
const mcpFailures = mcpInfo.servers.filter(server => server.status === "failed").map(server => server.name);

Expand Down Expand Up @@ -711,5 +753,6 @@ if (typeof module !== "undefined" && module.exports) {
formatCodexBashCall,
extractMCPInitialization,
extractCodexErrorMessages,
extractCodexModel,
};
}
103 changes: 103 additions & 0 deletions actions/setup/js/parse_codex_log.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -658,4 +658,107 @@ github.list_pull_requests(...) success in 123ms:
expect(errorsIndex).toBeLessThan(reasoningIndex);
});
});

describe("session preview (logEntries always populated)", () => {
let extractCodexModel;

beforeEach(async () => {
const module = await import("./parse_codex_log.cjs");
extractCodexModel = module.extractCodexModel;
});

it("should always include a system init entry", () => {
const result = parseCodexLog("thinking\nsome thinking here");

const initEntry = result.logEntries.find(e => e.type === "system" && e.subtype === "init");
expect(initEntry).toBeDefined();
});

it("should extract model from Codex log header", () => {
const logContent = `OpenAI Codex v1.0
--------
workdir: /tmp/test
model: o4-mini
provider: openai`;

const model = extractCodexModel(logContent);
expect(model).toBe("o4-mini");
});

it("should include model in system init entry when present in log", () => {
const logContent = `model: gpt-4o
thinking
Some analysis here`;

const result = parseCodexLog(logContent);

const initEntry = result.logEntries.find(e => e.type === "system" && e.subtype === "init");
expect(initEntry).toBeDefined();
expect(initEntry.model).toBe("gpt-4o");
});

it("should still include system init entry when model is absent from log", () => {
const logContent = `thinking
Some analysis here`;

const result = parseCodexLog(logContent);

const initEntry = result.logEntries.find(e => e.type === "system" && e.subtype === "init");
expect(initEntry).toBeDefined();
expect(initEntry.model).toBeUndefined();
});

it("should add error messages as assistant entries when there are no tool calls", () => {
const logContent = `model: o4-mini
ERROR: cyber_policy_violation`;

const result = parseCodexLog(logContent);

const assistantEntries = result.logEntries.filter(e => e.type === "assistant");
expect(assistantEntries.length).toBeGreaterThan(0);
const textContent = assistantEntries.flatMap(e => e.message?.content || []).find(c => c.type === "text");
expect(textContent).toBeDefined();
expect(textContent.text).toContain("cyber_policy_violation");
});

it("should add reconnect count as assistant entry when no tool calls and reconnects occurred", () => {
const logContent = `Reconnecting... 1/3 (connection lost)
Reconnecting... 2/3 (connection lost)
ERROR: connection lost`;

const result = parseCodexLog(logContent);

const assistantEntries = result.logEntries.filter(e => e.type === "assistant");
const textContents = assistantEntries.flatMap(e => e.message?.content || []).filter(c => c.type === "text");
const reconnectEntry = textContents.find(c => c.text.includes("Reconnect attempts:"));
expect(reconnectEntry).toBeDefined();
expect(reconnectEntry.text).toContain("2/3");
});

it("should not add error assistant entries when tool calls are present", () => {
const logContent = `ERROR: some error
tool github.list_issues({})
github.list_issues(...) success in 50ms:
{"items":[]}`;

const result = parseCodexLog(logContent);

const assistantEntries = result.logEntries.filter(e => e.type === "assistant");
const toolUseEntries = assistantEntries.filter(e => e.message?.content?.some(c => c.type === "tool_use"));
expect(toolUseEntries.length).toBeGreaterThan(0);

// Error messages should NOT be added as extra assistant text entries
const errorTextEntries = assistantEntries.filter(e => e.message?.content?.some(c => c.type === "text" && c.text.includes("some error")));
expect(errorTextEntries.length).toBe(0);
});

it("should have non-empty logEntries for a failed run with only error output", () => {
const logContent = `model: o4-mini
ERROR: This user's access to o4-mini has been temporarily limited`;

const result = parseCodexLog(logContent);

expect(result.logEntries.length).toBeGreaterThan(0);
});
});
});
Loading