ConnectOnionAll Posts
Agents

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.

By ConnectOnion Team · 3 min read · 479 words

AgentsWatchersArchitecture

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.

Star us on GitHub

If ConnectOnion saves you time, a ⭐ goes a long way — and earns you a coffee chat with our founder.