arizuko › concepts › proactive interjection
proactive interjection
Proactive interjection lets the bot speak when nobody spoke to it. Engagement covers the other direction. For example, someone asked a question in a channel forty minutes ago, three people are in the room, and nobody answered. The agent has the answer. Proactive interjection is a narrow permission, behind many checks, to say it.
PROACTIVE_ENABLED, so no arizuko instance interjects by default. When the variable is unset, routd builds no scanner at all. You opt in twice, on purpose: once for the instance and once for each group. The rest of the page describes what happens after you opt in.Two switches must both be on
The instance switch starts the scanner
Set PROACTIVE_ENABLED=true in the .env of the instance and restart. This starts the scanner and nothing else. Every group stays silent.
# in /srv/data/arizuko_<instance>/.env
PROACTIVE_ENABLED=true
The group switch sets the mode
Add a proactive: block to the frontmatter of the CLAUDE.md of that group, at <data-dir>/groups/<folder>/CLAUDE.md. Only the mode lurk can start a turn.
---
proactive:
mode: lurk # silent (default) | lurk
quiet_hours: ['22:00-08:00 Europe/Prague'] # optional, repeatable
---
routd reads the file again when its modification time changes, so a saved edit applies with no restart.
Each quiet-hours entry has the form HH:MM-HH:MM <IANA timezone>. A window can cross midnight: 22:00-08:00 is the night, not a 14-hour day. You can list as many windows as you need.
silent, because then a typo would look the same as a setup that you turned off on purpose.Six checks can each stop the turn
routd scans on a loop iteration when at least PROACTIVE_SCAN_INTERVAL has passed since the last scan. It reads the eligible chats and runs these checks in this order. The first check that objects stops the scan for that chat. There is no score: no check adds to another or outvotes another.
| Check | Stops the turn when | Setting and default |
|---|---|---|
RunningTurn | a turn already runs in this chat | — |
Silence | the gap since the last real inbound message is below the floor or above the ceiling | PROACTIVE_SILENCE_MIN 90s, PROACTIVE_SILENCE_MAX 12h |
Cooldown | this chat had a proactive turn recently | PROACTIVE_COOLDOWN 24h |
QuietHours | the local time is inside one of the windows of the group | the quiet_hours list |
BotQuiet | the bot spoke here recently | PROACTIVE_BOT_QUIET 15m |
RecentActivity | the last hour had too few inbound messages | PROACTIVE_RECENT_ACTIVITY_MIN 3 |
If all six checks pass, the answer is still no by default. One check must argue for a turn, and only one such check exists: UnansweredQuestion. It passes when the last inbound message ends with a question mark and the bot has said nothing since. A new signal is a separate named check in this list.
Silence passes only between the floor and the ceiling. “The last real inbound message” means a message from a person. The replies of the bot and its proactive messages do not reset that clock.
The 24-hour cooldown cannot be turned off
A mechanism that starts turns from its own state, with no person in the loop, can loop. So after a chat gets a proactive turn, it gets no other one for PROACTIVE_COOLDOWN, 24 hours by default. No group setting and no mode makes the cooldown shorter.
routd writes the cooldown in the same database transaction that queues the turn, before it dispatches anything. If routd stops between the two, the cooldown is already set. In the worst case, one interjection does not happen. Two interjections never happen.
The timestamp lives in the chat_proactive table of routd, not in CLAUDE.md. It is not a setting: nobody edits it, and the system records it.
The agent can still decide to say nothing
The checks decide whether the moment is right for a turn. They do not decide that the agent has something worth saying. So the turn runs, and the agent can send nothing. That result is normal, not a failure, and it uses the cooldown like a turn that spoke.
The turn carries one extra block, so the agent knows what kind of turn it is:
<proactive_reason check="UnansweredQuestion">
Last inbound "so do we roll back or not?" ends with a question and has no bot reply.
</proactive_reason>
The block means “here is a moment, consider speaking”. It is not a question to the agent. routd logs the check that started the turn, or the check that stopped it, with the chat and the folder. If an interjection goes badly, the log shows which check let it through.
A proactive turn also leaves no mark on engagement. It opens no window, so the next message from a person routes as if the bot had stayed quiet.
Interjection has three limits by design
- It is not an event-trigger engine
- The checks use a small fixed set of signals from arizuko itself. To make the agent react to your deploys or alerts, run that watcher next to arizuko and expose it over MCP. The agent then reaches it as a tool.
- It works in one chat at a time
- It does not read
#designand speak in#eng. - Operators cannot script it
- You choose
lurkorsilentand the quiet hours. You do not write new checks. For a custom trigger, use a watcher over MCP as above.
Nothing else changes. The output leaves by the usual path of the adapter. The turn has the same permissions as any turn. The agent uses the same send tool, and no special proactive verb exists.
Seven variables tune the scanner
The settings apply to the whole instance. All of them are optional, and routd reads them once at startup. reference/env has the full table with types and source lines.
| Variable | Default | What it sets |
|---|---|---|
PROACTIVE_ENABLED | unset, which is off | the main switch: unset means no scanner exists |
PROACTIVE_SCAN_INTERVAL | 30s | how often the scan runs |
PROACTIVE_SILENCE_MIN | 90s | how quiet the chat must be before an interruption is polite |
PROACTIVE_SILENCE_MAX | 12h | how quiet is too quiet to speak |
PROACTIVE_COOLDOWN | 24h | the time per chat before a second interjection |
PROACTIVE_BOT_QUIET | 15m | how long the bot stays out after its own message |
PROACTIVE_RECENT_ACTIVITY_MIN | 3 | the inbound messages needed in the last hour for the room to count as busy |
A missed scan is skipped, never repeated. If a long turn holds the loop for three scan intervals, the next scan runs once. It does not start three interjections to catch up.
Related pages explain the shared parts
routd/README.md describes the synthetic row that the scanner writes and why it renews no engagement. engagement covers the mention half of the same mechanism. personas shows the other settings in the CLAUDE.md of a group.