fix(codex): improve local session import compatibility (#995)

Co-authored-by: LIUZHIRU <ryuu@fine-net.co.jp>
This commit is contained in:
Himehane
2026-07-12 18:44:39 +08:00
committed by GitHub
co-authored by LIUZHIRU
parent d97b270ba8
commit de07643828
2 changed files with 851 additions and 14 deletions
+667
View File
@@ -46,6 +46,276 @@ function createTranscript(codexHome: string, sessionId: string, cwd = 'C:\\work\
writeFileSync(transcriptPath, `${lines.map((line) => JSON.stringify(line)).join('\n')}\n`, 'utf-8')
}
function createMirroredTranscript(codexHome: string, sessionId: string): void {
const sessionDir = join(codexHome, 'sessions', '2026', '06', '04')
mkdirSync(sessionDir, { recursive: true })
const transcriptPath = join(sessionDir, 'rollout-' + sessionId + '.jsonl')
const transcriptLines = [
{
type: 'session_meta',
payload: {
id: sessionId,
cwd: 'C:/work/project',
originator: 'codex_cli_rs',
cli_version: '0.0.0-test'
}
},
{
type: 'event_msg',
payload: { type: 'user_message', message: 'mirrored user message' }
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'mirrored user message\n' }]
}
}
]
writeFileSync(transcriptPath, transcriptLines.map((line) => JSON.stringify(line)).join('\n') + '\n', 'utf-8')
}
function createMirroredAgentMessageTranscript(codexHome: string, sessionId: string): void {
const sessionDir = join(codexHome, 'sessions', '2026', '06', '04')
mkdirSync(sessionDir, { recursive: true })
const transcriptPath = join(sessionDir, 'rollout-' + sessionId + '.jsonl')
const transcriptLines = [
{
type: 'session_meta',
payload: {
id: sessionId,
cwd: 'C:/work/project',
originator: 'codex_cli_rs',
cli_version: '0.0.0-test'
}
},
{
type: 'event_msg',
payload: { type: 'agent_message', message: 'duplicated assistant message' }
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'assistant',
content: [{ type: 'output_text', text: 'duplicated assistant message' }]
}
}
]
writeFileSync(transcriptPath, transcriptLines.map((line) => JSON.stringify(line)).join('\n') + '\n', 'utf-8')
}
function createInjectedResponseUserTranscript(codexHome: string, sessionId: string): void {
const sessionDir = join(codexHome, 'sessions', '2026', '06', '04')
mkdirSync(sessionDir, { recursive: true })
const transcriptPath = join(sessionDir, 'rollout-' + sessionId + '.jsonl')
const transcriptLines = [
{
type: 'session_meta',
payload: {
id: sessionId,
cwd: 'C:/work/project',
originator: 'codex_cli_rs',
cli_version: '0.0.0-test'
}
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: `# AGENTS.md instructions for /repo
<INSTRUCTIONS>ignore</INSTRUCTIONS>` }]
}
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: `<environment_context>
<current_date>2026-07-07</current_date>
<filesystem>workspace</filesystem>
</environment_context>` }]
}
},
{
type: 'event_msg',
payload: { type: 'user_message', message: 'real event user message' }
}
]
writeFileSync(transcriptPath, transcriptLines.map((line) => JSON.stringify(line)).join('\n') + '\n', 'utf-8')
}
function createEmbeddedEnvironmentPromptTranscript(codexHome: string, sessionId: string): void {
const sessionDir = join(codexHome, 'sessions', '2026', '06', '04')
mkdirSync(sessionDir, { recursive: true })
const transcriptPath = join(sessionDir, 'rollout-' + sessionId + '.jsonl')
const transcriptLines = [
{
type: 'session_meta',
payload: {
id: sessionId,
cwd: 'C:/work/project',
originator: 'codex_cli_rs',
cli_version: '0.0.0-test'
}
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: `Please inspect this transcript block:
<environment_context>
<current_date>2026-07-07</current_date>
</environment_context>
This is part of my prompt.` }]
}
}
]
writeFileSync(transcriptPath, transcriptLines.map((line) => JSON.stringify(line)).join('\n') + '\n', 'utf-8')
}
function createSameSourceDuplicateTranscript(codexHome: string, sessionId: string): void {
const sessionDir = join(codexHome, 'sessions', '2026', '06', '04')
mkdirSync(sessionDir, { recursive: true })
const transcriptPath = join(sessionDir, 'rollout-' + sessionId + '.jsonl')
const transcriptLines = [
{
type: 'session_meta',
payload: {
id: sessionId,
cwd: 'C:/work/project',
originator: 'codex_cli_rs',
cli_version: '0.0.0-test'
}
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'same source user message' }]
}
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'same source user message' }]
}
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'assistant',
content: [{ type: 'output_text', text: 'same source assistant message' }]
}
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'assistant',
content: [{ type: 'output_text', text: 'same source assistant message' }]
}
}
]
writeFileSync(transcriptPath, transcriptLines.map((line) => JSON.stringify(line)).join('\n') + '\n', 'utf-8')
}
function createRepeatedSameTextDistinctTurnTranscript(codexHome: string, sessionId: string): void {
const sessionDir = join(codexHome, 'sessions', '2026', '06', '04')
mkdirSync(sessionDir, { recursive: true })
const transcriptPath = join(sessionDir, 'rollout-' + sessionId + '.jsonl')
const transcriptLines = [
{
type: 'session_meta',
payload: {
id: sessionId,
cwd: 'C:/work/project',
originator: 'codex_cli_rs',
cli_version: '0.0.0-test'
}
},
{
type: 'event_msg',
payload: { type: 'user_message', message: 'repeat user message' }
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'repeat user message' }]
}
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'assistant',
content: [{ type: 'output_text', text: 'assistant answer' }]
}
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'repeat user message' }]
}
}
]
writeFileSync(transcriptPath, transcriptLines.map((line) => JSON.stringify(line)).join('\n') + '\n', 'utf-8')
}
function createMirroredAndDistinctUserTranscript(codexHome: string, sessionId: string): void {
const sessionDir = join(codexHome, 'sessions', '2026', '06', '04')
mkdirSync(sessionDir, { recursive: true })
const transcriptPath = join(sessionDir, 'rollout-' + sessionId + '.jsonl')
const transcriptLines = [
{
type: 'session_meta',
payload: {
id: sessionId,
cwd: 'C:/work/project',
originator: 'codex_cli_rs',
cli_version: '0.0.0-test'
}
},
{
type: 'event_msg',
payload: { type: 'user_message', message: 'first user message' }
},
{
type: 'response_item',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'first user message' }]
}
},
{
type: 'event_msg',
payload: { type: 'user_message', message: 'second user message' }
}
]
writeFileSync(transcriptPath, transcriptLines.map((line) => JSON.stringify(line)).join('\n') + '\n', 'utf-8')
}
function writeSessionIndex(codexHome: string, records: Array<{ id: string; thread_name: string; updated_at: string }>): void {
writeFileSync(
join(codexHome, 'session_index.jsonl'),
records.map((record) => JSON.stringify(record)).join('\n') + '\n',
'utf-8'
)
}
function createMachine(id: string, workspaceRoots: string[], namespace = 'default'): Machine {
return {
id,
@@ -163,6 +433,403 @@ describe('Codex Desktop import routes', () => {
}
})
it('deduplicates mirrored event_msg and response_item user messages', async () => {
const codexHome = mkdtempSync(join(tmpdir(), 'hapi-codex-home-mirror-test-'))
const store = new Store(':memory:')
const codexSessionId = '66666666-6666-4666-8666-666666666666'
process.env.CODEX_HOME = codexHome
try {
createMirroredTranscript(codexHome, codexSessionId)
const result = await importSelectedCodexSessions({
codexSessionIds: [codexSessionId],
store,
namespace: 'default',
getSyncEngine: () => null
})
expect(result.success).toBe(true)
const session = store.sessions.getSessionsByNamespace('default')[0]
expect(session).toBeDefined()
const messages = store.messages.getAllMessages(session.id)
expect(messages).toHaveLength(1)
expect(messages[0].content).toEqual({
role: 'user',
content: {
type: 'text',
text: 'mirrored user message'
},
meta: {
sentFrom: 'cli'
}
})
} finally {
store.close()
rmSync(codexHome, { recursive: true, force: true })
}
})
it('deduplicates adjacent mirrored agent messages with different ids', async () => {
const codexHome = mkdtempSync(join(tmpdir(), 'hapi-codex-home-agent-mirror-test-'))
const store = new Store(':memory:')
const codexSessionId = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'
process.env.CODEX_HOME = codexHome
try {
createMirroredAgentMessageTranscript(codexHome, codexSessionId)
const result = await importSelectedCodexSessions({
codexSessionIds: [codexSessionId],
store,
namespace: 'default',
getSyncEngine: () => null
})
expect(result.success).toBe(true)
const session = store.sessions.getSessionsByNamespace('default')[0]
expect(session).toBeDefined()
const messages = store.messages.getAllMessages(session.id)
expect(messages).toHaveLength(1)
expect(messages[0].content).toEqual({
role: 'agent',
content: {
type: AGENT_MESSAGE_PAYLOAD_TYPE,
data: {
type: 'message',
message: 'duplicated assistant message',
id: expect.any(String)
}
},
meta: {
sentFrom: 'cli'
}
})
} finally {
store.close()
rmSync(codexHome, { recursive: true, force: true })
}
})
it('skips injected response_item user context messages', async () => {
const codexHome = mkdtempSync(join(tmpdir(), 'hapi-codex-home-injected-user-test-'))
const store = new Store(':memory:')
const codexSessionId = '88888888-8888-4888-8888-888888888888'
process.env.CODEX_HOME = codexHome
try {
createInjectedResponseUserTranscript(codexHome, codexSessionId)
const result = await importSelectedCodexSessions({
codexSessionIds: [codexSessionId],
store,
namespace: 'default',
getSyncEngine: () => null
})
expect(result.success).toBe(true)
const session = store.sessions.getSessionsByNamespace('default')[0]
expect(session).toBeDefined()
const messages = store.messages.getAllMessages(session.id)
expect(messages).toHaveLength(1)
expect(messages[0].content).toEqual({
role: 'user',
content: {
type: 'text',
text: 'real event user message'
},
meta: {
sentFrom: 'cli'
}
})
} finally {
store.close()
rmSync(codexHome, { recursive: true, force: true })
}
})
it('preserves a real response_item user prompt containing embedded environment_context text', async () => {
const codexHome = mkdtempSync(join(tmpdir(), 'hapi-codex-home-embedded-env-test-'))
const store = new Store(':memory:')
const codexSessionId = 'abababab-abab-4bab-8bab-abababababab'
process.env.CODEX_HOME = codexHome
try {
createEmbeddedEnvironmentPromptTranscript(codexHome, codexSessionId)
const result = await importSelectedCodexSessions({
codexSessionIds: [codexSessionId],
store,
namespace: 'default',
getSyncEngine: () => null
})
expect(result.success).toBe(true)
const session = store.sessions.getSessionsByNamespace('default')[0]
expect(session).toBeDefined()
const messages = store.messages.getAllMessages(session.id)
expect(messages).toHaveLength(1)
expect(messages[0].content).toMatchObject({
role: 'user',
content: {
type: 'text',
text: expect.stringContaining('<environment_context>')
}
})
} finally {
store.close()
rmSync(codexHome, { recursive: true, force: true })
}
})
it('preserves adjacent duplicate messages from the same transcript source', async () => {
const codexHome = mkdtempSync(join(tmpdir(), 'hapi-codex-home-same-source-duplicate-test-'))
const store = new Store(':memory:')
const codexSessionId = 'babababa-baba-4aba-8aba-babababababa'
process.env.CODEX_HOME = codexHome
try {
createSameSourceDuplicateTranscript(codexHome, codexSessionId)
const result = await importSelectedCodexSessions({
codexSessionIds: [codexSessionId],
store,
namespace: 'default',
getSyncEngine: () => null
})
expect(result.success).toBe(true)
const session = store.sessions.getSessionsByNamespace('default')[0]
expect(session).toBeDefined()
const messages = store.messages.getAllMessages(session.id)
expect(messages).toHaveLength(4)
expect(messages.map((message) => (message.content as { role?: unknown }).role)).toEqual([
'user',
'user',
'agent',
'agent'
])
} finally {
store.close()
rmSync(codexHome, { recursive: true, force: true })
}
})
it('keeps a later response_item-only user turn with the same text', async () => {
const codexHome = mkdtempSync(join(tmpdir(), 'hapi-codex-home-repeat-user-test-'))
const store = new Store(':memory:')
const codexSessionId = '99999999-9999-4999-8999-999999999999'
process.env.CODEX_HOME = codexHome
try {
createRepeatedSameTextDistinctTurnTranscript(codexHome, codexSessionId)
const result = await importSelectedCodexSessions({
codexSessionIds: [codexSessionId],
store,
namespace: 'default',
getSyncEngine: () => null
})
expect(result.success).toBe(true)
const session = store.sessions.getSessionsByNamespace('default')[0]
expect(session).toBeDefined()
const messages = store.messages.getAllMessages(session.id)
const roles = messages.map((message) => (message.content as { role?: unknown }).role)
expect(roles).toEqual(['user', 'agent', 'user'])
expect(messages[0].content).toMatchObject({
role: 'user',
content: {
type: 'text',
text: 'repeat user message'
}
})
expect(messages[2].content).toMatchObject({
role: 'user',
content: {
type: 'text',
text: 'repeat user message'
}
})
} finally {
store.close()
rmSync(codexHome, { recursive: true, force: true })
}
})
it('keeps distinct user turns while deduplicating mirrored user events', async () => {
const codexHome = mkdtempSync(join(tmpdir(), 'hapi-codex-home-distinct-user-test-'))
const store = new Store(':memory:')
const codexSessionId = '77777777-7777-4777-8777-777777777777'
process.env.CODEX_HOME = codexHome
try {
createMirroredAndDistinctUserTranscript(codexHome, codexSessionId)
const result = await importSelectedCodexSessions({
codexSessionIds: [codexSessionId],
store,
namespace: 'default',
getSyncEngine: () => null
})
expect(result.success).toBe(true)
const session = store.sessions.getSessionsByNamespace('default')[0]
expect(session).toBeDefined()
const messages = store.messages.getAllMessages(session.id)
expect(messages).toHaveLength(2)
expect(messages.map((message) => message.content)).toEqual([
{
role: 'user',
content: {
type: 'text',
text: 'first user message'
},
meta: {
sentFrom: 'cli'
}
},
{
role: 'user',
content: {
type: 'text',
text: 'second user message'
},
meta: {
sentFrom: 'cli'
}
}
])
} finally {
store.close()
rmSync(codexHome, { recursive: true, force: true })
}
})
it('uses the latest session_index thread_name for list and imported session title', async () => {
const codexHome = mkdtempSync(join(tmpdir(), 'hapi-codex-home-index-title-test-'))
const store = new Store(':memory:')
const codexSessionId = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb'
process.env.CODEX_HOME = codexHome
try {
createTranscript(codexHome, codexSessionId)
writeSessionIndex(codexHome, [
{
id: codexSessionId,
thread_name: 'old thread title',
updated_at: '2026-07-07T01:00:00.000000000Z'
},
{
id: codexSessionId,
thread_name: 'new thread title',
updated_at: '2026-07-07T02:00:00.000000000Z'
}
])
const app = createRoutesApp('default')
const response = await app.request('/api/codex/sessions')
expect(response.status).toBe(200)
const body = await response.json() as { sessions: Array<{ id: string; title: string }> }
expect(body.sessions.find((session) => session.id === codexSessionId)?.title).toBe('new thread title')
const result = await importSelectedCodexSessions({
codexSessionIds: [codexSessionId],
store,
namespace: 'default',
getSyncEngine: () => null
})
expect(result.success).toBe(true)
const session = store.sessions.getSessionsByNamespace('default')[0]
expect(session.metadata).toMatchObject({
name: 'new thread title',
flavor: 'codex',
codexSessionId
})
} finally {
store.close()
rmSync(codexHome, { recursive: true, force: true })
}
})
it('falls back to transcript title when session_index is missing', async () => {
const codexHome = mkdtempSync(join(tmpdir(), 'hapi-codex-home-no-index-title-test-'))
const store = new Store(':memory:')
const codexSessionId = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc'
process.env.CODEX_HOME = codexHome
try {
createTranscript(codexHome, codexSessionId)
const app = createRoutesApp('default')
const response = await app.request('/api/codex/sessions')
expect(response.status).toBe(200)
const body = await response.json() as { sessions: Array<{ id: string; title: string }> }
expect(body.sessions.find((session) => session.id === codexSessionId)?.title).toBe('normal user message')
const result = await importSelectedCodexSessions({
codexSessionIds: [codexSessionId],
store,
namespace: 'default',
getSyncEngine: () => null
})
expect(result.success).toBe(true)
const session = store.sessions.getSessionsByNamespace('default')[0]
expect(session.metadata).toMatchObject({
name: 'normal user message',
flavor: 'codex',
codexSessionId
})
} finally {
store.close()
rmSync(codexHome, { recursive: true, force: true })
}
})
it('falls back to transcript title when session_index has no matching id', async () => {
const codexHome = mkdtempSync(join(tmpdir(), 'hapi-codex-home-index-miss-title-test-'))
const store = new Store(':memory:')
const codexSessionId = 'dddddddd-dddd-4ddd-8ddd-dddddddddddd'
process.env.CODEX_HOME = codexHome
try {
createTranscript(codexHome, codexSessionId)
writeSessionIndex(codexHome, [
{
id: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee',
thread_name: 'unrelated title',
updated_at: '2026-07-07T03:00:00.000000000Z'
}
])
const app = createRoutesApp('default')
const response = await app.request('/api/codex/sessions')
expect(response.status).toBe(200)
const body = await response.json() as { sessions: Array<{ id: string; title: string }> }
expect(body.sessions.find((session) => session.id === codexSessionId)?.title).toBe('normal user message')
const result = await importSelectedCodexSessions({
codexSessionIds: [codexSessionId],
store,
namespace: 'default',
getSyncEngine: () => null
})
expect(result.success).toBe(true)
const session = store.sessions.getSessionsByNamespace('default')[0]
expect(session.metadata).toMatchObject({
name: 'normal user message',
flavor: 'codex',
codexSessionId
})
} finally {
store.close()
rmSync(codexHome, { recursive: true, force: true })
}
})
it('binds imported transcripts to the unique online machine that owns the cwd', async () => {
const codexHome = mkdtempSync(join(tmpdir(), 'hapi-codex-home-machine-test-'))
const store = new Store(':memory:')
+184 -14
View File
@@ -88,10 +88,21 @@ type CodexImportedMessageContent = {
}
}
type CodexImportedMessageSource = 'event_msg' | 'response_item'
type CodexImportedMessageEntry = {
source: CodexImportedMessageSource
message: CodexImportedMessageContent
}
type CodexTranscriptImportData = CodexLocalSessionSummary & {
messages: CodexImportedMessageContent[]
}
type CodexSessionIndexTitle = {
threadName: string
updatedAt: string
}
type ImportCandidate = {
sessionId: string
active: boolean
@@ -207,6 +218,10 @@ function getCodexSessionRoots(): string[] {
return [join(codexHome, 'sessions')]
}
function getCodexSessionIndexPath(): string {
return join(getCodexHome(), 'session_index.jsonl')
}
function collectJsonlFiles(root: string, files: string[]): void {
if (!existsSync(root)) return
let entries
@@ -272,10 +287,13 @@ function truncateText(value: string, maxLength: number): string {
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}` : value
}
function shouldIgnoreSyntheticUserMessage(text: string): boolean {
function shouldIgnoreInjectedResponseUserMessage(text: string): boolean {
const normalized = text.trim()
return normalized.startsWith('# AGENTS.md instructions')
|| normalized.startsWith('<environment_context>')
const lower = normalized.toLowerCase()
const isAgentInstructions = lower.startsWith('# agents.md instructions')
const isEnvironmentContext = lower.startsWith('<environment_context>')
&& lower.endsWith('</environment_context>')
return isAgentInstructions || isEnvironmentContext
}
function inferSessionIdFromFileName(filePath: string): string | null {
@@ -371,7 +389,7 @@ function getLatestCodexUserMessage(lines: string[]): string | null {
const payload = asRecord(record.payload)
if (payload?.type !== 'message' || payload.role !== 'user') continue
const text = extractCodexText(payload.content)
if (text && !shouldIgnoreSyntheticUserMessage(text)) {
if (text && !shouldIgnoreInjectedResponseUserMessage(text)) {
return truncateText(text, 140)
}
} catch {
@@ -384,9 +402,14 @@ function getLatestCodexUserMessage(lines: string[]): string | null {
function getCodexSessionTitle(
cwd: string | null | undefined,
sessionId: string,
sessionIndexTitle: string | null,
changedTitle: string | null,
firstUserMessage: string | null
): string {
if (sessionIndexTitle) {
return truncateText(sessionIndexTitle, 80)
}
if (changedTitle) {
return truncateText(changedTitle, 80)
}
@@ -410,7 +433,48 @@ function isSubagentSource(value: unknown): boolean {
return record ? Object.prototype.hasOwnProperty.call(record, 'subagent') : false
}
function parseCodexLocalSession(filePath: string): CodexLocalSessionSummary | null {
function readCodexSessionIndexTitles(): Map<string, CodexSessionIndexTitle> {
let content: string
try {
content = readFileSync(getCodexSessionIndexPath(), 'utf-8')
} catch {
return new Map()
}
const titles = new Map<string, CodexSessionIndexTitle>()
for (const line of content.split(/\r?\n/).filter(Boolean)) {
let parsed: unknown
try {
parsed = JSON.parse(line)
} catch {
continue
}
const record = asRecord(parsed)
const id = typeof record?.id === 'string' ? record.id : null
const threadName = typeof record?.thread_name === 'string' && record.thread_name.trim()
? record.thread_name.trim()
: null
const updatedAt = typeof record?.updated_at === 'string' && record.updated_at.trim()
? record.updated_at.trim()
: null
if (!id || !threadName || !updatedAt) {
continue
}
const previous = titles.get(id)
if (!previous || previous.updatedAt < updatedAt) {
titles.set(id, { threadName, updatedAt })
}
}
return titles
}
function parseCodexLocalSession(
filePath: string,
sessionIndexTitles = new Map<string, CodexSessionIndexTitle>()
): CodexLocalSessionSummary | null {
let content: string
try {
content = readFileSync(filePath, 'utf-8')
@@ -461,7 +525,7 @@ function parseCodexLocalSession(filePath: string): CodexLocalSessionSummary | nu
const payload = asRecord(record?.payload)
if (payload?.type === 'message' && payload.role === 'user') {
const text = extractCodexText(payload.content)
if (text && !shouldIgnoreSyntheticUserMessage(text)) {
if (text && !shouldIgnoreInjectedResponseUserMessage(text)) {
firstUserMessage = text
}
}
@@ -473,6 +537,7 @@ function parseCodexLocalSession(filePath: string): CodexLocalSessionSummary | nu
sessionId = sessionId ?? inferSessionIdFromFileName(filePath)
if (!sessionId) return null
const sessionIndexTitle = sessionIndexTitles.get(sessionId)?.threadName ?? null
let modifiedAt = Date.now()
try {
@@ -483,7 +548,7 @@ function parseCodexLocalSession(filePath: string): CodexLocalSessionSummary | nu
return {
id: sessionId,
title: getCodexSessionTitle(cwd, sessionId, changedTitle, firstUserMessage),
title: getCodexSessionTitle(cwd, sessionId, sessionIndexTitle, changedTitle, firstUserMessage),
lastUserMessage,
cwd,
file: filePath,
@@ -499,9 +564,10 @@ function listLocalCodexSessions(limit = DEFAULT_CODEX_SESSION_SCAN_LIMIT): Codex
collectJsonlFiles(root, files)
}
const sessionIndexTitles = readCodexSessionIndexTitles()
const deduped = new Map<string, CodexLocalSessionSummary>()
for (const filePath of files) {
const session = parseCodexLocalSession(filePath)
const session = parseCodexLocalSession(filePath, sessionIndexTitles)
if (!session) continue
const previous = deduped.get(session.id)
if (!previous || previous.modifiedAt < session.modifiedAt) {
@@ -557,7 +623,7 @@ function convertCodexRecordToImportedMessage(record: Record<string, unknown>): C
const text = asString(payload.message)
?? asString(payload.text)
?? asString(payload.content)
if (!text || shouldIgnoreSyntheticUserMessage(text)) {
if (!text) {
return null
}
return buildImportedUserMessage(text)
@@ -595,11 +661,11 @@ function convertCodexRecordToImportedMessage(record: Record<string, unknown>): C
if (itemType === 'message') {
const role = asString(payload.role)
const text = extractCodexText(payload.content)
if (!text || shouldIgnoreSyntheticUserMessage(text)) {
if (!text) {
return null
}
if (role === 'user') {
return buildImportedUserMessage(text)
return shouldIgnoreInjectedResponseUserMessage(text) ? null : buildImportedUserMessage(text)
}
if (role === 'assistant') {
return buildImportedAgentMessage({ type: 'message', message: text, id: randomUUID() })
@@ -639,6 +705,86 @@ function convertCodexRecordToImportedMessage(record: Record<string, unknown>): C
return null
}
function getCodexImportedMessageSource(record: Record<string, unknown>): CodexImportedMessageSource | null {
const type = asString(record.type)
return type === 'event_msg' || type === 'response_item' ? type : null
}
function normalizeComparableUserMessage(content: unknown): string | null {
const record = asRecord(content)
if (!record || record.role !== 'user') {
return null
}
const body = asRecord(record.content)
if (body?.type !== 'text' || typeof body.text !== 'string') {
return null
}
return stableSerialize({
role: 'user',
text: body.text.trimEnd()
})
}
function normalizeComparableAgentMessage(content: unknown): string | null {
const record = asRecord(content)
if (!record || record.role !== 'agent') {
return null
}
const body = asRecord(record.content)
if (!body || body.type !== AGENT_MESSAGE_PAYLOAD_TYPE) {
return null
}
const data = asRecord(body.data)
if (data?.type !== 'message' || typeof data.message !== 'string') {
return null
}
return stableSerialize({
role: 'agent',
type: 'message',
message: data.message
})
}
function normalizeAdjacentDuplicateMessage(content: unknown): string | null {
return normalizeComparableUserMessage(content) ?? normalizeComparableAgentMessage(content)
}
function isAdjacentDuplicateImportedMessage(
previous: CodexImportedMessageContent,
next: CodexImportedMessageContent
): boolean {
const previousKey = normalizeAdjacentDuplicateMessage(previous)
const nextKey = normalizeAdjacentDuplicateMessage(next)
return previousKey !== null && previousKey === nextKey
}
function isMirroredAdjacentDuplicate(
previous: CodexImportedMessageEntry | undefined,
next: CodexImportedMessageEntry
): boolean {
return Boolean(
previous
&& previous.source !== next.source
&& isAdjacentDuplicateImportedMessage(previous.message, next.message)
)
}
function isResponseItemDuplicateOfEventUserMessage(
entry: CodexImportedMessageEntry,
recentEventUserMessageKey: string | null
): boolean {
if (entry.source !== 'response_item' || recentEventUserMessageKey === null) {
return false
}
return normalizeComparableUserMessage(entry.message) === recentEventUserMessageKey
}
function parseCodexTranscriptImportData(summary: CodexLocalSessionSummary): CodexTranscriptImportData | null {
let content: string
try {
@@ -648,7 +794,8 @@ function parseCodexTranscriptImportData(summary: CodexLocalSessionSummary): Code
}
const lines = content.split(/\r?\n/).filter(Boolean)
const messages: CodexImportedMessageContent[] = []
const entries: CodexImportedMessageEntry[] = []
let recentEventUserMessageKey: string | null = null
for (const line of lines) {
let parsed: unknown
@@ -660,15 +807,38 @@ function parseCodexTranscriptImportData(summary: CodexLocalSessionSummary): Code
const record = asRecord(parsed)
if (!record) continue
const source = getCodexImportedMessageSource(record)
if (!source) continue
const message = convertCodexRecordToImportedMessage(record)
if (message) {
messages.push(message)
const entry = { source, message }
const userMessageKey = normalizeComparableUserMessage(message)
if (source === 'event_msg' && userMessageKey !== null) {
const previous = entries[entries.length - 1]
if (previous?.source === 'response_item' && isMirroredAdjacentDuplicate(previous, entry)) {
entries[entries.length - 1] = entry
} else {
entries.push(entry)
}
recentEventUserMessageKey = userMessageKey
continue
}
if (isResponseItemDuplicateOfEventUserMessage(entry, recentEventUserMessageKey)) {
continue
}
const previous = entries[entries.length - 1]
if (isMirroredAdjacentDuplicate(previous, entry)) {
recentEventUserMessageKey = null
continue
}
entries.push(entry)
recentEventUserMessageKey = null
}
}
return {
...summary,
messages
messages: entries.map((entry) => entry.message)
}
}