Files
orca/src/main/codex/codex-structured-journal-contracts.ts
T
Brennan Benson b4b708c2c4 fix(native-chat): a Codex stream retry is one warning row that updates in place (#23684)
* fix(native-chat): a provider's own retry progress is quoted in its retry row

A retry row whose fact carries a detail the provider wrote for a person now
quotes it, the same way a rejected message or failed compaction does, so the
row says how the retry is going. A log detail still stays out of the sentence.

* fix(native-chat): a Codex stream retry is one warning row that updates in place

An error Codex says it will retry used to fall through to the generic frame
row: red, and a new row for every attempt. It now writes one providerRetrying
row per retry run, warning-toned, revised by each attempt with Codex's own
progress sentence. A run is the retry frames of one turn with nothing else the
thread journals between them; every attempt still publishes, so the idle sweep
keeps seeing activity. Errors Codex will not retry are unchanged.

* fix(native-chat): a Codex retry row says it is retrying and keeps the frame behind Details

The quoted retry sentence leads with "is retrying", which holds for any
provider's progress text. The Codex retry row also keeps the whole bounded
frame behind the row's Details, as the generic row did, so Codex's
additionalDetails stays available to diagnose a retry.

* test(native-chat): a Codex retry re-handled after backpressure keeps its one row

Pins the run being opened before the write: a first attempt whose publish is
refused and is handed back must revise the row it already wrote, not open a
second run. Also stops the fixture claiming Codex sends an idle thread status
beside each retry, which the app server does not do.

* perf(native-chat): a Codex frame with no retry run open is not classified

Ending a retry run classified every non-retry frame, and classifying walks the
whole payload: every streaming delta, and every large item/completed, paid a
walk about as costly as parsing the frame. Only a thread with a run open needs
the answer, so the classification now runs only there.

* fix(native-chat): each Codex retry attempt is its own warning row, with what failed on its second line

A stream error Codex says it will retry is written as its own warning row
with a providerRetrying fact, under the same per-frame identity every
Codex frame row gets. The host no longer tracks retry runs or rewrites
one row in place, so there is no run state to open, end or clear, and no
frame has to be classified to end a run. Every attempt publishes, which
keeps renewing the idle clock while Codex retries.

Codex's additionalDetails, which its own UI shows under the progress
message, is kept on the fact as the retry's cause and printed on the
row's second line. Errors Codex will not retry are unchanged.

* fix(native-chat): a transcript draws only the latest row of a provider retry run

The shared structured message projection, which both the desktop and the
mobile transcript read, collapses a run of retry rows into its latest
row. A run is retry rows from the same agent with no other drawn row
between them; a row that draws nothing, or a queued send drawn after the
conversation, does not split it. The earlier attempts stay in the
journal.

* test(native-chat): the retry-run render test uses the message list's current props

* fix(native-chat): a Codex frame row is named for its connection, so a later one never revises it

Frame rows were named provider-frame:codex:<n> from a counter that starts
over with every connection, so the first frame row after a reconnect in the
same session revised an earlier connection's row in place, at its old spot.
Each connection's frame rows now carry the acquisition generation, minted
once before the translator is built: provider-frame:codex:<generation>:<n>.
Rows already written keep their identities.

* fix(native-chat): agents retrying at once each keep one row, read from the row's own agent

* test(native-chat): a reconnect's Codex rows are named for the acquisition that received them

* fix(native-chat): a retry run is one agent's, so another agent's row never splits it

Each agent's rows are drawn apart: the session's own rows are the conversation,
and a subagent's rows open in that subagent's section. Splitting a run on any
other row in the flat list left two adjacent retry rows on screen whenever
another agent wrote between two attempts: a subagent finishing a command while
the session reconnected, or the session working while a subagent reconnected.

A run is now per agent: an agent's retry rows with none of its own other rows
between them, drawn as its latest.

* test(native-chat): a subagent's retry run is checked in its own section, and the run rule's words say same agent

* test(mobile): the retry-rows test typechecks, so the test ratchet keeps checking it
2026-09-30 15:05:22 -07:00

70 lines
3.3 KiB
TypeScript

import type { AgentJournalItemIdentity } from '../../shared/agent-session-journal-types'
import type { CodexDispatchRequestOrigin } from './codex-structured-dispatch-echo'
import type { AgentSessionDeltaCoalescerDeps } from '../native-chat/agent-session-wire/agent-session-delta-coalescer'
import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink'
import type { CodexStructuredSessionEvent } from './codex-structured-session-adapter'
import type { CodexSubagentExecutions } from './codex-subagent-executions'
import type { StructuredAgentSessionCommandRun } from '../native-chat/agent-session-wire/structured-agent-session-adapter'
export type CodexJournalTranslatorDeps = {
sink: StructuredAgentSessionEventSink
/** Names this connection in frame-row identities, so a later connection never revises its rows. */
acquisitionId?: string
/** Keys restored lifecycle rows to the live identity; without it history restore skips them. */
sessionId?: string
now?: () => number
bindPromptItemId?: (
journalItemId: string,
threadId: string,
promptKey: string,
turnId?: string | null
) => void
clearPromptTurn?: (threadId: string, turnId: string) => void
/** Settles a send's identity off the echoed user message, using the very
* identity the journal row carries so a replay computes the same key. */
onUserMessageEcho?: (clientMessageId: string, identity: AgentJournalItemIdentity) => void
primaryThreadId?: () => string | null
/** Codex reported the primary thread is not running while no turn is open here.
* A send whose dispatch was never answered is owed nothing after this. */
onPrimaryThreadStoppedRunning?: () => void
/** Submission origin for one exact client message still awaiting its echo. */
dispatchRequestOrigin?: (clientMessageId: string) => CodexDispatchRequestOrigin | null
subagentExecutions?: CodexSubagentExecutions
coalesceMs?: number
maxRetainedBytes?: number
schedule?: AgentSessionDeltaCoalescerDeps['schedule']
}
export type CodexJournalTranslator = {
handle: (event: CodexStructuredSessionEvent) => CodexJournalTranslationAdmission
/** Before Orca sends a conversation command: the next primary turn carries it out, and that
* turn's end is the command's. */
beginCommand: (command: StructuredAgentSessionCommandRun) => void
/** The command was never taken. */
forgetCommand: (turnId: string) => void
/** The provider turn a Stop naming `turnId` interrupts; undefined while a command has none. */
commandProviderTurnId: (turnId: string) => string | undefined
cancelPrompt: (journalItemId: string) => CodexJournalTranslationAdmission
restoreThread: (
threadId: string,
thread: Record<string, unknown>
) => CodexJournalTranslationAdmission
resolvePrompt: (journalItemId: string) => void
flush: () => void
dispose: () => void
}
export type CodexJournalTranslationAdmission =
| { accepted: true }
| { accepted: false; reason: 'backpressure' | 'failed' | 'closed' | 'untranslated' }
export type CodexItemTranslation =
| { handled: false }
| {
handled: true
admission: CodexJournalTranslationAdmission
dispatchEcho?: { clientMessageId: string; providerIdentity: AgentJournalItemIdentity }
}
export const CODEX_JOURNAL_ADMITTED = { accepted: true } as const