---
tags: [Agents, Watchers, Architecture]
---

# The file changed. Why didn't the agent wake up?

Issue #1499 asked for an agent that notices events by itself. Picture a Host
waiting quietly while `notes.md` changes. The tempting answer was to add a
`before_iteration` hook that checks the file. I followed that path through the
code and found the catch: every iteration begins *after* somebody calls
`agent.input()`. While the Host is idle, the hook never runs. There is no turn
to attach the file change to.

The Host already had two ways to start a turn without a person typing in the
UI. Its scheduler starts a turn when the clock fires; its inbox consumer starts
one when a chat message arrives. Both eventually call `input_handler()`. That
was the useful discovery. The missing piece was a common way to record an
observed event before asking the agent to do anything with it.

We first separated the two jobs. A small background poll records a file change
or timer firing in a durable queue. A worker then reads the event and hands its
envelope to `input_handler()` as a user message in a stable session. The agent
can be busy for minutes; observation does not have to wait for its answer.

Then a second event arrived while that turn was still running. Starting a
competing turn would fight the same session. Leaving it until the answer was
finished would make the Agent reason from stale state. The existing
`before_iteration` and `after_iteration` hooks provided the precise boundary:
claim the event before the next model decision, or request one more iteration
when it arrives during a final model call. That small piece is now a copyable
`watch_events` plugin. The Host only supplies the durable queue callback.

A restart exposed the harder edge. Suppose the agent's answer reaches the
session log, then the process dies before the queue records completion. Blindly
retrying would give the agent the same event twice and might repeat a tool's
side effect. Recovery now checks the saved session for that event ID before it
starts another turn. It cannot promise exactly-once external side effects after
an uncertain crash, but it can avoid repeating a turn whose completion is
already in the Host's own record.

The lesson was about where to put the boundary. A plugin helps an agent
*during* a turn; a watcher must also make an idle turn possible. The queue
makes that handoff survive a restart and gives the operator a place to see
what happened. Neither part needs to pretend it owns the other's lifetime.

The focused watcher and plugin suite passed 17 cases, including an event
arriving during a tool batch, one arriving during a final model call, a lost
queue acknowledgement, and the copied plugin loading outside the package.
The merged PR's Python 3.10–3.13 CI matrix passed. A different kind of watch,
registered by an Agent to resume its original conversation days later, still
needs the session-owned runtime planned in #1788.
