mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 08:02:44 +00:00
1e600a8f655a5b3acc51fef1ed299d319b1bcdad
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cb363444f3 |
feat(native-chat): register Codex default-mode helpers as subagents (#22619)
* feat(native-chat): register Codex default-mode helpers as subagents Codex's default multi-agent mode announces a helper only as the collabAgentToolCall that spawned it; it sends no subAgentActivity. The roster and the background-task tracker registered children only from subAgentActivity, so such a helper had no record, no strip row and no roster row, and its commands read as the session's own. One announcement reader now yields a child from either wire shape, and both the journal roster and the tracker register through it into the same executions, so a session sending both keeps one child per thread. A finished closeAgent ends the helper's running turn as stopped through the executions, beside the child's own turn and thread frames. Each collab call renders as a tool row (spawn_agent, wait_agent, close_agent, ...) naming the helper the way the roster does, with what the helper said back as its output, instead of the raw provider row. * fix(native-chat): name a spawn row by its prompt until the roster holds its helper Also pin that the roster row appears when the helper's first turn arrives before the spawn call finishes. * test(native-chat): a spawn call keeps its own row beside the roster row it starts * fix(native-chat): pair a structured tool row's output with its own call A run paired results to calls by position alone, so once one call finished with no output (a Codex spawn_agent row) every later output drew under the call before its own. A structured row carries its call and output together, so the projected result now names its call id and pairing honors it, falling back to position for results that name none. * fix(native-chat): the Codex roster row follows the executions, so every child ending settles it The subagent-group row was rewritten only on a child's turn/started and turn/completed. A child turn ended any other way — a fatal error, its thread closing, its caller closing it — settled the strip and the host record through the executions but left the transcript row reading working until the session ended. The executions now say when a child's execution changes, and the roster revises its row from that, whichever frame changed it. handleTurn still re-derives to hand back its write admission; the revision is idempotent. * fix(native-chat): a restored Codex call row names its helper, not its thread id History replay never runs the live item router, so the roster never learned the helpers a restored thread had spawned, and a restored wait_agent, close_agent or send_input row labelled its helper with the raw thread id. Replay now registers each announced helper for its name and membership only: register starts no execution, so no strip entry, record or roster row claims the helper runs until a live turn of its own says so. The replayed-item handling moves into the restore module beside the replay that calls it. Also name the roster's render contract in handleItem, and say why the collab tool-name map is not a spelling fix. * fix(native-chat): register a Codex helper whose spawn call failed but created its thread Codex reports a spawn as failed when the helper it created errored at birth, yet the call still names the thread it created, and that thread can run. The spawn was registered only when the call completed, so such a helper existed nowhere and its shell read as the session's own bare command: the original default-mode bug. A spawn now registers whenever it names a receiver; one in progress, or one that created nothing, names none. A failed closeAgent still ends nothing, because the helper was not closed. * refactor(native-chat): each Codex thread's latest token total gets its own home Every thread's running total, member or not, with the rule that the newest frame replaces the last and the recency-ordered cap, moves out of the roster into codex-thread-token-totals.ts. The roster still selects its children's totals at write time. With the producer linkage the roster now builds, it was past the size limit. * refactor(native-chat): the messages a session list quotes get their own module The readers of a session's newest own prompt and own assistant prose move from the status projection into structured-agent-session-latest-messages.ts, and are re-exported so their consumers keep one import site. With the status clock the projection now carries, a tool result naming its call put it past the size limit. * test(native-chat): a Codex helper's command is a live record until its process reports its exit A helper's in-turn shell is a live command record owned by the helper and is also its open Bash operation; its exit removes the record. A caller's closeAgent kills the helper's processes, and each still reports its exit on the helper's thread, so the close leaves the command to that exit rather than ending it itself. * fix(native-chat): every reader of a tool run pairs a result with the call it names The folded desktop run now pairs a result with the call it names, but mobile's tool run, the desktop edit cards and the task lists still paired by position through `pairToolBlocks`. In a Codex default-mode session the new `spawn_agent` row finishes with no output, so on mobile the helper's reply drew under `spawn_agent` and `wait_agent` showed none, and a desktop edit card could take the next command's output as its own. `pairToolBlocks` now follows the same rule as `pairNativeChatToolResults`: a result that names its call answers that call, and one that names none answers the oldest unanswered call as before. * fix(native-chat): a Codex helper's turn ends on its own frames, not on its caller's closeAgent A finished `closeAgent` call ended the helper's running turn as `stopped`. But the call's status is not the close's outcome: Codex sets it from the helper's own agent status, so a close that errors on a running helper still reports `completed`. Orca then marked a helper that was still running as cancelled in the strip, the host record and the roster row, and because the first ending a turn gets stands, its real ending could never correct it. A close that works does not need the edge either. Codex's shutdown of the helper aborts its running turn, and the app-server reports that as the helper's own `turn/completed` with status `interrupted`, which Orca already maps to `stopped`. So a helper's turn now ends only on its own frames (its `turn/completed`, a fatal `error`, `thread/closed`) or the session ending, and the close is only its call row. This also drops the child-work evidence re-keying that existed only for the close: every remaining frame names the thread that sent it. * fix(native-chat): a tool result that names a call is never given to a different call A result that named a call id with no unanswered match fell back to the oldest unanswered call. On mobile, a run shows at most six calls, so a command past that window whose output named its own call gave that output to an earlier call that finished with none, such as `spawn_agent`. A result that names its call now answers only that call and otherwise stays unpaired. A result that names no call still answers the oldest unanswered one. Both pairing readers share the one rule through `answeredToolCallIndex`. * test(native-chat): name the Codex default-mode test file after the subagents it covers * fix(native-chat): a Codex helper is known from any call that names it, not only its spawn A helper whose spawn Orca never saw (history replay after compaction dropped the spawn, or a resume or message to a helper from earlier) stayed unregistered, so its shell read as the session's own command: the bug this PR fixes, in another shape. Every collab call now announces each helper it names, skipping a receiver the finished call reports notFound. The spawn's prompt still labels a helper when it was seen; otherwise the helper has no label and reads as any unnamed subagent does. A send_input, like the spawn, names the parent turn the helper's next run belongs to. The session's own thread is excluded once, in the reader, instead of at each of its three callers. * fix(native-chat): every finished Codex collab call row carries what the call reports A client that predates result call ids (an older mobile app, or an older desktop reading a newer host) projects the journal itself and pairs each tool result with the oldest unanswered call. This PR publishes collab calls as tool rows, and a finished spawn_agent, send_input, close_agent or resume_agent on a running helper had no output, so such a client drew each later output in the run under the call before its own (the parent's shell output under spawn_agent, the helper's reply under the shell). Every finished call now has an output taken from the item: a wait's reply from each helper that finished (an errored helper's error), and for every other call the helper's reported status in Codex's own words (Pending init, Running, Completed, ...). A close or resume no longer shows the helper's last reply as if the call returned it. A wait whose end names no helper (it timed out, or v2) reads Finished waiting; any other call with no state reads its own status. A call also keeps naming the helpers its started item named: Codex ends a timed-out wait with no receivers, so its row lost the helper's name when it finished. * refactor(native-chat): the desktop run's tool pairing is a view of pairToolBlocks Desktop runs and every other reader (mobile, edit cards, task lists, the ask row) each had their own pairing loop sharing one index rule. The desktop's pairNativeChatToolResults now reads the pairs pairToolBlocks makes, so a run is paired by one loop and a new reader cannot add a third. No behaviour change. * test(native-chat): type the positional-client pairs without assertions * fix(native-chat): a finished Codex collab call row says what the call did, in Codex's own words A close_agent row read `Running` and a spawn_agent row `Pending init`: each showed the helper's status snapshot from before the call took effect, so a close that stopped its helper read as though it had not worked. Each finished call's output now follows Codex's own client: spawn reads `Spawned` (or `Agent spawn failed` when no helper was created), send_input `Sent input`, close `Closed`, and resume the helper's status summary. A wait keeps each finished helper's reply, with other statuses in Codex's summary wording (`Error - <message>`). The row label already names the helper, so the output does not repeat it. * docs(native-chat): the collab call reader says which calls show the helper's snapshot as output Since spawn, send_input and close rows say what the call did, the reader's header was wrong to claim every call row shows the reported snapshot as its output: only a wait or resume does. * fix(native-chat): a Codex spawn call no longer claims it ran an agent Codex's spawn_agent call ends as soon as the helper starts, so counting it as running an agent drew "Ran 1 agent" directly above "Kicked off 1 subagent · working" while the helper was still working, and "Ran 1 agent · 1 failed" when Stop cancelled a spawn before any helper existed. Claude's Task call lasts as long as its subagent, so it keeps the agent category; the Codex spawn row is now a plain tool call and the roster row alone stands for the helper. * test(native-chat): a Codex helper's row settles on the failed completion that follows a fatal error Codex follows every turn-ending `error` with the turn's failed `turn/completed`, on a helper's thread as on the primary, and only that completion ends the turn. The roster test now sends both and checks the row, strip and record stay working through the error and settle on the completion. * fix(native-chat): a Codex helper's section opens while the parent spawns, waits on or messages it A running chat holds a subagent's section open while the parent's newest row delegates to that subagent. For Codex that rule only knew the raw collab status row; this PR writes each collab call as a tool row (spawn_agent, wait_agent, send_input, close_agent, ...) naming its helpers in `input.agents`, so a default-mode helper's section stayed shut for its whole run while Claude's opened. The delegation reader now reads a Codex collab tool row as a delegation to the first helper it names, the same rule the raw row keeps for journals written before this change; a call naming no helper stays ordinary output. The row names and the `agents` key live in one shared module the host writes from and the reader reads, instead of a second list. The new test drives the real adapter's rows through the transcript projection to the delegation; the collab frame harness moves to a fixture shared with the positional-clients test. * fix(native-chat): a Codex collab call with a long prompt still names its helper A collab call row bounded its whole input as one value, so a spawn or send_input whose prompt passed the 16 KB journal limit was stored as a clipped wrapper: the row lost the helper's name and thread ids, and with them the label and the delegation that opens the helper's section. The prompt is now clipped on its own with the journal's inline-text bound, marker included; the helper's name, ids, model and effort are bounded as before and always survive. |
||
|
|
bb91e39eb9 |
test(native-chat): main's Codex child tests expect a turn to end on its own turn/completed (#23783)
* fix(native-chat): a Codex child's turn ends on its own turn/completed #22553 ended a child thread's turn on an `error` Codex will not retry, reading the verdict module #23682 deleted when it made turn/completed the only end of a Codex turn. The two landed minutes apart with no textual conflict, so main no longer typechecks. Codex runs every thread's events, spawned children included, through the same per-thread handler: a turn-ending error is recorded as the turn's last error and the turn then completes as failed. So a child's failed turn/completed is its end, as on the primary thread, and a closed thread stays the one child ending with no completion. * refactor(native-chat): a closed Codex child thread is its own frame With a child's turn now ending only on its own turn/completed, the turn-ended frame carried a fixed turnId (null) and state (unverifiable), and endTurn took parameters nothing passed. Name the one remaining case: a thread-closed frame, and closeThread ending the running turn as unverifiable. Drop a test step that no longer exercised anything. |
||
|
|
153d3fd3fa |
feat(native-chat): Codex sessions write their subagents into the host status store (#22553)
* 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. |
||
|
|
f2af92b2fa |
feat(native-chat): show live background work and name each row by kind (#19705)
* fix(codex): reserve the label's share of a qualified command row
A child's label is raw provider text and was spliced into the command
row unbounded, then the pair clipped to the description cap. A label at
or past that cap clipped the command away entirely, leaving a row of
kind 'command' that named an agent and showed no command - the failure
qualification exists to remove, inverted. The same clip could also cut a
surrogate pair, which boundSubagentField already guards against on the
agent row two lines away.
Give the label a reserved share and clip it the way the agent row does.
* feat(native-chat): show live background work and name each row by kind
The strip suppressed itself in three places: the Claude tracker blanked
its roster for the whole of any turn, the Codex tracker returned nothing
while a primary turn was open, and the renderer view gated on
`turnId === null`. Between them, work in flight was never shown — and a
task backgrounded in an earlier turn vanished from the strip as soon as
the next prompt was sent. Claude additionally dropped every foreground
subagent, so a fan-out reported nothing at all.
Report work while it is live, in all three layers. Foreground Claude
work is turn-scoped, so `result` retires it — that is the provider's own
outcome for a task it marked foreground, not a roster sweep. Nothing
settles a Codex child on turn end: those keep reporting well past their
parent, so turn frames only prompt a republish.
Name each ROW by kind — Subagent, Shell command, Workflow, Monitor —
instead of a generic "Background <kind>", each drawing the glyph the
shared tool-icon table already uses for that category. A row that
carries a provider description still shows it unchanged. The collapsed
header summary is deliberately untouched; it is owned elsewhere.
The conversation-command gate is unchanged in effect: an open turn
already refuses first, and Claude foreground work never reaches the
backgrounded set the gate reads.
* fix(native-chat): withhold the row stop Claude foreground work cannot honour
The strip now publishes foreground rows, but `stoppableTaskIds` still filters
on `backgrounded`, so `stopClaudeBackgroundTasks` resolved an empty target list
and returned `{ cancelled: false }` that no renderer reads: the user clicked
"Stop Subagent" and nothing ever happened.
Carry stoppability per row instead of widening the stop to a target the SDK has
no way to reach. `AgentSessionBackgroundTask.stoppable` is absent-means-yes, so
hosts that predate it keep their working control, Claude emits `false` only on
foreground rows, and the strip hides that row's button the same way it already
hides the stop-all a provider cannot honour.
* fix(claude): scope aggregate-roster authority to the work it enumerates
`background_tasks_changed` lists BACKGROUNDED tasks, so a foreground subagent
can never appear in it. Treating it as the whole world meant any such frame
cleared every live foreground row mid-flight and then dropped every later
foreground `task_started` for the rest of the session, killing the in-turn
fan-out the strip exists to show in any session that ever backgrounds anything.
Decide `backgrounded` before the staleness guard and apply the guard only to a
backgrounded start, and retain live foreground entries across a roster replace.
Retained rows count against MAX_TRACKED_TASKS, so the map stays bounded, and a
stale backgrounded start the roster no longer lists is still dropped.
* test(native-chat): pin the strip's monitor amber to the constant that defines it
`MONITOR_GLYPH_COLOR`'s comment claimed a test held it and AgentStateDot's amber
together, but no test imported it — the assertions hardcoded 'text-yellow-500',
so the two could drift with every test still green. Read the colour from the
module, which is what the comment always said was happening. Drop the unused
`BackgroundTaskGlyph` export too: nothing outside the module names it.
* fix(native-chat): keep the task list open across a gap in live work
The strip is now mounted on live work, so a sequential fan-out unmounts it
between one subagent finishing and the next starting: local `useState` meant
the expanded list collapsed itself on every such gap, on top of the strip
flickering above the composer.
Hand the disclosure to the session, keyed by session id so it does not leak
across a session switch. The strip is now controlled and holds no state of its
own, which is what makes it survive its own mount churn.
* fix(codex): route every command-row cut through one surrogate-safe clip
`boundLabel` avoided splitting a pair, then `qualifiedDescription` re-cut the
COMPOSED string with a raw slice: label (<=96) plus separator plus description
(<=512) is up to 611 chars, so that second cut landed at an arbitrary index
inside the description and could publish a lone high surrogate — lossy through
any non-JSON UTF-8 hop. `parse` had the identical hazard on an unqualified
primary-thread command.
One `boundText` helper now owns all three cuts, so no path in the file can emit
a lone surrogate from well-formed input.
* fix(claude): keep terminal evidence for ids an aggregate roster never lists
Narrowing the admission guard to backgrounded starts left a finished FOREGROUND
id with no defence: `replaceAggregateRoster` wiped `terminalTaskIds` wholesale,
so after any `background_tasks_changed` a replayed `task_started` revived a task
whose completion had already been seen — and only a later `result` could settle
it again.
Scope the wipe the same way the guard was scoped: delete only the ids the
incoming roster actually enumerates. A roster still overrules terminal evidence
for the work it lists, which is what that behaviour was added for.
* fix(claude): keep retained rows in place and evict the stalest, not the newest
Re-adding retained foreground entries after the roster made a live row the user
is reading jump below the backgrounded rows on every `background_tasks_changed`,
and the cap `break` kept the STALEST retained rows while dropping the newest.
Merge in the tracked map's own order so a surviving row holds its position, and
count the overflow up front so eviction takes the oldest retained rows. Roster
entries are never starved and the map stays bounded either way.
* fix(claude): retire leftover foreground rows when the next turn starts
A foreground `task_started` arriving with no turn open has no `result` coming
to retire it, so it sat in the strip indefinitely — with no per-row stop, since
foreground rows are not stoppable — and refused conversation commands behind an
instruction nobody could follow.
Settle on turn start as well as on `result`. This is cleanup only: visibility
never consults `startsTurn`, so a missed one degrades to today's behaviour and
can never switch the feature off. It shortens the row's life to the next turn;
the case where no further turn is ever sent is filed separately.
* fix(agent-session): withhold unstoppable rows from readers that predate them
Rule 3 of remote-wire-compatibility: changing what the host publishes reaches
old clients with no wire change. The Claude host published no foreground rows
before this feature; it does now, and a client that cannot read `stoppable`
draws a per-row Stop on every one of them — Claude always sets
`supportsTaskStop` — which filters to the backgrounded ids, stops nothing, and
returns a result no renderer inspects. That is the dead button `stoppable` was
added to remove, reappearing across a version skew.
Negotiate it. A client can advertise the existing background-task-stop
capability and still predate `stoppable`, so this needs its own constant.
Readers that do not advertise it get unstoppable rows dropped, and a state whose
every row is dropped becomes no strip — exactly their pre-feature view.
RUNTIME_PROTOCOL_VERSION is not bumped: this adds an optional field and a new
negotiated capability, and changes no existing field's meaning, which is the
explicit do-not-bump case in protocol-version.ts.
* test(agent-session): name the projected rows so the fixture typechecks
An indexed lookup into the fixture's task list is possibly-undefined under
`pnpm tc`; the rows are more readable named anyway.
* test(web): advertise the row-stop capability in the e2ee auth expectation
The web e2ee handshake started sending
AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY, and this test asserts the
advertised list by deep equality, so it went red on CI while every targeted
test run stayed green. Add the capability in the position the router sends it.
* test(claude): pin why the roster empties mid-turn in a sequential fan-out
The strip unmounting between two sequential subagents is truthful, not a swept
row: A leaves on the provider's own terminal frame, B does not exist yet, and
backgrounded work spanning the same gap holds the roster open — so an empty
roster is never work the strip is hiding.
Also pins the previous-turn rule against the one the subagent roster already
applies on the same frame: a still-working FOREGROUND child becomes
`unverifiable` there and a backgrounded one is left alone, so the strip drops
the first and keeps the second rather than asserting `live` for either.
---------
Co-authored-by: Merge Sim <merge-sim@users.noreply.github.com>
Co-authored-by: Merge Sim <sim@local>
|
||
|
|
5868fdc9e3 |
feat(native-chat): report Codex background tasks in the chat strip (#19346)
* feat(native-chat): report Codex background tasks in the chat strip The background-tasks strip works for Claude only; a structured Codex session shows nothing in it. Feed it from the Codex app-server stream. The strip stands for work that OUTLIVED a turn, which is what the monitoring header, Claude's foreground suppression, and the conversation command gate all already assume. Codex has no `is_backgrounded` flag, so that fact is derived from the turn boundary: a `subAgentActivity` child or a primary-thread `commandExecution` becomes visible once the turn it belongs to completes and it is still unsettled. `turn/completed` only reveals a task here, never settles one — measured on `codex app-server` 0.153.4, a spawn_agent child reported `completed` 95.8s after its parent turn ended. Only a child's own activity kind settles it. Codex exposes no honest stop: `turn/interrupt` on a child ends its turn without emitting a terminal activity item and leaves its shell running. So the state carries a new optional `supportsStopAll: false`, the strip hides a control that could not act, and the blocked-command message asks the user to wait rather than to press a button that does not exist. * refactor(codex): move session teardown out of the structured adapter Merging main crossed the 300-line cap on `codex-structured-session-adapter.ts`: the rewind backend (#19235) and this branch's close-time strip clear both landed in it. The four close paths move verbatim into `codex-structured-session-teardown.ts`, where they funnel through one `settled` helper instead of repeating the notification-retry and background-task cleanup at each call site. No ratchet bump. Also normalize a background task's description once at receipt rather than on every projection; the roster is re-projected on each observed frame. * fix(codex): drop the shell row the journal already settles A `commandExecution` still `inProgress` when its turn ends was reported as a `command` task. But `settleCodexJournalTurn` writes exactly those items to the journal as `state: 'failed'` on `turn/completed` and forgets them, so the strip row would have claimed a shell was still running at the same instant Orca recorded that it was not — two surfaces contradicting each other about the same process. A subagent is the opposite case and stays: the roster pointedly does not sweep at a turn boundary, because children measurably outlive it. That leaves the producer making exactly one claim — these spawn_agent children are still live after their turn — which the durable roster row corroborates. * fix(native-chat): track Codex background execution lifetimes * fix(native-chat): keep running tool groups from claiming completion * Fix runtime catalog and capability expectation * fix(codex): keep a child's name on the command row that outlives it A child agent's commands stay hidden behind its agent row while the child works. Once the child's turn settles with a command still running, that command surfaces as its own row labelled from the raw command string, so 'long_probe' became "/bin/zsh -lc 'ping -c 300 127.0.0.1 > /dev/null'" at the moment that row was the only remaining signal for the work. Qualify a child's command row with the child's label. Resolved on read, so a label registered after the command still lands, and bounded by the existing description cap so admission accounting stays valid. Primary- thread commands are left unqualified: they have no child to name. --------- Co-authored-by: Merge Sim <sim@local> |