Summary
After BB recreates a Codex provider session, a root agent can resume an existing native subagent with followup_task, but BB projects the child's next turn as foreground/root work. If the visible root finishes first, that misclassified child occupies the foreground slot and BB rejects the user's next prompt as a competing turn.
The child should remain attached to the root's delegation, and the next user prompt should still go to the root.
Versions and environment
Human/UI reproduction
-
Open a BB thread using the Codex provider.
-
Ask the root to spawn a subagent named repro_child, wait for it to finish, and then finish the root turn.
-
Cause BB to recreate the provider session while preserving the same Codex rollout. Normally, leave the thread idle until the session is reaped, then return. Quitting/reopening or reconnecting can also exercise this boundary.
-
In the same root thread, send:
Use followup_task to resume /root/repro_child. Ask it to keep working for at
least 30 seconds, then finish your own response immediately without waiting.
-
As soon as the root response finishes, while the child is still running, send:
-
BB rejects the prompt as a competing turn. It should deliver the prompt to the root because the running child is delegated work.
Negative controls: this does not reproduce while the original translator remains live; replacing followup_task with send_message should not create another child turn.
Deterministic source-level reproduction
Add this test inside describe("codex subagent activity correlation", ...) in plugins/provider-codex/src/translator.test.ts. All identifiers are synthetic, and the helpers already exist in that block.
it("links a rawless resumed subagent when its child turn starts", () => {
const harness = createHarness();
expect(
harness.translate(
subAgentActivity({
id: "synthetic-followup-call",
kind: "interacted",
agentThreadId: "synthetic-agent-thread",
}),
),
).toEqual([]);
const events = harness.translate(childTurnStarted("synthetic-child-turn"));
expect(events).toContainEqual(
expect.objectContaining({
type: "item/started",
item: expect.objectContaining({
type: "delegation",
childRef: "synthetic-agent-thread",
}),
}),
);
expect(events).toContainEqual(
expect.objectContaining({
type: "turn/started",
parentToolCallId: harness.itemId("synthetic-followup-call"),
}),
);
});
Run:
pnpm --filter bb-plugin-provider-codex test -- translator.test.ts -t "links a rawless resumed subagent"
On unmodified main, the second assertion fails: the child turn is emitted without a delegation or parentToolCallId.
Expected vs actual
Actual: the resumed child is persisted without parent_tool_call_id, and the next user prompt can fail with:
Refusing to start a competing turn for thread "<synthetic-thread-id>" while another turn is active or starting
Expected: the resumed child turn carries its delegation's parentToolCallId. A later user prompt targets the root. send_message remains non-turn-producing, and genuine concurrent root turns remain rejected.
Evidence and root cause
The shared runtime correctly treats an unparented turn/started as foreground, so the competing-turn guard is behaving as designed. The provider projection is missing the parent link.
After translator recreation, the in-memory child map is empty. Current Codex app-server emits subAgentActivity(kind: "interacted") followed by the resumed child's turn/started, but does not forward the raw followup_task collaboration item on this path. The rollout contains the function call, but the app-server notification stream does not. BB currently discards an unknown child's interacted activity, so the following child turn is projected as root work.
An anonymized occurrence followed this sequence:
| Event |
Persisted result |
Root calls followup_task; child starts |
Child has no parent_tool_call_id and is stored as root work |
| Visible root completes |
Misclassified child still owns foreground state |
| User submits the next prompt |
Rejected as a competing turn |
| Child completes |
Foreground slot clears |
Scope
Candidate fix and validation: #2539
Suggested priority and effort (optional)
High priority: normal provider-session lifecycle can block new prompts in long-running Codex threads. The change is confined to Codex event correlation, regression tests, and the host-daemon protocol version; shared turn ownership and the competing-turn guard remain unchanged.
Checks
AGENT GENERATED
Summary
After BB recreates a Codex provider session, a root agent can resume an existing native subagent with
followup_task, but BB projects the child's next turn as foreground/root work. If the visible root finishes first, that misclassified child occupies the foreground slot and BB rejects the user's next prompt as a competing turn.The child should remain attached to the root's delegation, and the next user prompt should still go to the root.
Versions and environment
0.40.0desktop app; macOS26.2(25C56)provider-codex; Codex CLI0.149.0mainat31a190ddb5f0ffb86cb8dcd80482497663de2685Human/UI reproduction
Open a BB thread using the Codex provider.
Ask the root to spawn a subagent named
repro_child, wait for it to finish, and then finish the root turn.Cause BB to recreate the provider session while preserving the same Codex rollout. Normally, leave the thread idle until the session is reaped, then return. Quitting/reopening or reconnecting can also exercise this boundary.
In the same root thread, send:
As soon as the root response finishes, while the child is still running, send:
BB rejects the prompt as a competing turn. It should deliver the prompt to the root because the running child is delegated work.
Negative controls: this does not reproduce while the original translator remains live; replacing
followup_taskwithsend_messageshould not create another child turn.Deterministic source-level reproduction
Add this test inside
describe("codex subagent activity correlation", ...)inplugins/provider-codex/src/translator.test.ts. All identifiers are synthetic, and the helpers already exist in that block.Run:
On unmodified
main, the second assertion fails: the child turn is emitted without a delegation orparentToolCallId.Expected vs actual
Actual: the resumed child is persisted without
parent_tool_call_id, and the next user prompt can fail with:Expected: the resumed child turn carries its delegation's
parentToolCallId. A later user prompt targets the root.send_messageremains non-turn-producing, and genuine concurrent root turns remain rejected.Evidence and root cause
The shared runtime correctly treats an unparented
turn/startedas foreground, so the competing-turn guard is behaving as designed. The provider projection is missing the parent link.After translator recreation, the in-memory child map is empty. Current Codex app-server emits
subAgentActivity(kind: "interacted")followed by the resumed child'sturn/started, but does not forward the rawfollowup_taskcollaboration item on this path. The rollout contains the function call, but the app-server notification stream does not. BB currently discards an unknown child'sinteractedactivity, so the following child turn is projected as root work.An anonymized occurrence followed this sequence:
followup_task; child startsparent_tool_call_idand is stored as root workScope
send_message, which emits the sameinteractedactivity but starts no child turn.Candidate fix and validation: #2539
Suggested priority and effort (optional)
High priority: normal provider-session lifecycle can block new prompts in long-running Codex threads. The change is confined to Codex event correlation, regression tests, and the host-daemon protocol version; shared turn ownership and the competing-turn guard remain unchanged.
Checks
maincommit above.