From b8cb6a39c1fb0355a4862041bc1b4190aa9e4881 Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Wed, 12 Aug 2026 14:39:27 +0200 Subject: [PATCH 1/3] fix(agent-manager): parse current Codex session messages --- .../__tests__/adapters/CodexAdapter.test.ts | 51 ++++++++++ .../src/adapters/CodexAdapter.ts | 97 +++++++++++++++---- 2 files changed, 127 insertions(+), 21 deletions(-) diff --git a/packages/agent-manager/src/__tests__/adapters/CodexAdapter.test.ts b/packages/agent-manager/src/__tests__/adapters/CodexAdapter.test.ts index 96a8d062..bf5af4be 100644 --- a/packages/agent-manager/src/__tests__/adapters/CodexAdapter.test.ts +++ b/packages/agent-manager/src/__tests__/adapters/CodexAdapter.test.ts @@ -1212,6 +1212,57 @@ describe('CodexAdapter', () => { expect(messages[1]).toEqual({ role: 'assistant', content: 'I found the issue', timestamp: '2026-03-27T10:00:05Z' }); }); + it('should parse Codex response_item message records', () => { + const filePath = writeJsonl([ + { type: 'session_meta', payload: { id: 'sess-1', cwd: '/repo', timestamp: '2026-03-27T10:00:00Z' } }, + { + type: 'response_item', + timestamp: '2026-03-27T10:00:01Z', + payload: { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'Fix the bug' }], + }, + }, + { + type: 'response_item', + timestamp: '2026-03-27T10:00:05Z', + payload: { + type: 'message', + role: 'assistant', + content: [{ type: 'output_text', text: 'I found the issue' }], + }, + }, + ]); + + const messages = adapter.getConversation(filePath); + expect(messages).toHaveLength(2); + expect(messages[0]).toEqual({ role: 'user', content: 'Fix the bug', timestamp: '2026-03-27T10:00:01Z' }); + expect(messages[1]).toEqual({ role: 'assistant', content: 'I found the issue', timestamp: '2026-03-27T10:00:05Z' }); + }); + + it('should parse completed Codex AgentMessage event records', () => { + const filePath = writeJsonl([ + { type: 'session_meta', payload: { id: 'sess-1', cwd: '/repo', timestamp: '2026-03-27T10:00:00Z' } }, + { + type: 'event_msg', + timestamp: '2026-03-27T10:00:05Z', + payload: { + type: 'item_completed', + item: { + type: 'AgentMessage', + content: [{ type: 'Text', text: 'I found the issue' }], + }, + }, + }, + ]); + + const messages = adapter.getConversation(filePath); + expect(messages).toEqual([ + { role: 'assistant', content: 'I found the issue', timestamp: '2026-03-27T10:00:05Z' }, + ]); + }); + it('should skip session_meta entry', () => { const filePath = writeJsonl([ { type: 'session_meta', payload: { id: 'sess-1', cwd: '/repo', timestamp: '2026-03-27T10:00:00Z' } }, diff --git a/packages/agent-manager/src/adapters/CodexAdapter.ts b/packages/agent-manager/src/adapters/CodexAdapter.ts index 3b9f8679..6fef93a5 100644 --- a/packages/agent-manager/src/adapters/CodexAdapter.ts +++ b/packages/agent-manager/src/adapters/CodexAdapter.ts @@ -37,9 +37,22 @@ interface CodexEventEntry { id?: string; cwd?: string; timestamp?: string; + role?: string; + content?: CodexContent[]; + item?: CodexItem; }; } +interface CodexContent { + type?: string; + text?: string; +} + +interface CodexItem { + type?: string; + content?: string | CodexContent[]; +} + interface CodexSession { sessionId: string; projectPath: string; @@ -638,33 +651,75 @@ export class CodexAdapter implements AgentAdapter { continue; } - if (entry.type === 'session_meta') continue; + const message = this.toConversationMessage(entry, verbose); + if (message) messages.push(message); + } - const payloadType = entry.payload?.type; - if (!payloadType) continue; + return messages; + } - let role: ConversationMessage['role']; - if (payloadType === 'user_message') { - role = 'user'; - } else if (payloadType === 'agent_message' || payloadType === 'task_complete') { - role = 'assistant'; - } else if (verbose) { - role = 'system'; - } else { - continue; - } + private toConversationMessage(entry: CodexEventEntry, verbose: boolean): ConversationMessage | null { + if (entry.type === 'session_meta') return null; - const text = entry.payload?.message?.trim(); - if (!text) continue; + const payloadType = entry.payload?.type; + if (entry.type === 'response_item' && payloadType === 'message') { + const role = this.mapCodexRole(entry.payload?.role, verbose); + const text = this.extractContentText(entry.payload?.content); + if (!role || !text) return null; - messages.push({ - role, - content: text, - timestamp: entry.timestamp, - }); + return { role, content: text, timestamp: entry.timestamp }; } - return messages; + if (entry.type === 'event_msg' && payloadType === 'item_completed') { + const item = entry.payload?.item; + const role = this.mapCodexItemRole(item?.type, verbose); + const text = this.extractContentText(item?.content); + if (!role || !text) return null; + + return { role, content: text, timestamp: entry.timestamp }; + } + + if (!payloadType) return null; + + let role: ConversationMessage['role']; + if (payloadType === 'user_message') { + role = 'user'; + } else if (payloadType === 'agent_message' || payloadType === 'task_complete') { + role = 'assistant'; + } else if (verbose) { + role = 'system'; + } else { + return null; + } + + const text = entry.payload?.message?.trim(); + if (!text) return null; + + return { role, content: text, timestamp: entry.timestamp }; + } + + private mapCodexRole(role: string | undefined, verbose: boolean): ConversationMessage['role'] | null { + if (role === 'user') return 'user'; + if (role === 'assistant') return 'assistant'; + return verbose ? 'system' : null; + } + + private mapCodexItemRole(itemType: string | undefined, verbose: boolean): ConversationMessage['role'] | null { + if (itemType === 'AgentMessage') return 'assistant'; + if (itemType === 'UserMessage') return 'user'; + return verbose ? 'system' : null; + } + + private extractContentText(content: string | CodexContent[] | undefined): string { + if (typeof content === 'string') return content.trim(); + if (!Array.isArray(content)) return ''; + + return content + .map((part) => part.text) + .filter((text): text is string => typeof text === 'string' && text.trim().length > 0) + .map((text) => text.trim()) + .join('\n') + .trim(); } async listSessions(opts?: ListSessionsOptions): Promise { From 0f0840eb3a821b5fa16a14e638b0b0c5f6010be9 Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Wed, 12 Aug 2026 20:41:26 +0200 Subject: [PATCH 2/3] fix(agent-manager): align Codex list parsing --- .../__tests__/adapters/CodexAdapter.test.ts | 46 +++++++++++++++++++ .../src/adapters/CodexAdapter.ts | 42 ++++++++++++++--- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/packages/agent-manager/src/__tests__/adapters/CodexAdapter.test.ts b/packages/agent-manager/src/__tests__/adapters/CodexAdapter.test.ts index bf5af4be..631a3ccc 100644 --- a/packages/agent-manager/src/__tests__/adapters/CodexAdapter.test.ts +++ b/packages/agent-manager/src/__tests__/adapters/CodexAdapter.test.ts @@ -1133,6 +1133,52 @@ describe('CodexAdapter', () => { expect(session.summary).toBe('Last message'); }); + it('should extract summary from current Codex response_item messages', () => { + const parseSession = (adapter as any).parseSession.bind(adapter); + const filePath = path.join(tmpDir, 'response-item-summary.jsonl'); + fs.writeFileSync(filePath, [ + JSON.stringify({ type: 'session_meta', payload: { id: 'sess-ri', timestamp: '2026-03-18T15:00:00Z', cwd: '/repo' } }), + JSON.stringify({ + type: 'response_item', + timestamp: '2026-03-18T15:01:00Z', + payload: { + type: 'message', + role: 'assistant', + content: [{ type: 'output_text', text: 'Parsed from current schema' }], + }, + }), + ].join('\n')); + + const session = parseSession(undefined, filePath); + expect(session.summary).toBe('Parsed from current schema'); + expect(session.lastPayloadType).toBe('agent_message'); + }); + + it('should treat completed Codex AgentMessage events as waiting for list status', () => { + const parseSession = (adapter as any).parseSession.bind(adapter); + const determineStatus = (adapter as any).determineStatus.bind(adapter); + const filePath = path.join(tmpDir, 'agent-message-status.jsonl'); + fs.writeFileSync(filePath, [ + JSON.stringify({ type: 'session_meta', payload: { id: 'sess-am', timestamp: '2026-03-18T15:00:00Z', cwd: '/repo' } }), + JSON.stringify({ + type: 'event_msg', + timestamp: new Date().toISOString(), + payload: { + type: 'item_completed', + item: { + type: 'AgentMessage', + content: [{ type: 'Text', text: 'Waiting for the user now' }], + }, + }, + }), + ].join('\n')); + + const session = parseSession(undefined, filePath); + expect(session.summary).toBe('Waiting for the user now'); + expect(session.lastPayloadType).toBe('agent_message'); + expect(determineStatus(session)).toBe(AgentStatus.WAITING); + }); + it('should handle malformed JSON lines gracefully', () => { const parseSession = (adapter as any).parseSession.bind(adapter); const filePath = path.join(tmpDir, 'malformed.jsonl'); diff --git a/packages/agent-manager/src/adapters/CodexAdapter.ts b/packages/agent-manager/src/adapters/CodexAdapter.ts index 6fef93a5..09fe9beb 100644 --- a/packages/agent-manager/src/adapters/CodexAdapter.ts +++ b/packages/agent-manager/src/adapters/CodexAdapter.ts @@ -515,7 +515,7 @@ export class CodexAdapter implements AgentAdapter { } const lastEntry = this.findLastEventEntry(entries); - const lastPayloadType = lastEntry?.payload?.type; + const lastPayloadType = lastEntry ? this.normalizedPayloadType(lastEntry) : undefined; const lastActive = this.parseTimestamp(lastEntry?.timestamp) || @@ -609,15 +609,44 @@ export class CodexAdapter implements AgentAdapter { private extractSummary(entries: CodexEventEntry[]): string { for (let i = entries.length - 1; i >= 0; i--) { - const message = entries[i]?.payload?.message; - if (typeof message === 'string' && message.trim().length > 0) { - return this.truncate(message.trim(), 120); - } + const message = this.extractEntryText(entries[i]); + if (message) return this.truncate(message, 120); } return 'Codex session active'; } + private normalizedPayloadType(entry: CodexEventEntry): string | undefined { + const payloadType = entry.payload?.type; + + if (entry.type === 'response_item' && payloadType === 'message') { + if (entry.payload?.role === 'assistant') return 'agent_message'; + if (entry.payload?.role === 'user') return 'user_message'; + return payloadType; + } + + if (entry.type === 'event_msg' && payloadType === 'item_completed') { + const itemType = entry.payload?.item?.type; + if (itemType === 'AgentMessage') return 'agent_message'; + if (itemType === 'UserMessage') return 'user_message'; + return itemType ?? payloadType; + } + + return payloadType; + } + + private extractEntryText(entry: CodexEventEntry | undefined): string { + if (!entry) return ''; + + const legacyMessage = entry.payload?.message; + if (typeof legacyMessage === 'string' && legacyMessage.trim().length > 0) { + return legacyMessage.trim(); + } + + const conversationMessage = this.toConversationMessage(entry, false); + return conversationMessage?.content.trim() ?? ''; + } + private truncate(value: string, maxLength: number): string { if (value.length <= maxLength) return value; return `${value.slice(0, maxLength - 3)}...`; @@ -632,7 +661,8 @@ export class CodexAdapter implements AgentAdapter { /** * Read the full conversation from a Codex session JSONL file. * - * Codex entries use payload.type to indicate message role and payload.message for content. + * Codex entries use either legacy payload.message fields or current + * response_item/event_msg content arrays. */ getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] { const verbose = options?.verbose ?? false; From f783fa51b261d25c2d2d431f7ba81ba72cf4b66f Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Wed, 12 Aug 2026 20:47:44 +0200 Subject: [PATCH 3/3] fix(agent-manager): dedupe mirrored Codex detail messages --- .../__tests__/adapters/CodexAdapter.test.ts | 58 +++++++++++++++++++ .../src/adapters/CodexAdapter.ts | 41 ++++++++++++- 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/packages/agent-manager/src/__tests__/adapters/CodexAdapter.test.ts b/packages/agent-manager/src/__tests__/adapters/CodexAdapter.test.ts index 631a3ccc..7bc591c9 100644 --- a/packages/agent-manager/src/__tests__/adapters/CodexAdapter.test.ts +++ b/packages/agent-manager/src/__tests__/adapters/CodexAdapter.test.ts @@ -1309,6 +1309,64 @@ describe('CodexAdapter', () => { ]); }); + it('should not duplicate mirrored current Codex message records', () => { + const filePath = writeJsonl([ + { type: 'session_meta', payload: { id: 'sess-1', cwd: '/repo', timestamp: '2026-03-27T10:00:00Z' } }, + { + type: 'response_item', + timestamp: '2026-03-27T10:00:01Z', + payload: { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'Fix the bug' }], + internal_chat_message_metadata_passthrough: { turn_id: 'turn-1' }, + }, + }, + { + type: 'event_msg', + timestamp: '2026-03-27T10:00:01.001Z', + payload: { + type: 'item_completed', + turn_id: 'turn-1', + item: { + type: 'UserMessage', + content: [{ type: 'text', text: 'Fix the bug' }], + }, + }, + }, + { + type: 'event_msg', + timestamp: '2026-03-27T10:00:05Z', + payload: { + type: 'item_completed', + turn_id: 'turn-1', + item: { + type: 'AgentMessage', + id: 'msg-1', + content: [{ type: 'Text', text: 'I found the issue' }], + }, + }, + }, + { + type: 'response_item', + timestamp: '2026-03-27T10:00:05.005Z', + payload: { + type: 'message', + id: 'msg-1', + role: 'assistant', + content: [{ type: 'output_text', text: 'I found the issue' }], + internal_chat_message_metadata_passthrough: { turn_id: 'turn-1' }, + }, + }, + ]); + + const messages = adapter.getConversation(filePath); + expect(messages).toEqual([ + { role: 'user', content: 'Fix the bug', timestamp: '2026-03-27T10:00:01Z' }, + { role: 'assistant', content: 'I found the issue', timestamp: '2026-03-27T10:00:05.005Z' }, + ]); + }); + it('should skip session_meta entry', () => { const filePath = writeJsonl([ { type: 'session_meta', payload: { id: 'sess-1', cwd: '/repo', timestamp: '2026-03-27T10:00:00Z' } }, diff --git a/packages/agent-manager/src/adapters/CodexAdapter.ts b/packages/agent-manager/src/adapters/CodexAdapter.ts index 09fe9beb..28c1629f 100644 --- a/packages/agent-manager/src/adapters/CodexAdapter.ts +++ b/packages/agent-manager/src/adapters/CodexAdapter.ts @@ -40,6 +40,10 @@ interface CodexEventEntry { role?: string; content?: CodexContent[]; item?: CodexItem; + turn_id?: string; + internal_chat_message_metadata_passthrough?: { + turn_id?: string; + }; }; } @@ -671,18 +675,40 @@ export class CodexAdapter implements AgentAdapter { if (content === undefined) return []; const lines = content.trim().split('\n'); + const entries: CodexEventEntry[] = []; const messages: ConversationMessage[] = []; for (const line of lines) { - let entry: CodexEventEntry; try { - entry = JSON.parse(line); + entries.push(JSON.parse(line)); } catch { continue; } + } + + const responseItemMirrorKeys = new Set(); + for (const entry of entries) { + if (entry.type !== 'response_item') continue; + + const message = this.toConversationMessage(entry, verbose); + const mirrorKey = message ? this.mirroredMessageKey(entry, message) : null; + if (mirrorKey) responseItemMirrorKeys.add(mirrorKey); + } + for (const entry of entries) { const message = this.toConversationMessage(entry, verbose); - if (message) messages.push(message); + if (!message) continue; + + const mirrorKey = this.mirroredMessageKey(entry, message); + if ( + entry.type === 'event_msg' && + mirrorKey && + responseItemMirrorKeys.has(mirrorKey) + ) { + continue; + } + + messages.push(message); } return messages; @@ -740,6 +766,15 @@ export class CodexAdapter implements AgentAdapter { return verbose ? 'system' : null; } + private mirroredMessageKey(entry: CodexEventEntry, message: ConversationMessage): string | null { + const turnId = + entry.payload?.turn_id || + entry.payload?.internal_chat_message_metadata_passthrough?.turn_id; + + if (!turnId) return null; + return `${turnId}\0${message.role}\0${message.content}`; + } + private extractContentText(content: string | CodexContent[] | undefined): string { if (typeof content === 'string') return content.trim(); if (!Array.isArray(content)) return '';