Skip to content

test(native-chat): main's Codex child tests expect a turn to end on its own turn/completed - #23783

Merged
brennanb2025 merged 5 commits into
mainfrom
brennanb2025/codex-child-turn-ends-on-completion
Sep 29, 2026
Merged

brennanb2025 merged 5 commits into
mainfrom
brennanb2025/codex-child-turn-ends-on-completion

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​32 $\color{#cf222e}{\Huge{\mathbf{−}}}$​33 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1
Prod 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​17 $\color{#cf222e}{\Huge{\mathbf{−}}}$​42 $\color{#cf222e}{\Huge{\mathbf{−}}}$​25

ELI5

#23808 put back the rule "a Codex helper agent's turn ends when Codex reports a fatal error". This PR takes it out again, because Codex always sends its own "turn finished (failed)" message for that turn a few milliseconds after the error, for helpers exactly as for the main agent. A helper's turn now ends on that message. The one case with no such message, a helper thread that closed, keeps its own clearly named ending.

What Changed

  • Before (main at fix(native-chat): keep a child turn settling on an error Codex will not retry #23808): a helper's error with willRetry: false settles it as failed, and the failed turn/completed Codex sends 0-32 ms later is a second ending for the same turn.
  • After: the helper keeps working through the error and settles as failed on the failed turn/completed that follows. readCodexErrorWillRetry and the child error arm in codex-background-task-frames.ts are removed; nothing else reads them.
  • Also: the leftover frame { kind: 'turn-ended', turnId: null, state: 'unverifiable' } (two fields that can only hold one value, under a name that really means "the thread closed") becomes { kind: 'thread-closed', threadId }. endTurn(threadId, turnId, state) becomes closeThread(threadId). A test step that could no longer fail is dropped. No behaviour change.
  • Users see no change: a helper agent whose turn fails still shows as failed.

Conflicts with #23808

This merge reverses #23808's child error arm. #23808 restored "a child's turn settles as failed on an error with willRetry: false" (an arm in codex-background-task-frames.ts, plus readCodexErrorWillRetry in codex-structured-thread-facts.ts, which nothing else reads) on the claim that a child's turn/completed may never arrive. Its only evidence was the older tests.
Codex's source says the completion always follows. Every thread, spawned helpers included, runs through the same per-thread handler (app-server/src/lib.rs:1282-1298 attaches it on notify_thread_created from core/src/agent/control/spawn.rs:875; app-server/src/request_processors/thread_lifecycle.rs:344). A turn-ending error breaks the turn loop and returns normally (core/src/session/turn.rs:818-834). The task wrapper then always finishes the turn (core/src/tasks/mod.rs:395-398) with TurnComplete (core/src/tasks/mod.rs:844-857), or TurnAborted if it was cancelled; both reach the client as turn/completed. The app server records the error as the turn's last error and completes the turn as failed (app-server/src/bespoke_event_handling.rs:185-200, 1503-1531, 1607-1631).
Live on Codex 0.157.0, a helper's willRetry: false error was followed 3 ms later by that helper's failed turn/completed for the same turn.
The merge keeps #23808's unrelated fix to pr-workflow-parallelism.test.mjs, which still read the deleted track-community-prs.yaml.
A child still settles if Codex dies after the error: the exit handler sends the session's ended event and settleSession() sets every working child to unverifiable, and a helper thread that closes with no completion settles through closeThread. Both paths are covered by the session-end and closed-thread tests in codex-child-work-evidence.test.ts and codex-subagent-roster.test.ts.

Why

  • fix(native-chat): only Codex's turn completion ends a Codex turn #23682 made turn/completed the only live end of a Codex turn, backed by captures from the real app server, and fix(native-chat): drop the Codex error path #23682 removed #23801 applied that to helper threads in code. Tests written for the older rule now fail on main.
  • Codex runs every thread's events, spawned helpers included, through the same per-thread handler. That handler records a turn-ending error as the turn's last error, then completes the turn as failed.
    • Checked in Codex's app-server source.
    • Checked live on Codex 0.157.0: a helper thread's error with willRetry: false was followed by turn/completed with status failed for the same turn, 3 ms later.
  • Naming the closed-thread case for what it is keeps the frame from suggesting a named-turn ending still exists.

Linked Issue

Follow-up to #23682 and #23801: main's unit tests still expected the error ending those PRs removed.

Visual Proof

N/A: no UI change. The live QA screenshots are in the status comment.

Testing

  • pnpm tc:node passes.

  • oxlint is clean on the changed files.

  • The Codex child-work, producer, tracker, background-task, projection and close suites pass: 7 files, 79 tests. On main (with fix(native-chat): keep a child turn settling on an error Codex will not retry #23808), the child-work tests assert the error ending instead.

  • Live QA on a real Codex 0.157.0 (background rig, isolated home): a succeeding helper settles on its completion. The error-then-failed-completion sequence was captured on a real helper thread.

  • 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

This is main-process Codex event reading and its tests only. It has no platform, SSH, mobile or wire effect.

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)

#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.
@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Changes how child task completion is detected and tracked.

The PR should not merge until ready PRs retain cross-platform Bun qualification for platform-dependent inputs.

Summary

The PR updates Codex child-work tests and renames the closed-thread frame to match completion-driven turn settlement. Changes merged since the previous review also reduce unit-test sharding and alter Bun platform qualification. The latter leaves ready PRs without pre-merge cross-platform qualification for some platform-dependent inputs.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[PR changes Bun input] --> B{Platform-flavoured path?}
  B -->|Yes| C[Six-platform PR run]
  B -->|No| D[Ubuntu-only PR run]
  D --> E[Merge to main]
  E --> F[Six-platform push run]
Loading

Reviews (5) · Last reviewed commit: "Merge origin/main (#23810) into #23783"

@coderabbitai

coderabbitai Bot commented Sep 29, 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: 6a98e810-eea4-4914-81f1-11c2d2f5e2dc

📥 Commits

Reviewing files that changed from the base of the PR and between cbcd782 and 15e7a42.

📒 Files selected for processing (2)
  • src/main/codex/codex-background-task-frames.ts
  • src/main/codex/codex-structured-thread-facts.ts
💤 Files with no reviewable changes (1)
  • src/main/codex/codex-structured-thread-facts.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

The frame reader now emits a thread-closed frame for child thread closure. The tracker closes the child execution, and a working turn becomes unverifiable. A non-retried child error leaves the child live until failed turn completion settles it. Tests also verify that thread closure settles child work with an unknown outcome and that a later completion does not replace that outcome.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 15e7a

No actionable issue is established that would prevent merging after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 15e7a

The change affects how child-work outcomes are reported, but the reviewed path does not show a new privileged operation or broader access. Some event-ordering and producer-trust details remain unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The observed effect is confined to the tracked child execution and its published work status. The reviewed closure path does not introduce a tool-execution or other privileged sink; broader producer trust and runtime exposure remain unverified.

Trust Boundaries and Controls

  • observed — Notification parameters can supply the thread ID, but the session path checks journal admission before observation; child closure then requires an existing working execution under that ID. The reader itself does not authenticate the notification producer.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 5 files. 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 identifies the main change: Codex child turns now settle on their own turn/completed event. It is specific and related to both the updated tests and production handling.
Description check ✅ Passed The description is complete and follows the repository template. It explains the change, motivation, linked issues, testing, live QA, scope, and checklist status. It also states that no visual change …
  • 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.

@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

  • Dangling import removed — codex-background-task-frames.ts no longer imports the codex-structured-journal-provider-verdicts module that #23682 deleted, so pnpm tc:node compiles again.
  • Child turns now end only on turn/completed or thread/closed — the child-only "fatal error ends the turn" branch and the CodexChildTurnEnding type are gone; readCodexChildTurnEnding becomes readCodexChildThreadClosed, and a failed child completion flows through the existing turn frame branch (codexChildTurnState).
  • Tests retargeted to the surviving rule — codex-child-work-evidence.test.ts now asserts a fatal error leaves the child live and the following failed turn/completed settles it failed, plus the thread/closed → unknown path; the producer test adds the same live-then-failed sequence.

Verified locally at 77bd1cc: the three codex test files pass (59 tests), oxlint is clean on the changed files, and pnpm tc:node passes.

ℹ️ Child-thread parity is inferred from Codex source, not captured

The comment added at src/main/codex/codex-background-task-frames.ts:41 claims the error-then-completion pairing holds "on child threads as on the primary", but every capture in src/main/codex/__fixtures__/codex-app-server-turn-endings.jsonl is a single-thread (primary) session, and codex-structured-journal-turn-endings-replay.test.ts only exercises those. The change is well-supported — child threads run through the same per-thread app-server handler, and Orca can only mark a child working after seeing that child's own turn/started, so the subscription that delivers the start also delivers the completion — but per this repo's evidence convention the child case is the one assertion this PR adds without a transcript behind it. No action required to merge; worth a follow-up child capture (or a note in the fixture) so the thread/closed fallback is also covered by observation.

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

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.

@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

  • Split turn-ended into a dedicated thread-closed frame — a closed Codex child thread now carries only its threadId; the turnId/state fields and the CodexChildTurnEnding type are gone, and readCodexChildTurnEnding becomes readCodexChildThreadClosed.
  • CodexSubagentExecutions.endTurn becomes closeThread — it always settles a still-working child's running turn as unverifiable, dropping parameters that could only ever be null/'unverifiable'; a child running no turn is a no-op, as before.
  • Tests retargeted to the two surviving paths — the evidence suite asserts a fatal error keeps the child live and the following failed turn/completed settles it, plus the thread/closed → unknown path; the producer suite adds the live-then-failed sequence.

Verified at cbcd782: both changed test files pass (33 tests) and pnpm tc:node passes. The change is behavior-preserving — the old endTurn caller also passed turnId: null and 'unverifiable', so closeThread computes the same settlement.

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

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: ready for Brennan's decision (not merged; auto-merge not enabled)

Verdict: the fix is correct and in scope. It turns main's pnpm tc:node green again, and a Codex helper agent's turn now ends the same way the main agent's does: on Codex's own "turn finished" message, failed or not.

Commits on this PR

  • 77bd1ccd386: a Codex helper (child) thread's turn ends on its own turn/completed; a failed one reads as failed. A fatal error on the helper no longer ends the turn early. A helper thread that closes mid-turn still settles as "unknown".
  • cbcd782d0ee (from review): with fatal errors no longer ending a helper's turn, the only leftover case was the helper's thread closing. The code now names that case directly (a thread-closed event and closeThread) instead of a general "turn ended" event whose turn and state were always the same fixed values. It also drops a test step that could no longer fail. No behaviour change.

Reviews: two review loops (loop 1: that one cleanup, fixed above; loop 2: clean), plus start and end readiness checks and an independent read-only review. Codex's app-server source was read first-hand: spawned helper threads use the same per-thread event handler as the main thread, and every error that ends a turn is followed by a failed turn/completed for that turn.

Checks run locally at cbcd782d0ee: pnpm tc:node passes (it fails on main); oxlint is clean; 7 Codex suites, 108 tests, pass (child-work evidence, child-work producer, background-task tracker, session background tasks, session close, subagent executions, subagent roster).

CI at cbcd782d0ee: every check passes (typecheck, all 8 test shards, packaging on macOS and Windows, persistence on all runners, repository guards).

Live QA (real Codex 0.157.0 in a background test build of Orca; a Claude agent ran it because the Grok seat was out of credit)

  • A real helper turn failed with a 400 from the API. Codex sent the helper's error (willRetry: false), then turn/completed with status failed for the same turn 3 ms later. This is the first live capture of this sequence on a helper thread, and it confirms the rule this PR relies on.
  • A helper that succeeds settles as completed 1 ms after its turn/completed. The dock shows "1 agent — working" while it runs and clears afterwards. The transcript shows "Ran 1 subagent · completed".
  • The parent chat keeps working afterwards; no helper is left stuck as working.

Registered helper completed while the parent is still working

Settled: "Ran 1 subagent · completed"

Not verified live: a helper that Orca has registered, failing and reading "failed" in the chat. Helpers are registered only for models on Codex's newer multi-agent tools (GPT-6 and GPT-5.6 Sol/Terra), and those take only a text message, so none of the three attempts could make one fail. The code path from the failed turn/completed to "failed" is covered by the unit tests above.

Found during QA, not caused by this PR: with GPT-5.5 (Codex's older multi-agent tools), helpers are never registered on this build. So a helper that fails shows as raw red error cards at the parent's top level, with no subagent row, while the parent tab shows a green check. This PR doesn't touch the code that registers or draws those rows; open PR #22619 is the change that registers these helpers.

GPT-5.5 helper that failed: raw rows and red error cards, no subagent row

Scope: 4 files in the main-process code that reads Codex events. No UI, wire-format, mobile, SSH or platform change; a helper's record keeps the same shape and states.

#23801 already dropped the error branch and the deleted import. Kept this PR's
thread-closed frame; the tests that still expected an error to end a child's
turn are what remains.
@brennanb2025 brennanb2025 changed the title fix(native-chat): a Codex child's turn ends on its own turn/completed (fixes main typecheck) test(native-chat): main's Codex child tests expect a turn to end on its own turn/completed Sep 29, 2026
Resolves the conflict with #23808 in favour of this PR's model: a Codex
child's turn ends on its own turn/completed, and the one ending without it
is a closed thread. #23808's child error arm and readCodexErrorWillRetry
(no other consumer) are dropped; its pr-workflow-parallelism.test.mjs fix
is kept.

@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.

Important

Merging this branch silently reverts #23808 ("keep a child turn settling on an error Codex will not retry"), which landed on main at 06:42 UTC after this PR's last review. The merge of #23810 resolved the conflict in the branch's favour, so the head has no child error path while origin/main (b8a7dafa) does. The PR description's "that matches main's code" is now stale — please reconcile explicitly.

Reviewed changes

The only incremental change since the prior pullfrog review is main being merged into the branch (1dbb4c8 #23801, 215ce16 #23802, 15e7a42 #23810). The branch's own tracked files are byte-identical to the previously reviewed 215ce16, so the substance of this delta is the last merge's conflict resolution.

  • main merged, dropping #23808's child error path — the head lacks readCodexErrorWillRetry and the "a non-retryable error ends a child's turn" branch that current main has, so the merge reverted a fix merged hours earlier rather than reconciling with it.

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

Comment on lines +40 to +43
* The one way Codex ends a child's turn without `turn/completed`: a closed thread ran its last
* turn and Codex never said how it went. A turn-ending `error` is not one — Codex follows it with
* a failed `turn/completed` for the same turn, which is that turn's end and carries the duration
* and receipt time the error does not.

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.

⚠️ This JSDoc — and the removed error branch it describes — now contradicts main, not just the pre-#23808 code. #23808 restored exactly this path, and the merge of #23810 (15e7a42) resolved the conflict by keeping the branch's removal, so head has no child error path while origin/main does. Merging this PR would revert #23808, whose stated risk is a child that latches on working for the session when no turn/completed follows the error; please reconcile deliberately (keep the path, or record why it is being withdrawn and on what evidence).

Technical details
# Merge reverts #23808's child error-settling path

## Affected sites
- `src/main/codex/codex-background-task-frames.ts:40-43` — JSDoc asserts a turn-ending `error` is not an end; `main`'s `readCodexChildTurnEnding` returns `{ kind: 'turn-ended', threadId, turnId, state: 'failed' }` for a non-retryable `error`.
- `src/main/codex/codex-structured-thread-facts.ts` — `readCodexErrorWillRetry` exists on `origin/main` (`b8a7dafa`) and was added back by #23808; this branch deletes it.
- `src/main/codex/codex-child-work-evidence.test.ts:173-183` — the error step asserts `live`; `main`'s version asserts the error settles `failed`.

## Evidence
- `git show 215ce16:src/main/codex/codex-background-task-frames.ts` and `git show 15e7a42:...` are identical; `git diff 215ce16 15e7a42 -- src/main/codex/` is empty.
- `git show f8f656ca19:...` and `git show b8a7dafa:...` (current `main`) both contain the error branch.
- `git diff f8f656ca19 15e7a42 -- src/main/codex/` removes it (6 files, +49/-75).

## Required outcome
- `main`'s #23808 must not be dropped without an explicit decision: either retain the error path, or state in the PR why it is being reverted, with evidence that resolves the disagreement with #23808's no-guarantee argument.

@brennanb2025
brennanb2025 merged commit bb91e39 into main Sep 29, 2026
33 checks passed
brennanb2025 added a commit that referenced this pull request Sep 29, 2026
…3745, #23783, #22971, #22565, #22846, #23724, #23721, #23693, #23810) into #23059

#23671 (group chat rows by the turn that produced them) is ported onto
#23059's turn-scope model, so there is one grouping mechanism, not two:

- A host that states each row's turn scope groups by that scope, as
  before. #23671's journal-order derivation (rows between one turn record
  and the next belong to it) moves from the timing selector into
  src/shared/native-chat-turn-grouping.ts and runs only for a host that
  states no scope, replacing the positional fallback there.
- #23671's anchor rules live in structuredAgentTurnAnchors, which
  membership, settled timing and the live clock all read: a turn whose
  opener key nothing resolves yet takes the send still in flight ahead of
  its record (Codex before its echo), else its own record.
- The live turn is one key again. #23671 made row liveness follow the
  owning turn, so the bar key and the liveness key are always the same;
  nativeChatTurnMembership now returns only liveTurnKey (barTurnKey,
  activeTurnOpenedBy and turnKeysByItemId are gone).
- A turn with no user bubble draws its bar above its first row
  (nativeChatTurnBarRows, desktop and mobile), and a turn's diff rollup
  goes under its last row.
- The Codex full-history restore writes each record ahead of its items;
  that also fixes the derived turn scope on #23059's journal.

#23026 (Stop from the moment a message is sent) is ported onto #23059's
command turns: the cancel paths ask the translator for a command's
provider turn instead of the removed compaction tracker, and a Stop that
names no turn still ends a running /compact through its child.
performCancel moved to structured-agent-session-turns-cancel.ts to keep
the turns module under the line limit.

#22090's awaitingInput replaces showTurnStatus/showLiveTurnActivity; the
waiting-behind-/compact rows are kept.
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