arizuko

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 interjection is off until you turn it on. No shipped template sets 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.

A broken block is an error, not a default. An unknown mode, a window that does not parse or a timezone that does not exist logs a config error. The group then starts no turn. The block never falls back to 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.

CheckStops the turn whenSetting and default
RunningTurna turn already runs in this chat—
Silencethe gap since the last real inbound message is below the floor or above the ceilingPROACTIVE_SILENCE_MIN 90s, PROACTIVE_SILENCE_MAX 12h
Cooldownthis chat had a proactive turn recentlyPROACTIVE_COOLDOWN 24h
QuietHoursthe local time is inside one of the windows of the groupthe quiet_hours list
BotQuietthe bot spoke here recentlyPROACTIVE_BOT_QUIET 15m
RecentActivitythe last hour had too few inbound messagesPROACTIVE_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 #design and speak in #eng.
Operators cannot script it
You choose lurk or silent and 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.

VariableDefaultWhat it sets
PROACTIVE_ENABLEDunset, which is offthe main switch: unset means no scanner exists
PROACTIVE_SCAN_INTERVAL30show often the scan runs
PROACTIVE_SILENCE_MIN90show quiet the chat must be before an interruption is polite
PROACTIVE_SILENCE_MAX12hhow quiet is too quiet to speak
PROACTIVE_COOLDOWN24hthe time per chat before a second interjection
PROACTIVE_BOT_QUIET15mhow long the bot stays out after its own message
PROACTIVE_RECENT_ACTIVITY_MIN3the 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.