Fix native chat history across transcript formats (#13393)

* fix(native-chat): restore transcript history across formats

* fix(native-chat): preserve safe Codex history metadata
This commit is contained in:
Brennan Benson 2026-08-09 17:19:54 -07:00 committed by GitHub
parent f155c47d14
commit 1d3decdbc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 341 additions and 5 deletions

View File

@ -24,7 +24,8 @@ export function decodeCodexTranscriptLine(
}
const payload = asRecord(record.payload)
if (!payload) {
return null
const id = extractString(record.id) ?? fallbackId
return codexUnwrappedResponseItem(record, id, parseTimestamp(record.timestamp))
}
const timestamp = parseTimestamp(record.timestamp)
const baseId = extractString(payload.id) ?? fallbackId
@ -38,18 +39,34 @@ export function decodeCodexTranscriptLine(
return null
}
function codexUnwrappedResponseItem(
record: Record<string, unknown>,
id: string,
timestamp: number | null
): NativeChatMessage | null {
if (record.type !== 'message') {
return codexResponseItem(record, id, timestamp)
}
const role = record.role === 'assistant' ? 'assistant' : record.role === 'user' ? 'user' : null
const blocks = codexTurnItemBlocks(record.content)
return role && blocks.length > 0 ? { id, role, blocks, timestamp, source: 'transcript' } : null
}
function codexResponseItem(
payload: Record<string, unknown>,
id: string,
timestamp: number | null
): NativeChatMessage | null {
if (payload.type === 'message') {
const role =
payload.role === 'assistant' ? 'assistant' : payload.role === 'user' ? 'user' : null
if (!role) {
return null
}
const blocks = claudeContentBlocks(payload.content)
if (blocks.length === 0) {
return null
}
const role =
payload.role === 'assistant' ? 'assistant' : payload.role === 'user' ? 'user' : 'system'
return { id, role, blocks, timestamp, source: 'transcript' }
}
if (payload.type === 'reasoning') {
@ -65,7 +82,11 @@ function codexResponseItem(
source: 'transcript'
}
}
if (payload.type === 'function_call' || payload.type === 'local_shell_call') {
if (
payload.type === 'function_call' ||
payload.type === 'local_shell_call' ||
payload.type === 'custom_tool_call'
) {
const name = extractString(payload.name) ?? 'tool'
return {
id,
@ -75,7 +96,7 @@ function codexResponseItem(
source: 'transcript'
}
}
if (payload.type === 'function_call_output') {
if (payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output') {
return {
id,
role: 'tool',
@ -101,6 +122,9 @@ function codexEventMessage(
source: 'transcript'
}
}
if (payload.type === 'item_completed') {
return codexCompletedTurnItem(payload, id, timestamp)
}
if (payload.type === 'user_message') {
const text = extractString(payload.message)
return text
@ -116,6 +140,68 @@ function codexEventMessage(
return null
}
function codexCompletedTurnItem(
payload: Record<string, unknown>,
fallbackId: string,
timestamp: number | null
): NativeChatMessage | null {
const item = asRecord(payload.item)
if (!item) {
return null
}
const id = extractString(item.id) ?? fallbackId
const blocks = codexTurnItemBlocks(item.content)
if (blocks.length === 0) {
return null
}
if (item.type === 'UserMessage' || item.type === 'user_message') {
return { id, role: 'user', blocks, timestamp, source: 'transcript' }
}
if (item.type === 'AgentMessage' || item.type === 'agent_message') {
return { id, role: 'assistant', blocks, timestamp, source: 'transcript' }
}
return null
}
function codexTurnItemBlocks(content: unknown): NativeChatBlock[] {
if (!Array.isArray(content)) {
return []
}
const blocks: NativeChatBlock[] = []
for (const value of content) {
const item = asRecord(value)
if (!item) {
continue
}
if (
item.type === 'text' ||
item.type === 'Text' ||
item.type === 'input_text' ||
item.type === 'output_text'
) {
const text = extractString(item.text)
if (text) {
blocks.push({ type: 'text', text })
}
continue
}
if (item.type === 'image' || item.type === 'Image' || item.type === 'input_image') {
const url = extractString(item.image_url) ?? extractString(item.url)
if (url) {
blocks.push({ type: 'image-ref', url })
}
continue
}
if (item.type === 'local_image' || item.type === 'LocalImage') {
const path = extractString(item.path)
if (path) {
blocks.push({ type: 'image-ref', path })
}
}
}
return blocks
}
function codexCallInput(payload: Record<string, unknown>): unknown {
if (payload.arguments !== undefined) {
return payload.arguments

View File

@ -0,0 +1,250 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { decodeCodexTranscriptLine } from './transcript-line-decoders-codex'
import { readNativeChatTranscript } from './transcript-reader'
import { readNativeChatTranscriptTail } from './transcript-tail-reader'
let tempRoots: string[] = []
afterEach(async () => {
await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true })))
tempRoots = []
})
async function writeCodexFixture(records: unknown[]): Promise<string> {
const root = await mkdtemp(join(tmpdir(), 'orca-native-chat-codex-history-'))
tempRoots.push(root)
const filePath = join(root, 'rollout.jsonl')
await writeFile(filePath, records.map((record) => JSON.stringify(record)).join('\n'))
return filePath
}
function completedItem(item: unknown, timestamp: string): unknown {
return {
timestamp,
type: 'event_msg',
payload: {
type: 'item_completed',
thread_id: 'thread-1',
turn_id: 'turn-1',
item,
completed_at_ms: 1
}
}
}
describe('Codex transcript history modes', () => {
it('reads unwrapped response records from early rollouts', async () => {
const filePath = await writeCodexFixture([
{
id: 'session-1',
timestamp: '2025-06-28T10:00:00.000Z',
instructions: null
},
{
type: 'message',
id: null,
timestamp: '2025-06-28T10:00:01.000Z',
role: 'user',
content: [
{ type: 'input_text', text: 'Early prompt' },
{ type: 'input_image', image_url: 'data:image/png;base64,abc' }
]
},
{
type: 'message',
id: 'assistant-1',
timestamp: '2025-06-28T10:00:02.000Z',
role: 'assistant',
content: [{ type: 'output_text', text: 'Early response' }]
},
{
type: 'message',
id: 'system-1',
role: 'system',
content: [{ type: 'input_text', text: 'Internal instructions' }]
}
])
const result = await readNativeChatTranscript('codex', 'session-1', { filePath })
const tail = await readNativeChatTranscriptTail({
agent: 'codex',
sessionId: 'session-1',
filePath,
limit: 50
})
expect(result).toMatchObject({
messages: [
{
role: 'user',
timestamp: Date.parse('2025-06-28T10:00:01.000Z'),
blocks: [
{ type: 'text', text: 'Early prompt' },
{ type: 'image-ref', url: 'data:image/png;base64,abc' }
]
},
{
id: 'assistant-1',
role: 'assistant',
timestamp: Date.parse('2025-06-28T10:00:02.000Z'),
blocks: [{ type: 'text', text: 'Early response' }]
}
]
})
expect(tail).toMatchObject({ messages: 'messages' in result ? result.messages : [] })
})
it('reads canonical paginated messages without exposing model-only response copies', async () => {
const filePath = await writeCodexFixture([
{
timestamp: '2026-08-09T10:00:00.000Z',
type: 'session_meta',
payload: { id: 'session-1', history_mode: 'paginated' }
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'developer',
content: [{ type: 'text', text: 'internal instructions' }]
}
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'model copy' }]
}
},
completedItem(
{
type: 'UserMessage',
id: 'user-1',
content: [
{ type: 'text', text: 'Visible prompt', text_elements: [] },
{ type: 'image', image_url: 'data:image/png;base64,abc' },
{ type: 'local_image', path: '/tmp/reference.png' },
{ type: 'skill', name: 'example', path: '/tmp/SKILL.md' }
]
},
'2026-08-09T10:00:01.000Z'
),
{
type: 'response_item',
payload: {
type: 'message',
role: 'assistant',
content: [{ type: 'output_text', text: 'model copy' }]
}
},
completedItem(
{
type: 'AgentMessage',
id: 'assistant-1',
content: [{ type: 'Text', text: 'Visible response' }],
phase: 'final_answer'
},
'2026-08-09T10:00:02.000Z'
)
])
const result = await readNativeChatTranscript('codex', 'session-1', { filePath })
const tail = await readNativeChatTranscriptTail({
agent: 'codex',
sessionId: 'session-1',
filePath,
limit: 50
})
expect(result).toMatchObject({
messages: [
{
id: 'user-1',
role: 'user',
blocks: [
{ type: 'text', text: 'Visible prompt' },
{ type: 'image-ref', url: 'data:image/png;base64,abc' },
{ type: 'image-ref', path: '/tmp/reference.png' }
]
},
{
id: 'assistant-1',
role: 'assistant',
blocks: [{ type: 'text', text: 'Visible response' }]
}
]
})
expect('messages' in result && result.messages).toHaveLength(2)
expect(tail).toMatchObject({ messages: 'messages' in result ? result.messages : [] })
})
it('keeps legacy event messages without duplicating response copies', async () => {
const filePath = await writeCodexFixture([
{
type: 'response_item',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'Prompt' }]
}
},
{ type: 'event_msg', payload: { type: 'user_message', message: 'Prompt' } },
{ type: 'event_msg', payload: { type: 'agent_message', message: 'Response' } },
{
type: 'response_item',
payload: {
type: 'message',
role: 'assistant',
content: [{ type: 'output_text', text: 'Response' }]
}
}
])
const result = await readNativeChatTranscript('codex', 'session-1', { filePath })
expect(result).toMatchObject({
messages: [
{ role: 'user', blocks: [{ type: 'text', text: 'Prompt' }] },
{ role: 'assistant', blocks: [{ type: 'text', text: 'Response' }] }
]
})
})
it('decodes freeform tool calls and outputs', () => {
const call = decodeCodexTranscriptLine(
JSON.stringify({
type: 'response_item',
payload: {
type: 'custom_tool_call',
id: 'call-1',
call_id: 'durable-call-1',
name: 'exec',
input: 'pwd'
}
}),
'fallback-call'
)
const output = decodeCodexTranscriptLine(
JSON.stringify({
type: 'response_item',
payload: { type: 'custom_tool_call_output', call_id: 'durable-call-1', output: 'ok' }
}),
'fallback-output'
)
expect(call).toMatchObject({
id: 'call-1',
role: 'assistant',
blocks: [{ type: 'tool-call', name: 'exec', input: 'pwd' }]
})
expect(output).toMatchObject({
id: 'fallback-output',
role: 'tool',
blocks: [{ type: 'tool-result', output: 'ok' }]
})
})
})