Skip to content

feat(agent-status): combine Codex child work through the shared main-agent status fold - #22475

Merged
brennanb2025 merged 5 commits into
mainfrom
brennanb2025/lead-status-pr-d
Sep 24, 2026
Merged

brennanb2025 merged 5 commits into
mainfrom
brennanb2025/lead-status-pr-d

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​217 $\color{#cf222e}{\Huge{\mathbf{−}}}$​24 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​193
Prod 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​122 $\color{#cf222e}{\Huge{\mathbf{−}}}$​72 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​50

ELI5

Orca shows one status per agent pane: working, waiting for you, or done. When the main agent has helpers (subagents) running, that one status has to combine what the main agent is doing with what the helpers are doing. Every kind of agent used one shared rule for that, except Codex, which had its own copy. This change teaches the shared rule the one thing Codex's copy could do that it could not (a helper waiting for your approval makes the whole pane say "waiting"), moves Codex onto the shared rule, and deletes Codex's copy.

What Changed

Before and after, as the user sees it. No visible change. A Codex pane whose helper is waiting for approval reads "waiting", and one whose main turn finished while a helper still runs reads "working", exactly as before. What changes is that the next fix to the shared rule now reaches Codex too, instead of silently skipping it.

Mechanism.

  • The shared summary of "what are the helpers doing" (src/shared/agent-status-child-work-liveness.ts) gains a waiting answer, ranked above working and monitoring. Only a helper whose own state is waiting produces it. A helper's blocked means its task failed (the only producer maps a failed task to it, and the task view labels it "failed"), so it still counts as ordinary live work, as before. Lost contact (unverifiable) is not a request for a human either.
  • The shared rule (src/shared/agent-lead-status-fold.ts) reads that answer: a waiting helper makes the row waiting whatever the main agent is doing, unless the main agent is itself asking, in which case the main agent's own word wins.
  • Codex's private rule (codexRosterEffectiveState) is deleted. Codex now only gathers evidence (every tracked helper is an agent; its state feeds the shared summary), and one function, resolveCodexPaneStatus, folds the Codex main agent with that evidence. All three Codex publish paths go through it: main agent events, helper-driven events, and relayed rows that the main machine reconciles for SSH sessions.
  • The "was this turn cancelled" test the rule reads moves from a Claude-only helper into the shared rule's module, and Claude and Codex both use it.
  • Codex joins the cross-agent parity table (src/shared/main-agent-status-parity.test.ts), which checks that every agent type publishes what the shared rule says for its evidence.

Why Codex results cannot change: a Codex helper is only ever working or waiting, and a finished helper is removed rather than kept as done. So "any tracked helper" and "any live helper" are the same thing, and every combination of main agent state and helper states gives the same answer under the old and new rule. Codex never produces the monitoring mode, because every Codex helper is agent work.

Why

The agent status reference doc's rule is that every agent type derives the combined status from one shared rule. Codex was the last one with its own copy, so a future change to the shared policy would silently not reach it.

Alternatives considered:

  • A separate "a helper is waiting" input to the rule, beside the helper summary. Not chosen: the summary is already the rule's model of what the helpers are doing, and it already takes the waiting fact as its own input, so a second parameter would only rename it. The summary is in-memory only (never sent or saved), so this can change later without a migration.
  • Recording a helper's request on the main agent's row when it arrives, instead of reading it from the helper's own state. Not chosen: this change works the wait out from the helper's current state each time, which is how the rest of the status store works ("derive, don't store"). It cannot count several open requests from one helper, which a single status dot does not need.
  • Codex copying Claude's approach (the helper's wait replaces the main agent's own record). Not chosen: it hides what the main agent is really doing, and it is the source of the known issue below.

Known issues (existing on main, not caused by this PR)

The same event sequences give the same results on main and on this branch. They are listed in docs/reference/agent-status-store.md and will be fixed in a follow-up stacked on this PR:

  • Claude: a helper's approval request is kept in one slot on the main agent's record. The main agent's next event (running a tool, finishing its turn) overwrites it, so the pane goes back to "working" while the helper is still waiting. A second helper asking replaces the first.
  • Codex: when the main turn ends and Orca is not also tracking the helpers through their transcripts, the helper list is dropped, so a helper still running or still waiting stops holding the pane (the pane reads "done"). Screenshot C below shows this.
  • Native chat: a helper's pending prompt is treated as the main agent's own "needs you".

Linked Issue

Follow-up to #22452 (which added the main agent's own state to each row) and #22295. No separate issue.

Visual Proof

Validated in a hidden dev build of this branch at f46d03790a (macOS). Codex is not installed on the test machine, so each scenario was driven by running Orca's installed Codex hook script inside a real Orca terminal with Codex-shaped events. The screenshots show the sidebar row and the tab.

Scenario Expected Seen
A Main agent working, helper working working working
B Helper asks for approval waiting waiting ("Codex - action required")
C Main agent finishes while the helper still runs working done, the known Codex issue above, same on main
D Main agent finished, then the helper asks for approval waiting waiting
E Helper finishes too done done

A
A

B
B

C
C

D
D

E
E

Testing

  • pnpm tc: pass.

  • pnpm exec oxlint on changed files and pnpm run check:code-quality:changed: clean.

  • pnpm test over the shared rule, helper summary, Codex roster, parity, native chat status, Codex main agent, hook listener and src/main/agent-hooks suites: pass.

  • Deletion checks, each restored afterwards and each failing for the intended reason. These were run on the commits that introduced each piece, not re-run at the final head: the rule's waiting-helper branch, the main-agent-asks-first guard, the waiting evidence in the helper summary, the Codex helper state feeding the summary, the relayed-row re-derivation, and the waiting-only rule (restoring blocked as waiting fails the two tests that pin a failed helper as live work).

  • The Known issues sequences were run against origin/main and this branch, and matched.

  • Platform: macOS. The change is shared logic with no platform branch.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

No wire change: state, workingMode and mainAgent already exist on the row, and helper waiting is already an accepted helper state. For SSH, the main machine re-derives a relayed Codex row through the same rule; relays of any version send the helper list the rule reads, and old and new rules agree on it. Mobile reads the published row and needs no change.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — the Codex hook lane's private child-combine is deleted and Codex now folds through the shared foldAgentLeadStatus, which gains a waiting-child input; the full 16-file diff at a897f06602 was reviewed.

  • Shared fold gains a waiting child arm — agentChildWorkLiveness ranks a child in waiting/blocked above working/monitoring, and foldAgentLeadStatus returns waiting for a working or settled lead unless the lead is itself asking.
  • Codex combine deleted, fold adopted — codexRosterEffectiveState is removed; resolveCodexPaneStatus feeds the roster through the shared rule on all three publish paths (root events, child-driven events, relayed rows).
  • Cancellation carried into a late Stop — codexCarriedTurnOutcome preserves Orca's inferred cancel where Codex's own Stop carries no verdict, mirroring the Claude lane.
  • Claude lane insulated — hasWaitingChildWork: false, with the wait still expressed on the displaced lead record (waitingAgentId/stateBeforeWait).

Verified: Codex's published { state, workingMode, lead } is unchanged for every reachable input (the only delta is the intended lead.outcome: 'cancellation' on a late Stop); the new waiting arm is unreachable for the structured, Claude hook, and Grok lanes' current producers; no wire change. 71 scoped tests pass and pnpm tc exits 0. Two non-blocking notes: a foreign/other-version host emitting a structured live task in waiting/blocked would now flip the row to waiting, and the hasWaitingChildWork: false hardcode relies on TrackedClaudeSubagent.state staying working|idle.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025
brennanb2025 force-pushed the brennanb2025/lead-status-pr-d branch from a897f06 to b376568 Compare September 23, 2026 17:48
@brennanb2025 brennanb2025 changed the title feat(agent-status): combine Codex child work through the shared lead-status fold feat(agent-status): combine Codex child work through the shared main-agent status fold Sep 23, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — the delta since Pullfrog's prior review at a897f06 (a rebase onto PR A's mainAgent naming plus shared-helper consolidation) at b376568.

  • Rebased onto the renamed base — the PR now builds on PR A's mainAgent terminology; the parity and Codex test files, main-agent-turn-state.ts, and docs follow.
  • Interrupted helper consolidated into the fold — the base's Claude-local claudeMainAgentTurnInterrupted is deleted in favor of the shared mainAgentTurnInterrupted in agent-lead-status-fold.ts (now typed off AgentMainAgentStatus). Semantics are unchanged (outcome === 'cancellation').
  • Codex parity assertion simplified — the Codex lane now refolds solely through refold(row.mainAgent, codexRosterChildWorkLiveness(roster)); the now-unreachable codexMainAgentState blocked-root guard was dropped.

Verified locally: pnpm tc exits 0 and the six touched suites pass (71 tests). The waiting-child arm, Codex fold and cancellation carry are behaviorally unchanged from the previously reviewed commit.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025
brennanb2025 force-pushed the brennanb2025/lead-status-pr-d branch from 4d76d65 to f0486e5 Compare September 24, 2026 01:09
@brennanb2025
brennanb2025 changed the base branch from brennanb2025/lead-status-pr-a to main September 24, 2026 01:09
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 8dddb153-6cc8-484f-9e47-3c94967d8553

📥 Commits

Reviewing files that changed from the base of the PR and between 5fecbe2 and 26f1197.

📒 Files selected for processing (4)
  • docs/reference/agent-status-store.md
  • src/shared/agent-lead-status-fold.test.ts
  • src/shared/agent-lead-status-fold.ts
  • src/shared/main-agent-status-parity.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/shared/agent-lead-status-fold.test.ts
  • docs/reference/agent-status-store.md
  • src/shared/agent-lead-status-fold.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The shared status fold now accounts for child work waiting on a human. Blocked child work remains live but does not set the combined row to waiting. Claude and Codex hook-provider paths use shared interruption detection, and Codex pane status is resolved from main-agent state and child-work liveness. Tests cover these behaviors across provider lanes and structured sessions. Documentation describes the combining rule and the remaining named differences.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 26f11

The changed status paths have no established merge-blocking issue. The Codex child-event fallback preserves a recorded root state.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 25 functions across 15 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the main change: moving Codex child-work combination onto the shared main-agent status fold.
Description check ✅ Passed The description is complete and directly supports the change. It includes the user impact, mechanism, rationale, alternatives, known issues, linked prior issues, visual proof, testing, platform consid…
Full details: Docstring Coverage

Explanation

Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 25 functions across 15 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ade stale

A child failure reported in place as `blocked` now pins the row `waiting`, not
`working`; and no relay ever sent an unfolded `working` beside a waiting child.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — the delta since Pullfrog's prior review at b376568 (a rebase onto the newer base 80f5aae0f9, plus a helper consolidation), and the full 16-file diff at f0486e5f.

  • Adopted the base's mainAgent semantics for the Claude wait — the base now publishes the pre-wait state (stateBeforeWait) as mainAgent instead of waiting, so the parity table's two child-wait Claude rows expect working/done beside the waiting row, the test's claudeChildWorkLiveness helper derives hasWaitingChildWork from waitingAgentId, and the docs drop the Claude divergence note.
  • Dropped the redundant cancellation helper — the feature's codexCarriedTurnOutcome is deleted; both Codex publish paths now use the base's codexOutcomeRestatedByStop. Codex failure is unreachable (codexLeadStateForHookEvent handles no StopFailure), so this is behavior-preserving for every reachable input.
  • The waiting-child arm, Codex fold adoption, and relay re-derivation are unchanged from the previously reviewed commit.

Verified locally: the six touched suites pass (71 tests). No new issues.

ℹ️ The PR description's Claude divergence note is now stale

The "Two things a reviewer should know" point #2 still says Claude's published mainAgent.state reads waiting while the main agent is really working or done, and that the parity table pins that divergence. The code and docs now say the opposite — claudeMainAgentStatusForPayload publishes stateBeforeWait, so Claude's mainAgent matches Codex and only the cancelled-turn-with-watch-loop divergence remains. Worth updating the description so a reviewer is not sent looking for a divergence that no longer exists.

Technical details
# Stale PR description: Claude mainAgent no longer reads `waiting`

## Affected sites
- PR description, "Two things a reviewer should know" point #2 — claims Claude's `mainAgent.state` reads `waiting` and that the parity table pins it as a known divergence.
- `src/shared/agent-hook-listener/providers/claude-roster-state.ts:151` — `claudeMainAgentStatusForPayload` publishes `stateBeforeWait` when `waitingAgentId` is set.
- `docs/reference/agent-status-store.md:240` — "publishes the displaced state as `mainAgent`, so its rows match Codex."

## Required outcome
- The description's Claude note reflects the current behavior: the displaced wait is the child-work fact, Claude's published `mainAgent` matches Codex, and only the cancelled-turn divergence remains.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

A child's `blocked` state means its task failed (the only producer maps a
failed background task to it, and the background-task view labels it
"failed"), not that a human must act. Folding it into the waiting arm would
surface a failed child as needs-you. It stays live work, as before this
series.
… row wait

A child's blocked state means it failed; only its waiting state feeds the
waiting arm. Two fold comments, a test describe and two parity story names
still called the waiting child blocked.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — a couple of doc/description alignment nits for the narrowed waiting arm.

Reviewed changes — the delta since Pullfrog's prior review at f0486e5f (commits 2db5562 and 5fecbe2).

  • Child-waiting arm narrowed to waiting — isWaitingChildWork no longer treats a child's blocked state as a human request. A blocked child is a failed one (its only producer maps a provider failed task to it), so it is live work again and an idle main agent beside it reads working, not waiting.
  • Docs and tests realigned — the reference doc, the liveness and structured suites, and the fold expectations follow the narrower arm; isLiveChildWork's comment now says an in-place blocked failure pins working.
  • Comment-only corrections — 2db5562 fixed the relay test comment and the isLiveChildWork doc.

Verified: failed → blocked at claude-background-task-frames.ts:139 and blocked: 'failed' in native-chat-background-task-row.ts confirm the state means failure, not a human request; the Codex roster is typed working | waiting only, so no reachable Codex input changes. Five touched suites pass (66 tests).

ℹ️ The PR description still describes the reversed policy

The "Mechanism" bullet, point #1 of "Two things a reviewer should know", and the third rejected alternative all still say the waiting arm is produced by a child in waiting or blocked, and list "Keeping blocked children out of the waiting arm" as a rejected alternative. Commit 5fecbe2 implements exactly that rejected alternative: only waiting produces the arm, and a blocked child stays live working work. A reviewer reading the description would be sent looking for the opposite of what the head does.

Technical details
# PR description contradicts the narrowed waiting arm

## Affected sites
- PR description, "Mechanism" bullet 1 — "A live child whose own state is `waiting` or `blocked` produces it."
- PR description, "Two things a reviewer should know" #1 — says the structured pin now expects `waiting` for a `waiting` or `blocked` subagent; a `blocked` child still reads `working`.
- PR description, "Alternatives considered" bullet 3 — lists "Keeping `blocked` children out of the waiting arm" as rejected; `5fecbe2` does it.

## Required outcome
- The description reflects the head: a child's `waiting` state is the waiting-arm input; `blocked` (a failed task) stays live work and does not surface as needs-you.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/shared/agent-lead-status-fold.ts Outdated
…the structured lane's real input

The doc said the Claude hook lane's rows match Codex and that every lane feeds a
child's wait into the fold. Neither holds: Claude keeps the wait in one slot the
next main agent event overwrites, the structured lane turns a child's prompt
into the main agent's own attention, and Codex drops its roster on a root Stop
when it tracks no child transcripts. The parity story now drives the structured
lane with the input it actually receives.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: ready to merge at 26f1197cc9

Verdict: ready. This moves Codex onto the shared rule for combining the main agent's status with its helpers' statuses. No Codex pane shows anything different. Known issues that already exist on main are listed in the description and will be fixed in a follow-up stacked on this PR.

Rebase: retargeted to main after #22452 merged. The conflict was resolved in favour of main's more general helper for keeping a verdict when a late Stop arrives. The Claude parity stories were updated, because #22452 already makes Claude publish the main agent's own state during a helper's wait.

Review loops: five, same model each time, the last two clean.

  1. Two stale comments corrected.
  2. Policy fix: a helper in the blocked state means its task failed, so it no longer makes the pane read "waiting". Only a helper that is actually waiting does. Covered by a test that fails without the fix.
  3. Leftover "blocked on a human" wording renamed.
  4. Clean.
  5. After an architecture review, the reference doc's claim that Claude's results match Codex's was corrected. The doc now lists where a helper's wait is still lost: Claude on the main agent's next event, Codex when the main turn ends and helpers aren't tracked by transcript, and native chat, which treats a helper's prompt as the main agent's. The native chat parity story now uses the input that lane really receives. Clean.

Architecture review: keep this design. A helper's wait is worked out from the helper's own state inside the shared rule, not stored on the main row. The known issues come from Claude keeping a helper's wait in a single slot on the main agent's record, and from Codex dropping its helper list when the main turn ends. Both need the waiting step this PR adds, so the follow-up is stacked on top.

Readiness checklist:

  • Start of session: pass, with two minor findings, both since fixed.
  • End of session: pass, no findings.
  • An exhaustive comparison of the old and new Codex rules matched in all 63 input combinations.

Manual validation: hidden dev build, macOS. Codex events were driven through Orca's installed hook script from inside a real Orca terminal; the screenshots are in the description. A, B, D and E pass. C, where the main agent finishes while a helper still runs and the pane reads "done", is the known Codex issue. It is identical on main.

Checks:

  • pnpm tc passes.
  • oxlint and the changed-lines quality gate are clean.
  • The agent-status, hook-listener, Codex, native chat and parity suites pass.

Remaining gaps:

  • No run against a real Codex CLI, which isn't installed on the test machine.
  • No live SSH relay run. The relay path was checked by code review and tests.
  • Mobile needs no change, since nothing new goes over the wire.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — the delta since Pullfrog's prior review at f46d037 (commit 26f1197: the reference doc's divergence paragraph and one parity-story expectation), verified against the code it describes.

  • Reference doc's divergence list corrected — the agent-status-store.md claim that every lane feeds a child's wait into the fold and that Claude's rows match Codex is replaced with a precise list: only the Codex hook lane feeds the waiting-child input today, plus four named divergences — the cancel/watch-loop row, Claude's single-slot displaced wait, the structured lane's session-level attention, and Codex dropping its roster on a root Stop.
  • Parity story drives the structured lane with its real input — the "settled main agent whose child is waiting on the user" story now feeds status: 'attention' with a working child and pins the resulting blocked divergence under a KNOWN DIVERGENCE comment, replacing a synthetic state: 'waiting' task that no in-repo structured producer emits.

Each documented mechanism checks out in code: codex-events.ts:216 drops the roster on a childless-transcript Stop; claude-events.ts:235-251,295 stashes the displaced state and overwrites waitingAgentId on a later child wait; structured-agent-session-projection.ts:232-239 returns attention for a pending approval/question; and the structured live-task producers only emit working (claude-background-task-frames.ts:150) or no state (codex-background-task-tracker.ts:61), so no waiting task reaches the fold from a real producer. pnpm test on the parity, structured, fold and liveness suites: 58 passed.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant