codex: mark streamed reasoning as reasoning too, and pin the provider markers

The settled reasoning item carried the marker but the streaming one did not,
so a live Codex turn - the only time the indicator is on screen - never read
as reasoning. Both paths now stamp it; a plan document keeps its own
presentation and must never read as reasoning.
This commit is contained in:
Merge Sim
2026-09-10 20:11:43 -07:00
parent 2154c27cf8
commit 6861bf4a9f
5 changed files with 43 additions and 12 deletions
@@ -2,11 +2,12 @@ import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type {
AgentJournalItemBody,
AgentJournalItemIdentity,
AgentJournalRenderItem,
AgentSessionJournalIdentity
import {
AGENT_JOURNAL_THINKING_PRESENTATION,
type AgentJournalItemBody,
type AgentJournalItemIdentity,
type AgentJournalRenderItem,
type AgentSessionJournalIdentity
} from '../../shared/agent-session-journal-types'
import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key'
import { activeStructuredAgentSessionTurnId } from '../../shared/structured-agent-session-projection'
@@ -568,7 +569,9 @@ describe('Claude structured journal translation', () => {
expect(state.items.at(-1)?.body).toEqual({
kind: 'status',
text: boundInlineText(thinking, DEFAULT_JOURNAL_PAYLOAD_LIMITS).text
text: boundInlineText(thinking, DEFAULT_JOURNAL_PAYLOAD_LIMITS).text,
// Marks the row as the model reasoning, so a client can label the live turn.
presentation: AGENT_JOURNAL_THINKING_PRESENTATION
})
})
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { codexItemBody, codexStreamingJournalItem } from './codex-structured-item-translation'
import { AgentJournalItemBodySchema } from '../../shared/agent-session-journal-schemas'
import { AGENT_JOURNAL_THINKING_PRESENTATION } from '../../shared/agent-session-journal-types'
import { projectStructuredItemsToNativeChat } from '../../shared/structured-agent-session-projection'
describe('plan document translation', () => {
@@ -23,10 +24,15 @@ describe('plan document translation', () => {
codexItemBody({ id: 'r', type: 'reasoning', summary: ['Thinking through the problem.'] })
).toEqual({
kind: 'status',
text: 'Thinking through the problem.'
text: 'Thinking through the problem.',
presentation: AGENT_JOURNAL_THINKING_PRESENTATION
})
expect(codexStreamingJournalItem({ id: 'r', type: 'reasoning' }, 'Thinking…')).toEqual({
body: { kind: 'status', text: 'Thinking…' },
body: {
kind: 'status',
text: 'Thinking…',
presentation: AGENT_JOURNAL_THINKING_PRESENTATION
},
handled: true
})
})
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key'
import { AGENT_JOURNAL_THINKING_PRESENTATION } from '../../shared/agent-session-journal-types'
import {
briefToolArg,
createToolInputDisplay,
@@ -592,12 +593,17 @@ describe('codex item bodies', () => {
body: { kind: 'status', text, presentation: 'plan-document' },
handled: true
})
// A plan is a durable artifact, so it must never read as the model reasoning now.
expect(codexItemBody({ type: 'plan', id: 'plan-document', text })).not.toMatchObject({
presentation: AGENT_JOURNAL_THINKING_PRESENTATION
})
})
it('renders reasoning as status and exposes an unknown item as a provider frame', () => {
expect(codexItemBody({ type: 'reasoning', id: 'r', text: 'thinking' })).toEqual({
kind: 'status',
text: 'thinking'
text: 'thinking',
presentation: AGENT_JOURNAL_THINKING_PRESENTATION
})
expect(codexItemBody({ type: 'reasoning', id: 'r' })).toBeNull()
expect(codexItemBody({ type: 'agentMessage', id: 'm', text: '' })).toBeNull()
@@ -830,7 +836,11 @@ describe('codex item bodies', () => {
summary: ['first', 'second'],
content: [{ text: 'fallback' }]
})
).toEqual({ kind: 'status', text: 'first\nsecond' })
).toEqual({
kind: 'status',
text: 'first\nsecond',
presentation: AGENT_JOURNAL_THINKING_PRESENTATION
})
})
it('refuses a value that is not a thread item at all', () => {
@@ -337,5 +337,14 @@ export function codexStreamingJournalItem(item: CodexThreadItem, text: string):
}
}
const bounded = boundInlineText(text, DEFAULT_JOURNAL_PAYLOAD_LIMITS)
return { body: { kind: 'status', text: bounded.text }, handled: true }
return {
body: {
kind: 'status',
text: bounded.text,
// Streaming reasoning is the live turn's reasoning; the settled item says so
// too, and the indicator reads the tail of the journal while it is still open.
...(item.type === 'reasoning' ? { presentation: AGENT_JOURNAL_THINKING_PRESENTATION } : {})
},
handled: true
}
}
@@ -25,6 +25,7 @@ import {
CODEX_USER_INPUT_METHOD
} from './codex-structured-prompt-replies'
import type { CodexStructuredSessionEvent } from './codex-structured-session-adapter'
import { AGENT_JOURNAL_THINKING_PRESENTATION } from '../../shared/agent-session-journal-types'
const SESSION_ID = 'session-1'
const THREAD_ID = 'thread-abc'
@@ -800,7 +801,9 @@ describe('codex journal translation', () => {
const reduced = new Map(tap.rows.map((row) => [row.key, row.body]))
expect(reduced.get('orca:codex-item%3Athread-abc%3Ar-1')).toEqual({
kind: 'status',
text: 'thinking'
text: 'thinking',
// A streamed reasoning row is still reasoning, so the live indicator can read it.
presentation: AGENT_JOURNAL_THINKING_PRESENTATION
})
expect(reduced.get('orca:codex-item%3Athread-abc%3Apatch-1')).toMatchObject({
kind: 'diff',