Subagent completion watcher silently drops parent notification when child disappears before subscription

Resolved 💬 1 comment Opened Mar 2, 2026 by tristanang Closed Mar 2, 2026

What version of Codex CLI is running?

0.106.0

What subscription do you have?

ChatGPT Plus

Which model were you using?

gpt-5.3-codex

What platform is your computer?

Darwin 24.6.0 arm64 arm

What terminal emulator and version are you using (if applicable)?

iTerm2

What issue are you seeing?

No error message is produced. The bug is silent: when a sub-agent completes before the completion watcher can subscribe to its status, the watcher exits without notifying the parent thread. The parent thread appears to hang or lose track of the child agent with no visible error, log output, or prompt.

What steps can reproduce the bug?

A parent thread spawns a subagent that completes (or errors) almost instantly. The race is in AgentControl: send_input() is awaited first, and if the child dies during that call, it triggers remove_thread() to clean up the child. By the time maybe_start_completion_watcher() runs on the next line and the spawned tokio task calls subscribe_status(), the child thread is already gone from the thread manager, so subscribe_status() returns Err. The old code handled this with Err(_) => return, silently dropping the notification to the parent.

The race window is between these two consecutive calls in start_agent_thread (codex-rs/core/src/agent/control.rs):

self.send_input(new_thread.thread_id, items).await?; // can remove child on InternalAgentDied
self.maybe_start_completion_watcher(new_thread.thread_id, ..); // child already gone

Inside maybe_start_completion_watcher, the spawned tokio task does:

let mut status_rx = match control.subscribe_status(child_thread_id).await {
Ok(rx) => rx,
Err(_) => return, // silently exits — parent never notified
};

To reproduce: use Codex with sub-agents enabled and give a child agent a task that fails or completes near-instantly (e.g., a malformed request or a trivial no-op). Under load or unlucky scheduling, the child finishes and gets cleaned up before the watcher task runs. The parent thread then hangs waiting for a completion notification that will never arrive. There is no error message or log output — the failure is silent.

To artificially reproduce on main, add this test to codex-rs/core/src/agent/control.rs inside mod tests and run cargo test -p codex-core completion_watcher_missing_child. It simulates the race by calling maybe_start_completion_watcher with a ThreadId that was never registered (equivalent to a child that already vanished). On main, the assertion times out because the parent is never notified.

#[tokio::test]
async fn completion_watcher_missing_child() {
let harness = AgentControlHarness::new().await;
let (parent_thread_id, parent_thread) = harness.start_thread().await;
let child_thread_id = ThreadId::new(); // never registered — simulates vanished child

harness.control.maybe_start_completion_watcher(
child_thread_id,
Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn {
parent_thread_id,
depth: 1,
agent_nickname: None,
agent_role: Some("explorer".to_string()),
})),
);

// This times out on main — parent never receives the notification.
assert_eq!(wait_for_subagent_notification(&parent_thread).await, true);
}

What is the expected behavior?

When a subagent completes or disappears before the completion watcher can subscribe to its status, the parent thread should still receive a subagent completion notification. The watcher should fall back to a direct status lookup (get_status) and deliver the result (e.g., "not_found") to the parent, rather than silently exiting.

Additional information

Fix with regression test available on branch: https://github.com/tristanang/codex/tree/fix/subagent-notification-watcher

The fix changes the Err(_) arm in maybe_start_completion_watcher from a silent return to a fallback get_status() call, so the parent always receives a notification. The branch also includes a regression test (completion_watcher_notifies_parent_when_child_is_missing) that validates the fix.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗