mirror of
https://github.com/stablyai/orca.git
synced 2026-10-05 00:02:33 +00:00
* refactor(native-chat): the Codex acquire names its turn-boundary methods as a set Behavior-neutral: the same two methods stamp receipt time. Keeps the file under the size limit once the child-work sink lands. * feat(native-chat): Codex sessions write their subagents into the host status store A Codex child thread and each persistent command become host child records, fed through the same delivery, ingest and reducer the Claude lane uses. The child's own turn decides it: turn start is live, turn completion settles it with the outcome Codex reports, and a follow-up turn reopens the same record as a new run. Its open tool call, last message, usage and waiting-on-user flag come from its own thread's frames. A parent turn ending settles nothing. * fix(native-chat): close a Codex child's tool call by its item id alone A completion frame need not restate the tool it ran, so reading the tool name before closing left the call open and the record naming a finished tool. * test(native-chat): pin the Codex child-work evidence and every hop to the host's records Child turn start/end/follow-up, open tool call, last message, usage, waiting, the persistent command a child owns and its monitoring display, a primary turn end settling nothing, and session end. End to end through the real adapter: evidence after the journal and the legacy republish, and the parent state the records imply equals today's at every frame of a scripted session. Through the production runtime: a Codex session's child work reaches the status sink under its own address, and a provider exit ends it there. * test(native-chat): a Codex child's new run never inherits the last run's open call * test(native-chat): a Codex session with no child-work sink holds no evidence * test(native-chat): deliver a Codex child's announcement twice, as Codex does, before counting edges * refactor(native-chat): hand the Codex producer's pending edge over directly * fix(native-chat): name every Codex turn state in the outcome map; type the runtime test's fake opener * fix(native-chat): a Codex child's turn ends on the error that ends it, or on its thread closing Codex can end a child's turn with no turn/completed: an error it will not retry is that turn's own end (the verdict the transcript already settles the same turn on), and a closed thread ran its last turn. The executions, the one owner of child turn state, now end the turn on both, so the strip drops the child and its record settles (failed, or unknown for a close) together, instead of reading working for the life of the session. A systemError status is not an ending: Codex raises it for errors that leave the turn running. A child fact whose frame names no turn now belongs to the turn the child is running, instead of counting for every run. * test(native-chat): a Codex child's turn ending by fatal error or thread close settles strip and record together * test(native-chat): the Codex parity script reads a waiting child through the shared fold's waiting arm * test(native-chat): a Codex child row's journal attempt is its record's generation The journal numbers a Codex child's runs by the turns it observed on the child's thread; the host record numbers them by the runs its evidence opened. Both are keyed by the child's own turn id, so they must agree run for run, including when Codex reports the child's first turn before the spawn that announces it. * test(native-chat): a Codex session's end settles its live children and keeps the ended ones The host no longer erases a session's children when its provider goes away: a child still running settles with an outcome nobody reported, and a child that had already ended keeps what it said. The producer tests now expect exactly that, from the close path and from an unexpected exit. * fix(native-chat): a Codex subagent's shell is its open tool until the process exits Codex runs every agent shell through unified exec, so every subagent shell arrives with the source the persistent-command tracker keys on. The producer skipped those items, so a working subagent never named its shell, and an approved command (started on the approval path, completed from unified exec) stayed its open tool until the turn ended. The tracker still records the process separately, so a command that outlives the turn reads as monitoring. * fix(native-chat): a Codex shell becomes a subagent's own work only once it outlives its turn Codex runs every agent shell through unified exec and never says when one is left running, so the producer turned every shell, even a millisecond `rg`, into a command record the moment it started. Each settled into the session's pool of 32 settled records, so a busy turn evicted a finished subagent's record (its outcome row would vanish) and listed dozens of finished shells beside it. A command now becomes a record at the first turn boundary of the thread that launched it while its process still runs: until then it is the agent's open call. A shell that exits within its turn never becomes a record. * refactor(native-chat): child records keep every settled child and can be removed outright Settled child records now stay until the host drops the session's row; the 32-record trim is gone. A producer can say work stopped with nothing to report, and its record (and the handles it answered to) goes instead of settling. Evidence stays host-internal: the producer and the store share one process. * fix(native-chat): a Codex command is live work from its start until its process stops The command tracker is now the one owner of a Codex command's lifetime. It admits every command whatever `source` Codex tags it with (the approval path starts one as `agent`), and ends it when its process exits, when its thread closes (Codex stops the processes first, so no exit ever arrives), or when the session ends. The producer mirrors that one-to-one: a live record from the start, removed when the command stops, never settled. This removes the turn-boundary rule: a command that was only recorded at its turn's end left the parent reading done for one publish when the main agent's turn ended with a shell still running. The parity script now checks the parent at every journal write, not only at frame end. * fix(native-chat): a Codex command whose approval its turn abandoned never ran Codex starts an approval's command item before it asks, and when the turn ends with the question unanswered (the user stops at the approval), it drops the question and never completes the item. The command tracker admitted that start as a running process, so the strip kept a phantom command row and the session row read working until the session ended. The prompt registry, which owns which approvals are still unanswered, reports the command approvals a turn ended without; the tracker ends those commands with the frame that ended the turn. An answered approval keeps its command. * test(native-chat): start the Codex child-work runtime test without the removed hold Main no longer has host.hold: creating the session starts its child, and nothing a viewer does keeps it running. The test attaches and asserts the one child that attach started, then drives it as before.
89 lines
3.3 KiB
TypeScript
89 lines
3.3 KiB
TypeScript
// Codex items and statuses, read in the child-work vocabulary.
|
|
//
|
|
// A tool is named the way Codex names it to its own hooks (`Bash`, `apply_patch`,
|
|
// `mcp__server__tool`), so a structured Codex child running a shell reads exactly as a Codex
|
|
// CLI agent running one does.
|
|
|
|
import type { AgentChildWorkOutcome } from '../../shared/agent-status-child-work'
|
|
import type { NativeChatSubagentState } from '../../shared/native-chat-types'
|
|
import {
|
|
deriveFallbackToolInputPreview,
|
|
deriveToolInputPreview
|
|
} from '../../shared/agent-hook-listener/tool-input-preview'
|
|
import { readRecord, readString, readTextContent } from './codex-item-field-readers'
|
|
import type { CodexThreadItem } from './codex-structured-item-translation'
|
|
|
|
/** Raw provider text kept for a record; admission folds it to its own one-line bound. */
|
|
export const CODEX_CHILD_WORK_TEXT_MAX_CHARS = 2_048
|
|
|
|
export type CodexChildToolCall = { toolName: string; input?: string }
|
|
|
|
function bounded(text: string | null | undefined): string | undefined {
|
|
return text ? text.slice(0, CODEX_CHILD_WORK_TEXT_MAX_CHARS) : undefined
|
|
}
|
|
|
|
function withInput(toolName: string, input: string | undefined): CodexChildToolCall {
|
|
const preview = bounded(input)
|
|
return preview ? { toolName, input: preview } : { toolName }
|
|
}
|
|
|
|
function firstChangePath(changes: unknown): string | undefined {
|
|
const [first] = Array.isArray(changes) ? changes : []
|
|
return readString(readRecord(first), 'path') ?? undefined
|
|
}
|
|
|
|
/** The tool a thread item runs, or null for an item that is not a tool call (a message, a
|
|
* thought, a plan). */
|
|
export function codexChildToolCall(item: CodexThreadItem): CodexChildToolCall | null {
|
|
switch (item.type) {
|
|
case 'commandExecution':
|
|
return withInput('Bash', deriveToolInputPreview('Bash', { command: item.command }))
|
|
case 'fileChange':
|
|
return withInput('apply_patch', firstChangePath(item.changes))
|
|
case 'mcpToolCall': {
|
|
const server = readString(item, 'server')
|
|
const tool = readString(item, 'tool')
|
|
if (!tool) {
|
|
return null
|
|
}
|
|
return withInput(
|
|
server ? `mcp__${server}__${tool}` : tool,
|
|
deriveFallbackToolInputPreview(item.arguments)
|
|
)
|
|
}
|
|
case 'webSearch':
|
|
return withInput('web_search', readString(item, 'query') ?? undefined)
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
/** Whether an item frame says the call is over, whatever frame carried it. */
|
|
export function codexToolCallEnded(method: string, item: CodexThreadItem): boolean {
|
|
const status = readString(item, 'status')
|
|
return method === 'item/completed' || (status !== null && status !== 'inProgress')
|
|
}
|
|
|
|
/** What a child said: an assistant message's text. */
|
|
export function codexChildMessageText(item: CodexThreadItem): string | undefined {
|
|
return item.type === 'agentMessage'
|
|
? bounded(readString(item, 'text') ?? readTextContent(item, 'content'))
|
|
: undefined
|
|
}
|
|
|
|
/** A child turn's ending. Codex states three; anything else is an ending nobody classified. */
|
|
export function codexChildTurnOutcome(state: NativeChatSubagentState): AgentChildWorkOutcome {
|
|
switch (state) {
|
|
case 'completed':
|
|
return 'succeeded'
|
|
case 'failed':
|
|
return 'failed'
|
|
case 'stopped':
|
|
return 'cancelled'
|
|
case 'unverifiable':
|
|
case 'working':
|
|
case 'idle':
|
|
return 'unknown'
|
|
}
|
|
}
|