From b9ced502c652bfb0ea7cf0d96c9dc233866843d3 Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:11:23 +0530 Subject: [PATCH] fix(session): keep the text of a Codex tool output given as a list --- src/__tests__/session-cmd.test.ts | 31 ++++++++++++++++++++++++++++++ src/session-flow/adapters/codex.ts | 15 ++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/__tests__/session-cmd.test.ts b/src/__tests__/session-cmd.test.ts index 7d885e0e0..abd4172d4 100644 --- a/src/__tests__/session-cmd.test.ts +++ b/src/__tests__/session-cmd.test.ts @@ -1621,4 +1621,35 @@ describe('codex readSession: freeform tool calls', () => { const call = session.messages.flatMap((m) => m.content).find((b) => b.type === 'tool_call'); expect(call).toMatchObject({ type: 'tool_call', toolName: 'edit_file', callId: 'call_1', arguments: { input: patch } }); }); + + it('keeps the text of a tool output given as a list of content items', async () => { + const SID = '019e0000-1111-7222-8333-555555555555'; + const dir = path.join(mocks.home, '.codex', 'sessions', '2026', '10', '01'); + fs.mkdirSync(dir, { recursive: true }); + const lines = [ + { type: 'session_meta', payload: { id: SID, timestamp: '2026-10-01T10:00:00.000Z', cwd: '/work/app' } }, + { type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'look' }] } }, + { type: 'response_item', payload: { type: 'function_call', call_id: 'call_1', name: 'view_image', arguments: '{"path":"a.png"}' } }, + { + type: 'response_item', + payload: { + type: 'function_call_output', + call_id: 'call_1', + output: [ + { type: 'input_text', text: 'Output:' }, + { type: 'input_image', image_url: 'data:image/png;base64,iVBORw0KGgo=' }, + { type: 'input_text', text: 'done' }, + ], + }, + }, + ]; + fs.writeFileSync( + path.join(dir, `rollout-2026-10-01T10-00-00-${SID}.jsonl`), + lines.map((l) => JSON.stringify({ timestamp: '2026-10-01T10:00:00.000Z', ...l })).join('\n') + '\n', + ); + + const session = await new CodexAdapter().readSession({ sessionId: SID }); + const result = session.messages.flatMap((m) => m.content).find((b) => b.type === 'tool_result'); + expect(result).toMatchObject({ type: 'tool_result', callId: 'call_1', content: 'Output:\ndone' }); + }); }); diff --git a/src/session-flow/adapters/codex.ts b/src/session-flow/adapters/codex.ts index 8bda6c9e8..c2d1185df 100644 --- a/src/session-flow/adapters/codex.ts +++ b/src/session-flow/adapters/codex.ts @@ -556,7 +556,20 @@ export class CodexAdapter extends AgentAdapter { } } else if (ptype === 'function_call_output' || ptype === 'custom_tool_call_output') { const callId = String(payload.call_id ?? ''); - const output = String(payload.output ?? ''); + const rawOutput = payload.output; + let output: string; + if (Array.isArray(rawOutput)) { + // Some tools (images, the JS REPL) return a list of content items instead of a string + const parts: string[] = []; + for (const item of rawOutput) { + if (item && typeof item === 'object' && (item as Record).type === 'input_text') { + parts.push(String((item as Record).text ?? '')); + } + } + output = parts.join('\n'); + } else { + output = String(rawOutput ?? ''); + } const block: ToolResultBlock = { type: 'tool_result', callId, content: output, isError: false }; if (messages.length > 0 && messages[messages.length - 1].role === 'user') {