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
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.
ConnectOnion