Repository navigation
Feature: an agent-callable monitor tool that wakes Codex on background events (logs, files, builds, CI) without polling #29922
Description
What variant of Codex are you using?
CLI and the Desktop app. The capability lives in codex-core, so it lands once and works on every surface (CLI, app, IDE extension).
What feature would you like to see?
The gap
Codex is turn-driven, so between turns it sits idle and cannot react to anything happening in the background. When I want the agent to notice that a build finished, that a log just printed an error, that a file changed, or that a CI run flipped state, I have two options today and both are bad.
Polling is the expensive one. #13733 traced where the cost comes from: each background status check runs a full turn that re-sends the entire conversation history, so a long cargo build watched in a poll loop costs tokens proportional to history size times poll count while no real work happens. Blocking the turn is worse for anything that never exits on its own, like a file watcher.
What is missing is a way for the agent to start watching something, go fully idle, and be woken only when the thing it cares about actually emits an event.
The proposal
Add an agent-callable monitor tool. The model starts a watch on an ordinary shell command, and each line that command prints (stdout or stderr) is delivered back as a notification that wakes the idle session and begins a new turn. The watch keeps running in the background until the command exits or the model stops it.
monitor(action="start", command="fswatch ~/project/src", description="src changes")
-> "Started monitor mon_ab12: watching \"src changes\". Stop it with action=stop, id=mon_ab12."
monitor(action="list") -> active watches
monitor(action="stop", id="mon_ab12")
This is the shape Claude Code ships as its Monitor tool. The command is the watcher, the lines it prints are the wake events, and the model filters to what it cares about (for example tail -F app.log | grep --line-buffered ERROR; pipe-filtering a tool that writes to stderr needs 2>&1 first, though the watch wakes on stderr either way). While the watch is quiet the session makes no API calls and spends no tokens, the property #28144 asks for directly.
How this differs from #20312
#20312 asks for the same capability framed as a config-declared inbound event source: the user lists sources in config.toml, Codex keeps a durable resumable cursor over each, and their events wake the session. That is inbound plumbing the user sets up once and that persists. This proposal is a different mechanism: a within-session tool the model reaches for mid-task and drops when done. There is no config registry and no persisted cursor, by design, because the watch is ephemeral and torn down when the session ends.
So the two differ on more than an entry point:
| #20312 (config source) | this proposal (agent tool) | |
|---|---|---|
| Control | the user configures durable sources | the model starts ephemeral watches mid-task |
| State | a persisted, resumable cursor per source | none; the watch lives and dies with the session |
A maintainer could ship either without the other, and a config-declared source could sit on top of this tool. The implementations discussed on #20312 (an MCP-notification ingress path, a message-bus reference) are solving the durable-source problem; this is the in-session-capability half. I would rather fold the design in wherever it is most useful than fragment the discussion, but the agent-callable form is a distinct enough decision that it seemed worth its own thread.
Use cases
The ones a config-declared source cannot express, because the model decides them mid-task:
- Watch this file while I work on that one:
fswatch <path>orinotifywait -m <path>to react to an edit or a new file during a refactor, instead of re-reading on a timer. - Watch the build I just started: go idle, wake when it finishes or prints an error, without blocking other work (Request: monitor background services #2062, Background subprocesses/subagents do not wake the calling agent on completion #15723).
- Watch this log for the failure I expect:
tail -F <log> | grep --line-buffered <pattern>to wake on the first matching line. - Watch this CI run: a short poll loop as the watched command, so the agent is woken when the run flips state.
And the ones that overlap #20312's territory, for completeness:
- Queue and bus consumers: tail an events stream for multi-agent coordination.
- Long-running jobs: notice a training run that started crash-looping, or a deploy that flipped state.
Related issues
The same gap surfaces across the tracker from several angles. One monitor tool in core covers the common core of these:
- Add wait/wake support for goals without spending tokens #28144: wait/wake for goals without spending tokens. It reaches the same conclusion from the goal-runtime side (let a goal wait on an event without burning turns); this tool is one concrete event source such a goal could wake on.
- Background process polling wastes tokens: each write_stdin poll triggers full API turn with complete history #13733: background-process polling spends a full API turn, with the whole history, per check. The cost analysis above is its root cause.
- Feature request: native event-driven session wake primitive #20312: native event-driven session wake primitive (the config-declared framing, discussed above).
- Feature request: Claude Code-like /monitoring for background terminals #17737: a Claude-Code-like
/monitoringTUI for background terminals; a surface that could sit on top of a core primitive like this one. - Background subprocesses/subagents do not wake the calling agent on completion #15723: background subprocesses do not wake the calling agent on completion.
- Request: monitor background services #2062: monitor background services without blocking.
- Stream real-time output from long-running shell commands #4751: real-time output streaming from long-running commands; this builds on that streaming substrate.
Additional information
I built a working reference implementation and run it in both the CLI and the Desktop app. The notes below are from that build. Full diff (public fork), one commit on top of the v0.142.0 release tag it builds on: rust-v0.142.0...yaanfpv:codex:monitor-tool. It is about 1,100 lines across 19 files, no protocol change, behind a feature flag.
How it fits the architecture
It reuses two pieces Codex already has, which is why it stays small:
- Spawning reuses
unified_exec. The watcher is just a unified_exec process, so it inherits the existing sandbox, approval, environment, and shell handling with no new exec path. It is stored in the shared process manager and reaped at session shutdown like any other. - Waking reuses the session's own idle-start path. Each output line, batched over a short window, is delivered through the same mechanism the session already uses to begin a turn. No new event type, no protocol change.
A small per-session registry tracks active watches so list and stop work, and holds each delivery task as an abort-on-drop handle so stopping a watch (or ending the session) tears its loop down cleanly.
The tool the model sees
name: "monitor"
description: "Run a shell command as a long-lived background watcher. Each line the
command prints (stdout or stderr) is delivered to you as a notification, prefixed
with the label; lines emitted close together are batched. The watch ends when the
command exits. Use it to react to events without polling: `fswatch <path>` or
`inotifywait -m <path>` for file changes, `tail -F <log> | grep --line-buffered
<pattern>` for log signals, or a poll loop for remote state. action=start begins a
watch and returns its id; action=stop ends the watch with that id; action=list
shows active watches."
parameters:
action: "start" | "stop" | "list" (required)
command: the watcher command (required for action=start)
description: label prefixed to notifications (required for action=start)
id: a monitor id from a prior start (required for action=stop)
The wake (the one seam that matters)
Each batch of lines is delivered as a typed contextual fragment, injected into a running turn if one is active, or used to start a turn if the session is idle. That is the whole wake mechanism, and it is why no protocol change is needed:
// A MonitorNotification is a ContextualUserFragment: a user-role message
// wrapped in <monitor_notification> markers, registered alongside the other
// injected-context fragments. The markers keep watched-process output
// recognizable as injected context, never mistaken for the user's own input.
async fn deliver(session: &Weak<Session>, description: &str, body: String) {
let Some(session) = session.upgrade() else { return };
let items = vec![ContextualUserFragment::into(
MonitorNotification::new(description, body),
)];
// Inject into a running turn or wake an idle one; if the idle start loses
// a race or is refused, record the items rather than drop them, so even
// the terminal "watcher exited" notice is never silently lost.
if let Err(items) = session.inject_if_running(items).await
&& let Err(err) = session.try_start_turn_if_idle(items).await
{
session.inject_no_new_turn(err.into_input(), None).await;
}
}
On the idle path, try_start_turn_if_idle already refuses to start work when it should not (queued user input, Plan mode), so a watcher never starts a turn over the user. Injection into an already-running turn is currently unconditional, and that is the one piece I would want to refine in a PR: with several watches live, one watcher's output can land in a turn another woke. Scoping the injection, or holding output until the active turn ends, is a small follow-up; I am flagging it rather than hiding it.
Delivery, batching, back-pressure
The delivery loop subscribes to the process output stream (stdout and stderr, already combined upstream) as a second consumer, splits it into lines, coalesces lines that arrive close together into one notification, and bounds a runaway watcher. The spawn hands back whatever its initial yield already captured, and that seed is delivered first, so the very first lines (an inotifywait banner, a grep that already matches) are not lost in the window before this loop subscribes. Condensed:
const BATCH_WINDOW: Duration = Duration::from_millis(200);
const FLOOD_MAX_LINES: usize = 5000;
// broadcast receiver, capacity 64; a second consumer, the original stream untouched
extend_lines(seed); // deliver the pre-subscription output first
loop {
tokio::select! {
received = rx.recv() => {
let flooded = match received {
Ok(chunk) => extend_lines(chunk), // arms a 200ms flush; true at the ceiling
Err(Lagged(skipped)) => note_drop(skipped), // surfaced + counted, not swallowed
Err(Closed) => break,
};
if flooded { // ceiling enforced per line, mid-chunk
stop_for_flood().await; // flush, notify, kill, deregister
return;
}
}
() = wait_until(flush_at) => flush().await, // coalesced batch -> one wake
() = exit.cancelled() => closing_at = grace(), // start a short trailing-output grace
() = wait_until(closing_at) => break, // ... then exit, so a final chunk still lands
}
}
// drain remaining output, deregister from the registry, deliver "watcher exited"
The registry
pub(crate) struct MonitorManager { monitors: Mutex<HashMap<String, MonitorEntry>> }
// insert(id, process_id, description, command, task) | remove(id) -> process_id
// | deregister_self(id) (self-removal, no abort) | list() | abort_all()
It lives on the session's services. stop removes the entry (dropping its task aborts the loop) and terminates the underlying process; session shutdown calls abort_all and the process manager reaps the processes. When a watched command ends on its own, the delivery loop deregisters its own entry before it announces the exit, so list and stop never report a watcher that is already gone. The self-removal path defuses the entry's abort-on-drop, since that loop is the very task being removed and still has its final "watcher exited" notice to send.
Safety and limits
- The woken turn runs under the same sandbox and approval policy as any other turn. The watched command is a normal sandboxed unified_exec process; nothing new is granted.
- The output channel is bounded (64 chunks). If a watcher outruns it, the gap is surfaced to the model as a "dropped N chunks" notice and counted toward the flood ceiling, never dropped silently.
- Flood guard, by lines and by bytes: a watcher that crosses 5000 lines is auto-stopped with a notice, and a run of 16 KiB with no newline is truncated (which still counts toward that ceiling), so neither a line-flood nor a newline-free stream can spin the session or grow the buffer without bound.
- Notifications are never silently dropped: an idle session is woken, a running turn is injected into, and if the idle start loses a race the items are recorded against the turn rather than discarded. That matters for the terminal "watcher exited" notice, which fires after the loop has already deregistered itself and so cannot be retried.
- It does not ask Codex to host any bus or broker; the watcher is just a process the agent already had the rights to run.
- Edges handled rather than left ragged: the watch is line-oriented, so a final line with no trailing newline is held until the command exits and then delivered; on exit the loop waits a short grace for any trailing chunk still racing the cancellation token before its final flush (the same grace pattern the codebase already uses for streaming output); the first lines printed before this loop subscribes are recovered from the spawn's captured output (only a vanishingly small snapshot-to-subscribe window aside); and the flood ceiling is enforced per line, mid-chunk, so one giant chunk cannot blow past it before the check runs.
Tests and verification
The reference includes tests (core/tests/suite/monitor.rs, plus two unit tests in core/src/unified_exec/monitor.rs) covering the wake path and the safety behavior:
- stdout output wakes the agent (the common fswatch / tail case).
- stderr output wakes the agent too (the process stream the monitor reads carries both).
- a line printed before the initial yield ends still arrives, delivered from the seed.
- an unterminated final line is delivered when the watch ends (the line-oriented contract above).
- the flood guard auto-stops a runaway line-flood with a notice.
- a newline-free stream is truncated rather than buffered without bound.
- a command that exits delivers a "watcher exited" notice.
- the registry self-empties after a watch ends, so a later
listreports no active monitors. - unit tests cover the registry (insert / list / remove) and that self-deregister prunes an entry without aborting its task.
The integration tests each exercise the whole path: the tool call, a real background subprocess, the output, the idle-wake, the labelled delivery. The remaining edges I would cover with the PR are the lag back-pressure path under a genuinely lagging producer, the snapshot-to-subscribe seed window, and multi-watcher injection attribution.
The monitor suite passes: eight integration tests plus two unit tests, run with the repo's CI settings. The changed crate is clean under fmt and clippy. It is additive and gated behind a feature flag that defaults off, so it changes nothing when disabled.
Real-world, in production Codex: I built from source and ran it in both the CLI and the Desktop app with a live model. The agent calls monitor(action=start, command="fswatch <folder>", description="..."), finishes its turn, and the session goes idle making no API calls. Touching a file in that folder wakes it on its own and it reports the change. It reproduces in a few steps: enable the feature, prompt "use the monitor tool to watch and tell me when it changes," then touch a file in the folder.
On a PR
The diff is deliberately small: three new files (the tool handler, the delivery loop, and the typed notification fragment), plus a handful of one-to-nine-line wiring edits, no protocol change. It reuses unified_exec for spawning and the session's idle-start path for waking, so there is no new exec path or event type to reason about, and it is feature-gated, so it can land off by default and be turned on when ready. That should keep it cheap to review.
As per the contributing guidelines, I am limiting this to the proposal, the design, and the working reference rather than opening an unsolicited PR. If the approach is right, I am happy to open one and finalzie the details first (CLA included). I am also happy to split the flood guard or the registry into separate reviewable pieces if that reviews more easily.
Activity
github-actions commentedon Jun 24, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
yaanfpv commentedon Jun 24, 2026
Reviewed the flagged #29865. It is the same underlying need (wake Codex on background process output without polling) in the
wake_on_output-flag-on-exec_commandform; the author closed it and folded the work into #22003, the broader "inject background output into the session" issue.Keeping this open as the dedicated-tool form of the capability: a
monitortool the model starts/stops/lists, backed by a per-session registry, rather than a flag on a single exec call or a generic injection path. Same core wake seam, different surface.Cross-linking the cluster for whoever triages: #22003 (the open hub, where this is converging) and #29865 (the flag form, now closed). I have also added the working reference over on #22003.
zeel01 commentedon Jun 26, 2026
I'm not sure how much this issue overlaps with my own needs, but it's certainly similar. I have a workspace that interacts with files generated by another program, and I want the agent to react to changes made to those files automatically. However the changes don't come in on any kind of set schedule, and scheduling the agent to check the files on an interval is both too slow (won't react immediately, waits for interval) and wasteful of tokens since the agent can't actually tell if files changed without reading them.
Ideally it should be able to create a file watcher that triggers it to read the files as soon as they change but only when they actually change, and without needing to burn tokens detecting if a file changed when the operating system can do that much cheaper.
Also I think it would be ideal if there was some degree of filtering of what actually wakes the model or not - rather than just filtering what it receives, preventing it from waking at all if the filter is not satisfied. Based on its instructions, the model could write a small program that pragmatically checks output, logs, files, etc. and only wakes up the agent if the program determines that it is needed. This would further reduce the amount of unnecessary token usage and ensure more consistent behavior. Basically, shift as much as possible to a deterministic local process and only engage the agent for things it is actually needed for.
yaanfpv commentedon Jun 27, 2026
@zeel01 that overlaps almost exactly, and the wake-filtering is the part I care most about too.
The shape I've been building: the monitor runs a command you hand it, and every line that command prints to stdout is a wake event. So the filter isn't a separate config knob, it's just whatever the command emits. For your case that's
fswatch <dir>(orinotifywait -mon Linux) piped through a grep, and the agent only wakes on the lines that survive it. The OS does the change detection, nothing polls, and the model never reads a file just to learn whether it changed.Your "push it to a deterministic local process" instinct composes nicely with that. The watched command can be a small script the model writes itself, one that inspects the logs/files/output and prints a line only when a wake is actually warranted and stays silent otherwise. The model then only gets engaged for things that already cleared a local gate.
I have a working version of this on a fork. Here's the full diff against rust-v0.142.0: rust-v0.142.0...yaanfpv:codex:monitor-tool (agent-callable
monitortool, with fswatch / inotifywait / tail -f / poll-loop backends, persistent or one-shot). It's there as a reference for what an upstream shape could look like, and if you want to try it against your file-watching case I'm happy to walk through the build-and-run steps.hassaans commentedon Jun 30, 2026
I went ahead and implemented this against current
main. The branch is up in my fork with a draft PR, in case it's useful as a reference: hassaans#1Quick summary of the approach: the
monitortool runs a shell command as a background watcher on top of the existingunified_execmachinery, and each line it prints comes back to the model as a notification, with a flood guard for runaway output. The watcher runs with the same sandbox access the session has already granted, so it can reach the same paths as the rest of the turn. There's an end-to-end test suite plus a regression test for the permission behavior.I know external PRs are invitation only, so no expectations here. Happy to keep this as a reference, or turn it into a real PR if the team would find it helpful.
yaanfpv commentedon Jun 30, 2026
@hassaans Nice. We basically landed on the same design without comparing notes: a
monitortool running a watcher on top ofunified_exec, every line it prints coming back as a notification, a flood guard for runaway output, and the watcher inheriting the session's sandbox so it reaches the same paths the turn already can.I put a working version up a few comments back too: rust-v0.142.0...yaanfpv:codex:monitor-tool. Mine's a bit further along if it's useful to pull from: it also runs in the desktop app, does persistent and one-shot watches, keeps a per-session registry for list/stop, and handles a few things that bit me along the way (trailing output racing the exit, the first slice of output before the watch attaches, and which live turn a watcher's output lands in when several are running). ~1,100 lines, behind a flag, no protocol change.
Either way, two of us building this independently probably says the design's about right. If the team would rather land one PR than two forks, I'll merge ours together.
weathon commentedon Aug 24, 2026
Will that PR be merged
weathon commentedon Aug 24, 2026
Is this possible to use a hook?
ichabodcole commentedon Aug 25, 2026
@hassaans @yaanfpv Glad to see this is already in the works, and I hope it lands soon. I just wanted to say this feature in claude code has allowed me to create a new type of application experience where one or more agents can interact with each other or a human in the mix, so having this in codex would be a huge win. Thanks for putting in the effort!
This is repo is evolving set of skill based apps that use this technique to create collaborative experiences https://github.com/ichabodcole/spellbook
zeel01 commentedon Aug 25, 2026
A sort-of workaround I found is programmatic use of the Codex CLI. The big problem with it is that the App and the CLI can both apply a lock to a conversation, and then the other can't read it. This means you can't easily initiate turns in an app-owned task from the CLI, and if you create a task in the CLI that isn't marked as CLI only it will appear in the app but interacting with it in the app might lock it and break your script.
This would be solved by the two interfaces sharing one app server instance, rather than the app using its own private one that is separate from the CLI. Codex actually has a pretty robust ability for agents to inter-operate between tasks which does still work between a CLI task and an App task so there is some kind of private communication channel in use. But at the moment it's not something we can use directly.
That said, if you only use the CLI you should be able to have scripts invoke it to trigger turns in existing tasks. You just don't have the same level of monitoring/notifications that the app gives you.
jian-dong commentedon Aug 26, 2026
Strong +1 for this feature. This is an important workflow gap compared with Claude Code's
Monitortool.The key requirement is not just “run a command in the background”; Codex can already do that. The missing primitive is event-driven wakeup: let the agent start a watcher, become fully idle, and automatically resume only when the watcher emits an event or the background job changes state.
That matters for common coding workflows such as:
Today these workflows tend to require polling, which can trigger repeated model turns and replay a large context just to ask “is it done yet?”. An event-driven monitor would eliminate that idle token/turn overhead and make long-running agent workflows significantly more practical.
Ideal semantics are very close to what this proposal describes:
start / list / stop, normal sandbox/approval rules, no model calls while quiet, and a watcher event waking the idle session into a new turn.I would prioritize the core wakeup primitive over UI polish; once Codex can reliably wake an idle thread from a background event, richer monitoring UX can be built on top of it.
zeel01 commentedon Aug 26, 2026
We might actually have something interesting already: bc8b25e + 83d0153. It's not perfect, but the latest beta release of the CLI allows a message to be queued to an existing thread without the same single-owner issue I mentioned before. It's still kind of limited, but it has a lot of potential.
zeel01 commentedon Aug 26, 2026
Plus, I've found it's pretty good at hacking itself. Just having the minimal surface area would allow the agent to build its own tooling around it for whatever you want it to do. It does need a clean fully featured interface, but just the bare minimum would do wonders.
feat: detect Codex as the connecting client and warn against --follow
Roy3838 commentedon Sep 8, 2026
Hey! Different shape from what you're proposing (this wakes you, not the agent hahaha), but in case it's useful while this lands upstream. I had this type of problem a lot, so I built a free/FOSS tool for it: https://github.com/Roy3838/Observer
It runs a tiny local agent that watches your screen/terminal and pings you (Discord, Telegram, email, etc.) when it sees the thing you're waiting for, a build finishing, an error line, a CI flip. Inference is local so your screen doesn't leave the machine. Won't resume an idle Codex turn the way your monitor tool would, but it could help out!
Not trying to spam the thread, just adjacent enough that it might help someone here. Happy to answer anything :D
lherron commentedon Sep 20, 2026
Is something like this planned? It's a great feature in CC, would like to see it in Codex CLI.
Develata commentedon Sep 24, 2026
Strong +1 from a current Codex user. I run into this frequently with long-running jobs and agent/subagent workflows: the practical workaround today is to have the agent repeatedly probe process state, which turns idle waiting into repeated model turns and can consume a very large amount of tokens/quota for essentially no new information.
Claude Code's
Monitorbehavior is the reference point I would like here: let a deterministic local watcher own the wait, make zero model calls while nothing changes, and wake the original Codex thread only when the watched process/log/file/CI state emits a relevant event.For me, the important part is the event-driven wakeup primitive rather than UI polish. Once Codex can sleep without inference and resume on an event, the agent can build richer monitoring logic on top of it.
jian-dong commentedon Oct 10, 2026
@openai/codex-core-agent-team @etraut-openai — could we get an explicit maintainer triage on this proposal? CC @yaanfpv @hassaans, who have shared independent reference implementations.
The missing capability is event-driven wakeup of the same idle Codex session, not simply running a process in the background. For long-running builds/tests, logs, robotics simulations, and training jobs, the current alternatives are polling (extra model turns) or leaving the agent unable to react until the next user turn.
The two implementations linked above suggest a reviewable, opt-in MVP in
codex-core:monitor(start/list/stop)for a sandboxedunified_execwatcher;Remote-host persistence/management can be a follow-up (#44855) rather than blocking the basic primitive. Related discussions: #22003, #20312, and #15723.
Could the team clarify (1) whether native Monitor is on the roadmap, (2) any technical/safety blockers, and (3) whether one of the existing patches could be reviewed for a feature-flagged experimental implementation? Even a brief accepted/deferred/not-planned decision would help the community focus its effort.
zeel01 commentedon Oct 10, 2026
Also relevant are the recently introduced MCP events: https://developers.openai.com/plugins/build/mcp-events (https://github.com/modelcontextprotocol/experimental-ext-triggers-events/blob/main/docs/design-sketch-proposal.md) which do provide the possibility for an outside tool to be invoked by the agent and later return an event without polling. However it's quite limited at the moment and doesn't quite provide the more streamlined workflow desired here. Still, it's another example of extremely similar infrastructure that exists already.
sarinstein-yan commentedon Oct 18, 2026
Strong +1. I use Codex for research experiments with long hardware queues. A native monitor that lets the agent go idle with zero model calls, then wakes the same thread only on a meaningful event, would make a big difference. Periodic model-driven checks are costly and clutter the context.