The watcher path, tool path, trigger files, and declared config do not meet. Record the traversal and prompt-injection boundaries before schedules wake an agent.
3.8 KiB
The nervous system
One channel. One kind of message. Anything that happens, a schedule coming due, a file changing, a peer reaching in, becomes a SensorEvent and goes onto the bus. Everyone listening hears all of it. No one waits on anyone else.
The code is src/core/nervous/mod.rs.
The bus
EventBus is a broadcast channel, cloneable, so anything holding the server can subscribe and get its own stream. Two moves: send and subscribe.
Send is fire-and-forget. If no one is listening, or a listener has fallen behind, the event is simply gone. The bus is a nerve, not a ledger. Anything that must be kept subscribes and writes it down; the event log does exactly that.
The ring holds 256. A listener that falls further behind is told it lagged and skips ahead. Nothing is replayed.
That makes the bus unsuitable as belief or audit storage. Evidence that must survive is accumulated by belief; state transitions and notable somatic events that must be reconstructed enter the device forensic trail.
It is a broadcast and not a queue because many things want every event at once: the log, the heartbeat, the firehose, the federation bridge. A new listener, a desktop face, a health monitor, attaches without the sender ever knowing it is there.
The message
Every event is a SensorEvent:
sensor_name: who fired (cron,energy,federation)event_type: what it is (schedule_due,sensorium:input,turn:segment)timestamp: whentarget: a conversation, a schedule, a room; depends on the typeurgency: 0 to 1, how loudly it asks to be seenpayload: whatever the type needsseed_id: empty for a local event, a peer's key when it came from elsewherereply_to: where a directed reply goes; empty for a broadcast
One type, not an enum per event, so a listener filters on event_type and ignores the rest, and the log can write everything down without knowing the taxonomy.
seed_id rides on every event though federation isn't here yet. When it lands, a sensor becomes federated without the envelope changing. See federation/01-node-enrollment.
What pushes
A SensorConfig says how a sensor takes part: which domain (Cron, Filesystem, FilesystemWatch, GitDiff, Memory, Process, Federation), whether it pushes on its own or waits to be asked, when it fires (once, on change, or on an interval), and how eagerly.
A sensor marked as a nerve ending fires onto the bus uncalled. The schedule is the first intended nerve ending, but it is not operational for agent-created schedules in the current source.
Schedule boundary — observed 2026-08-20
LocalBackendspawnsCronSensoragainstagents/<id>/schedules/, while the CLI and schedule tool writeagents/<id>/memory/schedules/. Those files are therefore not watched.schedule runand the tool'striggeraction create.trigger-*files;CronSensornever consumes them.[schedules].enabledandschedules_dirare declared in config but ignored by theLocalBackendwiring.- Schedule names are interpolated into a path without validation. Do not treat the tool as a safe authority boundary until traversal is rejected and file ownership is explicit.
- A due schedule injects its prompt as a background user turn. Schedule text is therefore untrusted input, not an authorization token for shell, Git, deployment, publication, or credential use.
The intended repair is one canonical directory, validated names, working
manual triggers, honoured configuration, and a capability-limited wake path.
For the Gitea watcher, schedules may later wake Ani to inspect already
reconciled read-only state; they must not replace signed webhooks, API
reconciliation, or CI authority. The other sensor domains remain named and
waiting. Tracked in docs/tasks/firehose-subconscious-subscription.md.