Remove `awaiter`
Summary
I’m raising this as a strong product/architecture objection: the awaiter role is fundamentally unnecessary and should be removed.
awaiter does not add unique capability. It adds prompt complexity, token cost, latency, and another failure surface for behavior that is deterministic orchestration and should either be:
- done directly by the main agent with long timeouts, or
- done by runtime code.
Right now, awaiter is literally an LLM told to poll in a loop.
Why this is a problem
awaiter is conceptually mismatched to what LLMs are good at.
- LLMs are for reasoning and adaptation.
- Polling/waiting loops are deterministic control flow.
- The role prompt explicitly demands deterministic behavior from a probabilistic model.
- The role duplicates behavior the main agent can already perform with
exec_commandand background polling with long timeout. - The only practical difference is timeout policy and prompt nudging, not capability.
The one thing it seemed maybe useful for does not work
The one thing I thought awaiter might still justify was mid-run status reporting. In practice, this is not usable from the parent/orchestrator context.
Empirical test:
- Spawned
awaiterrunning:sleep 10 && echo __AWAITER_MIDPOINT__ && sleep 30 && echo __AWAITER_DONE__. - Sent mid-run status request after ten seconds via
send_inputwhile command was still running. - Child thread did produce a status message internally and resumed waiting (confirmed in rollout file).
- Parent context did not receive that live status message.
- Parent only saw
submission_id, thenwaittimeout(s), then final completion.
If status updates are not surfaced to the parent context, “return status and resume awaiting” is effectively misleading guidance. In other words: the parent agent cannot ever use the awaiter agent to check on the status or progress of the command.
Also: even that “status check” benefit (if it actually worked) is fundamentally not special.
The parent can already do this with existing tools by polling the same running terminal session using a small/low timeout (e.g. empty write_stdin with short yield_time_ms) to fetch current output/progress directly. That gives in-band status without introducing a dedicated waiter role.
The only possible advantage
There is one real potential advantage worth acknowledging:
- A parent can spawn a sub-agent, continue other work, and later receive an explicit completion notification when the child reaches terminal state.
- That can reduce “should I check on command status right now?” uncertainty while the parent is busy.
However, this does not justify awaiter as a role:
- This advantage is not communicated to the model as a reason to use
awaiter. Therefore it will still worry “should I check on command status right now?”, and it cannot even do this via the subagent (see above). - The notification arrives as an injected contextual user message (
<subagent_notification>...</subagent_notification>), not a structured tool output contract. - The mechanism is generic sub-agent behavior, not a unique awaiter capability.
So yes, notifications are useful, but they should be a deterministic runtime feature exposed directly to the main agent/tooling surface, not hidden behind a special LLM role.
Tool-call and token overhead
Direct path for long-running commands:
- One
exec_commandcall with a large timeout/yield can cover the wait in a single call.
awaiter path usually adds more overhead:
- Parent calls
spawn_agent. - Parent often calls
wait(possibly repeatedly). - Parent should call
close_agentafterward.
Nuance: it is not always 3 or more tool calls on the parent side because the parent can skip wait and rely on injected completion notification.
But even in that best case, parent still pays extra calls (spawn_agent + close_agent) and ultimately ingests / outputs more tokens compared to using a single exec_command call. This further defeats the whole purpose as the agent makes more tool calls than it would using a single exec_command.
Also, it can do the direct path in a single tool call via multi_tool_use.parallel. The subagent approach needs multiple tool calls as the agent has to retrieve the command ID returned by exec_command.
awaiter system prompt
This is the current prompt text:
You are an awaiter.
Your role is to await the completion of a specific command or task and report its status only when it is finished.
Behavior rules:
1. When given a command or task identifier, you must:
- Execute or await it using the appropriate tool
- Continue awaiting until the task reaches a terminal state.
2. You must NOT:
- Modify the task.
- Interpret or optimize the task.
- Perform unrelated actions.
- Stop awaiting unless explicitly instructed.
3. Awaiting behavior:
- If the task is still running, continue polling using tool calls.
- Use repeated tool calls if necessary.
- Do not hallucinate completion.
- Use long timeouts when awaiting for something. If you need multiple awaits, increase the timeouts/yield times exponentially.
4. If asked for status:
- Return the current known status.
- Immediately resume awaiting afterward.
5. Termination:
- Only exit awaiting when:
- The task completes successfully, OR
- The task fails, OR
- You receive an explicit stop instruction.
You must behave deterministically and conservatively.
The line “You must behave deterministically and conservatively” is the core red flag. If determinism matters, do not delegate this to an LLM role.
I want to be blunt: this is an astonishingly bad use of an LLM. Everything awaiter does can fundamentally be done without it.
Proposed fix
- Remove
awaiteras a built-in role. - Add an explicit completion-notification option to
exec_command(e.g.notify_on_completion: bool):
- If true and the command continues in background, runtime should enqueue a completion notification for the parent thread even if it never polls
write_stdin. - Include structured metadata (
session_id, final status, exit code; optional bounded summary).
- Raise the default
background_terminal_max_timeoutsubstantially (at least 1 hour). - Add explicit guidance to main-agent instructions:
- For long-running commands, use very large
yield_time_msand wait timeouts. - Avoid tight polling loops.
- For interim progress, poll the running session directly with low-timeout reads when needed. This one is more optional as the agent (
gpt-5.3-codex xhigh) already tacitly knows how to do this from my comprehensive testing with my local modifications removingawaiterand adopting this better system. - Use the new
exec_commandcompletion notification option when the agent should continue other work and be notified on completion.
Why this is better
- Fewer moving parts.
- Lower token and latency overhead.
- Same capability.
- Preserves the only useful part (async completion notification) as a deterministic runtime primitive.
- Cleaner mental model for users and maintainers.
- Better separation of concerns.
9 Comments
(I'm not a dev.) I'll be curious to see the official response. Here's my initial thoughts though. It could be that OpenAI saw enough times where they wanted to give further instructions about running tasks, due to cases where the model did something wrong. The system prompt you quoted allows a lot more control over how tasks are ran without polluting the parent agent's context.
awaiterdoes have fixed overhead for the parent agent to create/work with it, wait for it, and give it instructions. But, if those instructions were in the parent agent, given each time a task was ran, it would pollute its context heavily, I think more than the overhead of a subagent -- but maybe I'm wrong there.That said, you might be able to effectively disable it. (Not tested.) Roles appear that they can be overridden.
config.tomlunder[agents.<name>]can havedescriptionandconfig_file. The code appears to give the user's config preference over the built-in. If you set the description to something like "This agent does nothing; do not use it.", I think Codex CLI would give the model that description instead of the one quoted below, and it would be extremely unlikely to use it. You might need a dummy config file (seeawaiter.tomlreference above.) In itself, it would be unfortunate to pollute the parent agent's context with the don't use this command, but it might disable it.That said, I think it would be great for a user to be able to easily disable built-in roles without this "hack". I'm sure OpenAI will be adding a lot more roles in the future, and it would be great to give advanced users the ability to tweak this and manage their context a lot more.
For curiosity, the built-in description for
awaiteris (codex-rs/core/src/agent/role.rs:193):Use an
awaiteragent EVERY TIME you must run a command that might take some time.This includes, but not only:
When YOU wait for the
awaiteragent to be done, use the largest possible timeout.@darlingm I see why you think that because I thought that initially too.
awaiterdoes not avoid parent-context pollution.The parent already gets
awaiterguidance in its own tool schema/instructions. Thespawn_agenttool description includes built-in role descriptions (including awaiter), so thedescriptiontext is already in parent context. Yes, technically the developer prompt is not included. But the parents context is still polluted by convoluted and unneededawaiter-related guidance.Spawned sub-agents inherit the parent config/instructions, then add role-specific overrides. What instructions does this agent have that the parent agent does not tacitly have? This is a weak argument as nothing special is conveyed to this agent that the parent agent doesn't already know. The subagents system prompt is LITERALLY: "Endlessly poll the command until it finishes". Is that useful? No.
[agents.awaiter]is only a local workaround and does not disable the role.You can override its description/config, but not remove the built-in role itself. So this doesn’t provide a clean, explicit “disable built-in role” control.
The optimal design is this. Remove awaiter, add deterministic async completion signaling directly to
exec_command(e.g.notify_on_completion), and raise background timeout defaults.i can confirm that it does not add to more pleasant experience
I'd love to open a PR to fix this issue, as I've been fixing this exact issue and iterating and testing ever since this was commited to the repository (I run my own custom Codex with so many issues and nits fixed as this repository does not welcome pull requests anymore, even for past contributors like myself).
If a Codex developer would like to grant me permission, I'll open the PR to fix this issue.
I don't have an opinion on your proposed alternative. Just giving my initial thoughts of status quo vs
awaiter. Your proposed change isn't an area of Codex that I've look at to have an informed opinion.descriptiontext, but it keeps the longer system prompt out of it. My point was if they have determined that they need to add all the extra language when running a task, that it seems like there's a smaller context cost doing that in a subagent. Although, I now see the system prompt forawaiteris 235 tokens, and its description is 64. It felt like the system prompt would be a lot higher. In practice, I'm wondering if the 64 will grow with agent-related overhead close to or exceeding the 235.@darlingm I 100% agree we should be able to disable subagents. I have that feature implemented in my own local modifications of Codex and I have
explorerhard disabled as it's over-invoked and useless for me.I feel like Codex is different to Claude Code in that it's decently opionated, but for stuff like this, we need to be able to customize it as a default.
Sub-agent is an experimental feature. You actively decided to enable it. We are still trying to figure out the best default agents to use. We will take your feedback and iterate on this
For anyone who didn't notice, v0.107.0 says it temporarily removed awaiter.
This feature request hasn't received any upvotes in many months, so I'm going to close it.