Skip to content

feat(native-chat): register Codex default-mode helpers as subagents - #22619

Open
brennanb2025 wants to merge 9 commits into
brennanb2025/c4-codex-producerfrom
brennanb2025/c8-codex-default-helpers
Open

brennanb2025 wants to merge 9 commits into
brennanb2025/c4-codex-producerfrom
brennanb2025/c8-codex-default-helpers

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​613 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​613
Prod 21 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​552 $\color{#cf222e}{\Huge{\mathbf{−}}}$​189 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​363

ELI5

When a Codex chat runs in its default multi-agent mode and starts a helper agent, Orca did not know the helper existed. The helper got no row in the transcript and no entry in the status dock, and the dock showed the helper's sleep 45 command as if the main agent were running it. Now Orca picks the helper up from the call that started it. The helper gets the same subagent row, dock entry and host record as any other Codex subagent. The calls the main agent makes to it (start it, wait for it, close it) show as readable tool rows that name the helper, not as codex · item:collabAgentToolCall.

Stacked on #22553 (the Codex child-record producer; this branch was rebased onto its head 8e22ed0ad2). The live test that found this gap is #22532. #22614 (the strip and sidebar switch) depends on this one to show anything for Codex in default mode.

What Changed

The mechanism

Codex 0.155 has two multi-agent modes, and the model decides which one runs. In a live run, GPT-5.5 used the default mode and GPT-5.6-Terra used v2.

  • v2 announces a helper with subAgentActivity items.
  • Default mode sends no subAgentActivity at all. A helper appears only as the parent's collabAgentToolCall item: spawnAgent starts with receiverThreadIds: [], and the finished call names the helper's thread.

Before this PR, the journal roster and the background-task tracker registered children only from subAgentActivity. They share one CodexSubagentExecutions, so in default mode nothing registered: no child in the executions, no strip row, no host record and no roster row.

One membership model, two wire sources

  • readCodexSubagentAnnouncement (codex-subagent-activity.ts) returns the child an item announces, from either shape. That is a subAgentActivity (never the tree root), or the receiver an ended spawnAgent call names, whatever the call's status. Both are keyed by the child's thread id.
  • Why a spawn registers on any status that names a receiver: spawnAgent starts with receiverThreadIds: [] (in the schema and in the live journal), so the helper's thread id does not exist until the call ends. Codex reports the call as failed when the helper it created errored at birth, yet still names the thread it created, and that thread can run a turn. (Codex's collab_tool_call_status maps AgentStatus::Errored to Failed even with a receiver, and the spawn handler fills receiverThreadIds from the created thread.) Gating on completed would leave that helper nowhere and put its shell back in the dock as the session's own command, which is the bug this PR fixes. A spawn that created nothing names no receiver and registers nothing.
  • closeAgent keeps its completed gate: a failed close means the helper was not closed, so it ends nothing.
  • The journal roster (handleItem) and the tracker's frame reader (readCodexBackgroundTaskFrame) both register through it into the same executions. There is no second roster and no second state source.
  • Dedupe: a session that sends both shapes registers one child per thread id (the executions key on it). The first label stands. In v2 the activity item is sent before the spawn call finishes, so the task name wins there.
  • Execution state still comes only from the child's own turn frames, as in feat(native-chat): Codex sessions write their subagents into the host status store #22553. agentsStates is the caller's snapshot, not a state source, so it is never read as state. Its message is used only as the call row's output.
  • Label: default mode carries no task path or nickname on the item. The prompt is the only text that tells two helpers apart, so the label is the prompt's head on one line, at most 80 characters. Codex's internal spawn event has a nickname, but the app-server item drops it.
  • Lifecycle: a finished closeAgent ends the helper's running turn as stopped, which maps to cancelled. It goes through the same endTurn path feat(native-chat): Codex sessions write their subagents into the host status store #22553 uses for a fatal error or thread/closed, and the first ending a turn gets stands. Closing a helper that already finished changes nothing. There are no timers.
    • Why closeAgent counts as an ending: in every observed case the child's own frames end it. But in default mode a closed helper may send no further frames at all, and a caller deliberately shutting a helper down is a cancellation, not a failure. If Codex also sends thread/closed for the closed helper, whichever ending arrives first stands, so the two cannot conflict.
  • Evidence routing fix: CodexChildWorkEvidence.childThread now keys a frame on the frame's own child thread, not the event's. Before, a closeAgent (sent on the parent's thread) would have settled the strip but never queued the record's ending.

v2 sessions change too. Their collab calls (for example wait, which in v2 has empty receivers) also switch from raw status rows to typed rows. Helper registration in v2 is unchanged: it still comes from subAgentActivity.

The dock

This PR adds no new labeling path. Once the helper is registered, the existing tracker path applies:

  • While the helper works, the dock lists it as 1 agent — working, labelled with the helper, and its command is covered by that row.
  • A persistent command the helper leaves running after its turn ends shows as <helper> — <command>. The unit test covers this; it was not seen live.

The transcript

  • Each collabAgentToolCall now renders as a tool-call row (codex-collab-agent-item-translation.ts), named by the tool the model called: spawn_agent, send_input, resume_agent, wait_agent, close_agent, and so on.
    • The helper's name is in description, the key the row label reads, so the row names the helper the way the roster does.
    • The row also carries the prompt, model, effort and receiver ids for the expanded view.
    • What the helper said back (agentsStates[].message) is the row's output.
  • The row keeps its helper names when the turn is interrupted with the call still open: the active item keeps the resolver it was translated with.
  • A spawn_agent row counts as running an agent in the run sentence and gets the bot glyph. Wait, message and close calls run none, so they stay generic tools.
  • The roster row (Kicked off 1 subagent) now appears in default mode too, beside the spawn call's own row, as it does for other lanes.

This PR closes a gap in #22553: the transcript row now settles with the strip and the record

#22553 said a child's strip row and host record settle together when its turn ends by a fatal error or thread/closed. That held for those two, but not for the transcript's subagent row. CodexSubagentRoster rewrote that row only on a child's turn/started and turn/completed, while the other endings reach the executions through the tracker. So after a fatal error, a closed thread or a closeAgent, the strip dropped the child and the record settled, but the row read working until the session ended.

The fix does not add a new list of frames. CodexSubagentExecutions (the one owner of child turn state) now reports when a child's current execution changes (onExecutionChanged), and the roster rewrites the group row from that, whichever frame caused it. handleTurn still recomputes the row so it can return the write's admission; the second write is a no-op when nothing changed. If a write is refused while following, the group's lastSerialized is cleared, so the next write for that group retries it.

Which mode runs is the model's choice

On Codex 0.155, the model decides which multi-agent mode runs. In the live runs GPT-5.5 used the default mode (collab calls only, no subAgentActivity), and GPT-5.6-Terra used v2 (subAgentActivity for the spawn). Both announcement shapes have to register, and a session can switch shapes when the user switches models.

A renderer fix this exposed

A folded run paired tool results to calls by position. So once one call finished with no output (spawn_agent), every later output drew under the call before its own: the helper's reply under spawn_agent, and the shell's output under wait_agent. This was visible live.

A structured row carries its call and its output together, so the projected result now names its call id (NativeChatToolResultBlock.callId), and pairNativeChatToolResults pairs by it. A result that names no call is still paired by position.

callId is produced wherever the journal is projected:

  • in the renderer, for the chat transcript;
  • on the host, for orchestration worker transcripts (worker-read RPC, orchestration-structured-worker-lifecycle.ts);
  • on the host, for released-worker archives, which persist the projected messages (structured-worker-journal-archive.ts, read back by worker-output-archive.ts).

It is optional everywhere and copies the journal row's own callId. Mixed versions and old archives fall back to the previous positional pairing, and old readers ignore the field.

This also changes pairing for Claude folded runs: Claude tool rows already carry call ids (claude-structured-item-translation.ts), and the same misattribution existed there. On main it could also happen for Codex whenever a call finished with no output (a web_search with no results, for example). The non-structured transcript path sets no callId, so legacy native chat is unchanged.

Why

In Codex's default mode, helpers were invisible to every surface that reads the executions, and the dock blamed the main agent for a helper's command. Registering the helper at the executions (the one owner of Codex child identity) fixes the strip, the host records and the transcript roster together. The records then feed the next PR's strip and sidebar.

Alternatives considered:

  • A separate roster for collab-announced helpers. Rejected: two membership models for one child, and a v2 session would produce two children.
  • Reading agentsStates as the helper's state. Rejected: it is the caller's last-known snapshot, only refreshed when the caller makes a call. The helper's own turn frames already give its state (feat(native-chat): Codex sessions write their subagents into the host status store #22553).
  • Suppressing the raw collab rows as status chrome. Rejected: in default mode these calls are the only transcript trace of the fan-out.

Linked Issue

None — part of the structured chat status/orchestration program. Found by the live QA on #22532. Base: #22553.

Visual Proof

Codex codex-cli 0.155.0-alpha.9.2, GPT-5.5 (default multi-agent mode), dev builds with ORCA_BACKGROUND_LAUNCH=1, CDP screenshots of the hidden window. Prompt: "Spawn exactly one subagent. Tell it to run the shell command sleep 45; echo CHILD_DONE and then reply with the single word CHILD_REPLY…"

BEFORE (#22553 head f098d8db9e), while the helper sleeps: the transcript shows two raw codex item:collabAgentToolCall rows, there is no subagent row, and the dock reads 1 shell command — working. The dock is naming the helper's command as the session's own.

before

AFTER, same moment: a Kicked off 1 subagent · working roster row, and the dock reads 1 agent — working.

after-running

AFTER, dock expanded: the agent row names the helper by its prompt.

after-dock

AFTER, settled turn expanded: spawn_agent and wait_agent rows name the helper. wait_agent owns CHILD_REPLY and the shell owns CHILD_DONE (pairing fix), and the roster row reads Ran 1 subagent · completed · 52s · 66.4k tokens.

after-settled

The AFTER journal holds no status row with provider frame item:collabAgentToolCall. The BEFORE journal holds four (revisions of three calls). Both journals are preserved as ~/orca-qa/c8/journals/{before,after}-gpt55.db.

Testing

  • pnpm tc:node, pnpm tc:web and pnpm tc:cli all exit 0. pnpm run check:code-quality:changed shows 0 findings (React Doctor included). pnpm exec oxlint on every changed file exits 0, and pnpm run audit:anti-slop exits 0.
  • Suites run with env -u ORCA_STRUCTURED_SESSION -u ORCA_AGENT_SESSION_ID at the final head: src/main/codex, src/main/native-chat, src/shared and src/renderer/src/components/native-chat together: 1355 files, 14,531 passed, 0 failed. Full pnpm exec oxlint: exit 0 (2 existing warnings in mobile test files, none in this PR's files). src/main/native-chat/agent-session-wire also passes. In an earlier run under load, one lease-renewer temp-dir ENOTEMPTY failure (a suite this PR does not touch) passed 7/7 alone.
  • 19 new tests:
    • codex-default-mode-helpers.test.ts (16), through the real CodexStructuredSessionAdapter. The spawn and wait items are verbatim from the fix(native-chat): record which Codex agent produced each journal row #22532 live journal; only the sender is remapped to the fixture thread. It covers:
      • registration in the strip, the records and the roster row;
      • the dock label for a command left running;
      • collab calls as named tool rows and never raw rows;
      • names kept through an interrupted turn;
      • either arrival order;
      • closeAgent on a running helper giving cancelled, and on a finished one changing nothing;
      • one child for a session announcing both ways;
      • a failed spawn that created its thread registers the helper, and the helper's command no longer shows as the session's own; a spawn that created no thread registers nothing; a failed closeAgent leaves the helper running (3);
      • the subagent-group row settling with the strip and the record on a fatal error, on thread/closed, and on closeAgent (3). At the feat(native-chat): Codex sessions write their subagents into the host status store #22553 head all three fail; the row stays working;
      • a restored thread (2): a replayed wait_agent row names its helper as the live row did, and a name learned from history leaves no strip entry, no record evidence and no roster row.
    • The run sentence and glyph for spawn_agent (1).
    • Pairing by call id past an output-less call (1).
    • The projected result carrying its call id (1).
  • Against base: with the production files reverted to the feat(native-chat): Codex sessions write their subagents into the host status store #22553 head this is stacked on (8e22ed0ad2 after the stack rebase), 14 of the 17 new Codex and glyph tests fail, including all three roster-row tests, the restored-row test and the failed-spawn test. The three that pass are guards by design: "keeps one child for a session that announces the helper both ways", "registers nothing for a spawn that created no thread" and "claims no running helper for a name it learned from history".
  • Ablations: all 19 arms go red. Each arm is an exactly-once edit, run at the final head and restored from HEAD:
Deleted or reverted Red
An ended spawn call counts as an announcement 11/16
The roster registers from a spawn call 1: registers the helper whichever order its first turn and its spawn arrive in
The tracker registers from a spawn call 1: same test
closeAgent ends the helper's running turn 1: ends a running helper its caller closed as cancelled…
Evidence keyed on the frame's child thread (reverted to the event's) 1: same test
The collab tool row 5
A settled row keeps its helper names 1: keeps naming the helper when the turn ends with its wait still open
The label from the spawn prompt 8
The roster leaves a spawn call to its own row (substituted: it claims it) 1: the either-order test
Helper names reach the item rows 4
spawn_agent counts as running an agent 1
A projected result names its call 1
Pairing honors a named call 1
The roster follows execution changes (listener body deleted) 3: settles the row with the strip and the record on a fatal error; …on its thread closing; settles the row when its caller closes the helper
The executions announce a changed execution (deleted in observeTurn) 3: the same three
Restore replay registers announced helpers 1: names the helper on a replayed call row, as the live row did
Restore registration starts no execution (substituted: it starts a running turn) 1: claims no running helper for a name it learned from history
A spawn registers whatever its status (substituted: only when completed) 1: registers a helper whose spawn call failed but created its thread…
A close ends a helper only when it completed (status gate deleted) 1: leaves a helper running when its caller failed to close it

The roster and the tracker share one executions instance. When the helper's first turn arrives after its spawn, either registration alone reaches every surface. Each one is needed only when the turn arrives first, and that is the one test each arm reddens.

Review

What I did NOT verify:

  • closeAgent live. The model never closed its helper in these runs. It is covered only by the unit tests, which were built from the schema and the provider's own item mapping.
  • A v2 session on this branch. GPT-5.6-Terra ran v2 on the BEFORE build only. v2 sends wait with receiverThreadIds: [] and no spawn call, so on this branch its wait row reads a bare wait_agent (no helper to name). That comes from reading the captured journal shape, not a live AFTER run.
  • Restore, and what a user sees after a restart. History replay does not run the live item router, so before this fix a restored wait_agent, close_agent or send_input row showed the raw thread UUID as its label. Replay now registers each announced helper for its name and membership only (restoreCodexHistoryItem). register starts no execution, so the helper gets no strip entry, record or roster row until a live turn of its own. The spawn row keeps its prompt label either way. The roster's group rows are still not re-seeded on restore (the known limit in codex-subagent-roster.ts, from feat(native-chat): Codex sessions write their subagents into the host status store #22553), so there are no live Codex child records to render after a restart until new frames arrive. Restore is unit-tested only, not run live.
  • A refused roster write while following the executions is reported to nobody until the next write of that group: a later execution change, a token-usage frame, or at the latest the session-end sweep, whose retry exists for exactly this case. For the roster's own turn events the admission is still returned.
  • A failed spawn that created its thread now registers (above). Live, only a completed spawn was ever seen; the failed shape is from Codex's source and a unit test.
  • A spawn dropped by compaction: if context compaction removed a spawn call from the replayed history, restore never learns that helper's name, so its restored call rows fall back to the thread-id label.
  • Richer names are available upstream. Codex core records each receiver's nickname and role (receiver_agents) on the collab call, but the app-server item does not forward them yet (absent from the 0.155 schema). Once it does, readCodexSubagentAnnouncement is the one place that should adopt them.
  • The dock label on a persistent command left running after the helper's turn. Unit-tested only; in the live runs the command finished inside the helper's turn.
  • Platforms. No SSH, WSL, Linux or Windows runs. The host logic is platform-neutral and runs in the execution host's process (the same path as feat(native-chat): Codex sessions write their subagents into the host status store #22553). Folder workspace: the live test used one. Mobile renders the same shared tool-row vocabulary, but its suites were not run.
  • Capacity. GPT-5.5 was at capacity for 3 of 6 attempts. Those runs are excluded, and no verdict rests on them.

Notes

  • Wire. No new RPC field or opcode. What the host publishes for a default-mode session changes: raw status rows become tool-call rows, and a roster row appears. Both are existing body kinds that older clients already render. NativeChatToolResultBlock.callId crosses the worker-read RPC and is persisted in released-worker archives (see "A renderer fix this exposed"). It is optional, so mixed versions degrade to positional pairing.
  • Tool names are the names the model calls. The collab tool list comes from the generated app-server schema (codex app-server generate-json-schema, 0.155): spawnAgent, sendInput, resumeAgent, wait, closeAgent, sendMessage, followupTask, interruptAgent, listAgents.
  • For the next PR: in default mode, a Codex helper's description is its prompt head, not a task name.

Merge order

@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

One gap worth addressing before merge: a helper the caller closes updates the strip and the host record, but the durable subagent roster row keeps reporting working.

Reviewed changes

  • Default-mode helper membership — readCodexSubagentAnnouncement now treats a finished spawnAgent call as a second way to announce a child, and both the roster and the background-task tracker register through it into one shared CodexSubagentExecutions.
  • Collab tool rows — a new codex-collab-agent-item-translation renders each collabAgentToolCall as a named tool-call row (spawn_agent, wait_agent, …) instead of a raw codex · item:collabAgentToolCall status row, and forwards the roster's label so an interrupted turn keeps its helper names.
  • Close lifecycle — a finished closeAgent becomes a turn-ended/stopped frame; CodexChildWorkEvidence now keys on the frame's child thread rather than the event's.
  • Output pairing — the projection attaches its journal row's callId to the tool-result block, and pairNativeChatToolResults resolves a named result to its call before falling back to positional pairing.
  • Run vocabulary — spawn_agent counts as running an agent and takes the bot glyph.

ℹ️ Nitpicks

  • The roster row appends a collision ordinal when two helpers share a label (claimLabel), but the new tool row and the strip both name a helper by the raw executions.label. Two default-mode helpers whose prompt heads coincide will therefore read X/X 2 on the roster but X/X on their spawn_agent/wait_agent rows, so the "two rows read as the same child" invariant in codex-collab-agent-item-translation.ts does not hold for colliding labels. The agents array still disambiguates them.
  • The diff branch of itemBlocks still emits an unnamed tool-result, so an edit patch can still paint under an earlier output-less call (for example a spawn_agent). AgentJournalDiffItem carries no callId, so closing this is a schema addition rather than a projection tweak — worth a follow-up, not this PR.

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

Comment thread src/main/codex/codex-background-task-frames.ts

@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 — delta since the prior pullfrog review (80b2032040 → d09c8b2286)

  • The roster row now follows the executions — CodexSubagentExecutions fires a new onExecutionChanged listener from observeTurn and settleSession, and CodexSubagentRoster subscribes in its constructor and rewrites the group row from the changed execution. A fatal error, a thread/closed and a closeAgent therefore settle the durable subagent row, not only the strip and the host record. handleTurn still returns the write's admission via an idempotent re-derive, and dispose() unsubscribes.
  • Three tests added — the roster row settles with the strip and the record on a fatal error (failed), on thread/closed (unverifiable), and on closeAgent (stopped), each asserting the last subagent-group revision.

This closes the closeAgent gap flagged in the prior review. The listener propagation reads correct: it fires only when an execution is actually assigned, duplicate writes collapse on lastSerialized, and it cannot touch the primary thread because that thread is never registered as a child.

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

@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 — delta since the prior pullfrog review (d09c8b2286 → 6214d76206)

  • Restored call rows name their helper — replayed-item handling moved into restoreCodexHistoryItem, which registers each announced helper in CodexSubagentExecutions (register starts no execution, so no strip entry, record or roster row claims it runs). A restored wait_agent/close_agent/send_input row now names its helper instead of the raw thread id.
  • Roster exposes its executions — CodexSubagentRoster.executions is now public and helperLabel is gone; the collab row's name callback reads subagents.executions.label directly.
  • Render contract named — handleItem's claim flag is renamed renderedByRoster, documenting that a subAgentActivity renders as the roster row alone while a spawn call keeps its own row.
  • Comment clarification — the collab tool-name map notes it maps the wire's camelCase to the model-facing snake_case, not a spelling fix.
  • Tests — a restored thread (2) through the real translator; the naming test reddens without the registration (verified by ablation).

The registration runs after primaryThreadId is set (codex-structured-session-acquire.ts:197), so it cannot register the primary thread as a child; register's idempotence and the MAX_CHILDREN cap bound a restored thread's historical helpers. Both existing tests and the new file pass at this head.

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

@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 — delta since the prior pullfrog review (bd540b299f → c915a9cf3e)

  • A failed spawn now registers its helper — codexCollabSpawnedThread (codex-collab-agent-tool-call.ts:65) drops the status === 'completed' gate, so a spawnAgent call that ended failed but named a receiver registers that helper in the one shared CodexSubagentExecutions. Codex reports failed for a helper that errored at birth while still naming the thread it created, and that thread can run a turn.
  • Three tests — a failed spawn with a receiver registers the helper (strip, records and roster row); a failed spawn with no receiver registers nothing; a failed closeAgent leaves the helper running (the close gate is preserved). Each reads the real adapter path.

The removal is safe against the shapes it must not touch. An in-progress spawnAgent names no receiver, and the first test still asserts that the inProgress/empty-receiver call registers nothing. Registration is idempotent and starts no execution, so a failed spawn whose thread never runs a turn yields no strip entry, record or roster row — the child stays registered only for its name, bounded by MAX_CHILDREN. The closeAgent gate is unchanged and now guarded by its own test.

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

@brennanb2025
brennanb2025 force-pushed the brennanb2025/c4-codex-producer branch from 8e22ed0 to 08ca92b Compare September 24, 2026 21:09
@brennanb2025
brennanb2025 force-pushed the brennanb2025/c8-codex-default-helpers branch from c915a9c to d0d1de8 Compare September 24, 2026 21:24

@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 — delta since the prior pullfrog review (c915a9c → d0d1de8)

  • Token totals moved to their own module — the roster's per-thread token map became CodexThreadTokenTotals in codex-thread-token-totals.ts, which owns the newest-report-replaces rule and the recency-ordered LRU cap (MAX_CODEX_TOKEN_USAGE_THREADS). The roster now calls record/get/clear and still selects its children's totals at write time, so behavior is unchanged.
  • Test — codex-thread-token-totals.test.ts covers newest-report-replaces and that a thread which keeps reporting outlives quieter ones under eviction.

Verified at d0d1de85: pnpm tc:node, pnpm exec oxlint on the changed files, and pnpm run check:max-lines-ratchet all exit 0; codex-thread-token-totals.test.ts and codex-subagent-roster.test.ts pass (37). The branch was also rebased onto c4-codex-producer 08ca92b2; reading the full diff against that base confirms the producer-linkage plumbing is content-preserving for this PR.

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

@brennanb2025
brennanb2025 force-pushed the brennanb2025/c4-codex-producer branch from 08ca92b to 358d1a9 Compare September 24, 2026 23:36
@brennanb2025
brennanb2025 force-pushed the brennanb2025/c8-codex-default-helpers branch from d0d1de8 to c77bad1 Compare September 24, 2026 23:48
brennanb2025 added a commit that referenced this pull request Sep 25, 2026
…wn 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, scoping its status to the session's own asks put it past
the size limit. The module is byte-identical to the one #22619 extracts for the
same reason, so the two branches carry one change.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/c4-codex-producer branch from 358d1a9 to 1db34d1 Compare September 25, 2026 04:57
@brennanb2025
brennanb2025 force-pushed the brennanb2025/c8-codex-default-helpers branch from c77bad1 to de9ea37 Compare September 25, 2026 05:02
brennanb2025 added a commit that referenced this pull request Sep 25, 2026
…wn 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, scoping its status to the session's own asks put it past
the size limit. The module is byte-identical to the one #22619 extracts for the
same reason, so the two branches carry one change.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/c4-codex-producer branch from 1db34d1 to 36242df Compare September 25, 2026 05:53
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.
…ds its helper

Also pin that the roster row appears when the helper's first turn arrives
before the spawn call finishes.
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.
…ry 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.
… 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.
… 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.
…s 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.
…wn 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.
@brennanb2025
brennanb2025 force-pushed the brennanb2025/c8-codex-default-helpers branch from de9ea37 to 748ef6e Compare September 25, 2026 05:56
brennanb2025 added a commit that referenced this pull request Sep 25, 2026
…wn 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, scoping its status to the session's own asks put it past
the size limit. The module is byte-identical to the one #22619 extracts for the
same reason, so the two branches carry one change.

This branch has not been deployed

No deployments
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