Skip to content

Feature: an agent-callable monitor tool that wakes Codex on background events (logs, files, builds, CI) without polling #29922

Open

Description

@yaanfpv

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> or inotifywait -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:

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:

  1. 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.
  2. 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 list reports 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

  1. added
    CLIIssues related to the Codex CLI
    appIssues related to the Codex desktop app
    tool-callsIssues related to tool calling
    agentIssues related to the core agent loop
    on Jun 24, 2026
  2. github-actions commented on Jun 24, 2026

    @github-actions
    Contributor

    Potential duplicates detected. Please review them and close your issue if it is a duplicate.

    Powered by Codex Action

  3. yaanfpv commented on Jun 24, 2026

    @yaanfpv
    Author

    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_command form; 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 monitor tool 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.

  4. zeel01 commented on Jun 26, 2026

    @zeel01

    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.

  5. yaanfpv commented on Jun 27, 2026

    @yaanfpv
    Author

    @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> (or inotifywait -m on 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 monitor tool, 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.

  6. hassaans commented on Jun 30, 2026

    @hassaans

    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#1

    Quick summary of the approach: the monitor tool runs a shell command as a background watcher on top of the existing unified_exec machinery, 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.

  7. yaanfpv commented on Jun 30, 2026

    @yaanfpv
    Author

    @hassaans Nice. We basically landed on the same design without comparing notes: a monitor tool running a watcher on top of unified_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.

  8. weathon commented on Aug 24, 2026

    @weathon

    Will that PR be merged

  9. weathon commented on Aug 24, 2026

    @weathon

    Is this possible to use a hook?

  10. ichabodcole commented on Aug 25, 2026

    @ichabodcole

    @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

  11. zeel01 commented on Aug 25, 2026

    @zeel01

    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.

  12. jian-dong commented on Aug 26, 2026

    @jian-dong

    Strong +1 for this feature. This is an important workflow gap compared with Claude Code's Monitor tool.

    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:

    • waiting for a long build/test/training job to finish;
    • watching logs for the first error or a specific pattern;
    • reacting to file changes while working elsewhere;
    • watching a dev server/process and resuming when it exits;
    • waiting for CI/deployment state changes.

    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.

  13. zeel01 commented on Aug 26, 2026

    @zeel01

    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.

  14. zeel01 commented on Aug 26, 2026

    @zeel01

    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.

    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.

  15. Roy3838 commented on Sep 8, 2026

    @Roy3838

    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

  16. lherron commented on Sep 20, 2026

    @lherron

    Is something like this planned? It's a great feature in CC, would like to see it in Codex CLI.

  17. Develata commented on Sep 24, 2026

    @Develata

    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 Monitor behavior 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.

  18. jian-dong commented on Oct 10, 2026

    @jian-dong

    @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 sandboxed unified_exec watcher;
    • no model calls while the watcher is quiet;
    • bounded, explicitly untrusted events that safely wake the owning idle session, with defined behavior for active turns;
    • cleanup, cancellation, and flood protection.

    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.

  19. zeel01 commented on Oct 10, 2026

    @zeel01

    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.

  20. sarinstein-yan commented on Oct 18, 2026

    @sarinstein-yan

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    CLIIssues related to the Codex CLIagentIssues related to the core agent loopappIssues related to the Codex desktop appenhancementNew feature or requesttool-callsIssues related to tool calling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions