diff --git a/hub/src/web/routes/codexDesktop.test.ts b/hub/src/web/routes/codexDesktop.test.ts
index 9b4324ea..33bbfcc7 100644
--- a/hub/src/web/routes/codexDesktop.test.ts
+++ b/hub/src/web/routes/codexDesktop.test.ts
@@ -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
+ignore` }]
+ }
+ },
+ {
+ type: 'response_item',
+ payload: {
+ type: 'message',
+ role: 'user',
+ content: [{ type: 'input_text', text: `
+2026-07-07
+workspace
+` }]
+ }
+ },
+ {
+ 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:
+
+2026-07-07
+
+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('')
+ }
+ })
+ } 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:')
diff --git a/hub/src/web/routes/codexDesktop.ts b/hub/src/web/routes/codexDesktop.ts
index d5723f40..82e5d71f 100644
--- a/hub/src/web/routes/codexDesktop.ts
+++ b/hub/src/web/routes/codexDesktop.ts
@@ -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('')
+ const lower = normalized.toLowerCase()
+ const isAgentInstructions = lower.startsWith('# agents.md instructions')
+ const isEnvironmentContext = lower.startsWith('')
+ && lower.endsWith('')
+ 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 {
+ let content: string
+ try {
+ content = readFileSync(getCodexSessionIndexPath(), 'utf-8')
+ } catch {
+ return new Map()
+ }
+
+ const titles = new Map()
+ 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()
+): 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()
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): 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): 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): C
return null
}
+function getCodexImportedMessageSource(record: Record): 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)
}
}