mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 00:02:19 +00:00
* 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
110 lines
4.6 KiB
TypeScript
110 lines
4.6 KiB
TypeScript
// The handful of facts Orca reads out of Codex app-server payloads. Codex has
|
|
// moved these fields between the envelope and a nested `thread` / `turn` object
|
|
// across releases, so each reader accepts both shapes rather than pinning one.
|
|
|
|
function record(value: unknown): Record<string, unknown> | null {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: null
|
|
}
|
|
|
|
function nonEmptyString(value: unknown): string | null {
|
|
return typeof value === 'string' && value.length > 0 ? value : null
|
|
}
|
|
|
|
/** `thread/start`, `thread/resume`, and `thread/started` all name the thread. */
|
|
export function readCodexThreadId(payload: unknown): string | null {
|
|
const root = record(payload)
|
|
if (!root) {
|
|
return null
|
|
}
|
|
return nonEmptyString(record(root.thread)?.id) ?? nonEmptyString(root.threadId)
|
|
}
|
|
|
|
/** Rollout file for the thread, when Codex reports one. Journal recovery reads
|
|
* it; a null just falls back to the existing session-file resolver. */
|
|
export function readCodexThreadPath(payload: unknown): string | null {
|
|
const root = record(payload)
|
|
return root ? nonEmptyString(record(root.thread)?.path) : null
|
|
}
|
|
|
|
/** `turn/start` responses carry `turn.id`; `turn/started` notifications carry
|
|
* the same under `turn`, and older builds put `turnId` on the envelope. */
|
|
export function readCodexTurnId(payload: unknown): string | null {
|
|
const root = record(payload)
|
|
if (!root) {
|
|
return null
|
|
}
|
|
return nonEmptyString(record(root.turn)?.id) ?? nonEmptyString(root.turnId)
|
|
}
|
|
|
|
/** `turn/completed` carries `turn.status`; thread history puts `status` on the turn record itself. */
|
|
export function readCodexTurnStatus(payload: unknown): string | null {
|
|
const root = record(payload)
|
|
if (!root) {
|
|
return null
|
|
}
|
|
return nonEmptyString(record(root.turn)?.status) ?? nonEmptyString(root.status)
|
|
}
|
|
|
|
/** A failed `turn/completed` carries Codex's reason as `turn.error.message`. */
|
|
export function readCodexTurnErrorMessage(payload: unknown): string | null {
|
|
return nonEmptyString(record(record(record(payload)?.turn)?.error)?.message)
|
|
}
|
|
|
|
/** Codex's own turn duration, already in milliseconds; absent or malformed reads as null. */
|
|
export function readCodexTurnDurationMs(payload: unknown): number | null {
|
|
const root = record(payload)
|
|
if (!root) {
|
|
return null
|
|
}
|
|
const value = record(root.turn)?.durationMs ?? root.durationMs
|
|
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null
|
|
}
|
|
|
|
/** `error` carries `willRetry`: Codex sets it on a stream error it is about to
|
|
* retry, and omits it (false) on one that ended the turn the frame names. */
|
|
export function readCodexErrorWillRetry(payload: unknown): boolean {
|
|
return record(payload)?.willRetry === true
|
|
}
|
|
|
|
/** `error.message` on an `error` frame: Codex's own words for the person reading the chat. */
|
|
export function readCodexErrorMessage(payload: unknown): string | null {
|
|
return nonEmptyString(record(record(payload)?.error)?.message)
|
|
}
|
|
|
|
/** `error.additionalDetails`: what failed underneath, which Codex shows under its message. */
|
|
export function readCodexErrorAdditionalDetails(payload: unknown): string | null {
|
|
return nonEmptyString(record(record(payload)?.error)?.additionalDetails)
|
|
}
|
|
|
|
/** `error.codexErrorInfo` is a bare variant (`"serverOverloaded"`) or one keyed to its fields
|
|
* (`{"responseStreamDisconnected":{"httpStatusCode":502}}`); read as the variant and its status. */
|
|
export function readCodexErrorInfo(payload: unknown): { error: string; status?: unknown } | null {
|
|
const info = record(record(payload)?.error)?.codexErrorInfo
|
|
const bare = nonEmptyString(info)
|
|
if (bare) {
|
|
return { error: bare }
|
|
}
|
|
const [variant, fields] = Object.entries(record(info) ?? {})[0] ?? []
|
|
return variant ? { error: variant, status: record(fields)?.httpStatusCode } : null
|
|
}
|
|
|
|
/** `thread/status/changed` carries a TAGGED status (`{status:{type}}`), never a
|
|
* bare string. `idle` and `systemError` are the two arms that mean the thread
|
|
* is not running; `active` and `notLoaded` are not. */
|
|
export function codexThreadStoppedRunning(payload: unknown): boolean {
|
|
const type = record(record(payload)?.status)?.type
|
|
return type === 'idle' || type === 'systemError'
|
|
}
|
|
|
|
/** An `active` thread flags each request it has open on the user (an approval, a question). */
|
|
export function codexThreadWaitsOnUser(payload: unknown): boolean {
|
|
const status = record(record(payload)?.status)
|
|
const flags = status?.type === 'active' ? status.activeFlags : null
|
|
return (
|
|
Array.isArray(flags) &&
|
|
flags.some((flag) => flag === 'waitingOnApproval' || flag === 'waitingOnUserInput')
|
|
)
|
|
}
|