arizuko › concepts › engagement
engagement
Routing delivered the message to a folder. Now engagement decides whether to keep listening. Mentioning the bot for every single message kills conversation flow, so once the bot has replied in a (chat, topic), that pair is “engaged” for a window of time. Subsequent inbounds in the same pair fire the agent without needing another mention.
two states
A (jid, topic) pair is either engaged or idle.
- Idle → engaged: a mention, a reply to a bot message, or an explicit
engage()call. The bot replies, and the timer starts. - Engaged → engaged: every inbound that arrives before the deadline fires a turn, no re-mention needed.
- Engaged → idle: the TTL expires, or the agent calls
disengage()when it’s done helping. - Idle stays idle: after disengage, the next inbound needs a fresh mention.
The column stores the deadline itself, not the time of the last message, so a read is a plain comparison against the clock. Each bot reply pushes the deadline out again — a long back-and-forth never times out mid-conversation, but a quiet 30 minutes ends it. Your own messages don’t extend it; only the bot’s replies do. The window is configured via ENGAGEMENT_TTL (default: 30m).
thread by default on Slack channels
The bot’s reply to a channel mention opens a Slack thread off the user’s message. The engagement window then attaches to that thread. Subsequent thread replies engage automatically and live in-thread, so the main channel stays uncluttered.
Broadcasts (scheduled tasks, migrate announcements, system events) bypass the rule — they have no parent message to thread under, so they land top-level.
corrective side-fork
When the user is correcting the bot’s last reply (“no, that’s wrong”, “rephrase”) rather than continuing the conversation, the agent can fork the topic to <current>#fix and run the correction loop there. The main thread stays clean; the fix loop happens in a child thread off the bot’s incorrect reply. Convergence reached, the agent posts one corrected answer to the parent and calls disengage() on the fix topic.
This is a convention — the agent decides when to fork. routd doesn’t enforce it.
length per surface
Every turn carries a <surface> hint so the agent can self-cap reply length:
<surface>slack-channel-thread</surface> threaded reply, full ceiling
<surface>slack-channel</surface> rare: top-level, hard-cap 200 chars
<surface>slack-dm</surface> full ceiling
<surface>slack-pane</surface> assistant sidebar, full ceiling
<surface>discord-channel</surface> default ceiling
<surface>telegram-dm</surface> full ceiling
The hint is computed from the chat JID shape and thread context. The agent reads it and obeys.
MCP control
engage(jid, topic)— explicit re-engagement, e.g. before a scheduled autonomous turn.disengage(jid, topic)— clear the engagement so the next inbound needs a fresh mention.
Both args are required — the agent passes the jid it intends to act on.
(chat, topic) is engaged, routd delivers to the engaged folder and fires a turn regardless of what the route table says — including #observe targets, trigger routes pointing elsewhere, or no matching route at all. The bot is mid-conversation; the route table is irrelevant until the TTL expires or disengage() is called.seeing who is engaged, and ending it
Engagement is invisible from the outside — the bot answering a message that carried no @mention is the only clue you get. The dashboard answers it at a glance: /dash/engagement/ lists every conversation the bot is currently staying in, with the group it belongs to and how long the window has left. It is operator-only, because it covers the whole instance rather than one folder.
Each row carries a disengage button. Most windows need nothing — they wear off on their own. Use it when the bot is talking in a chat it should stay out of: the window ends immediately and the bot will not speak there again until someone names it. It asks you to confirm first, because the people in that chat see the effect straight away.
The button goes through the same POST shown below rather than editing the database, so it gets the same folder check and writes the same audit entry — every early ending records who did it, whether it came from the dashboard, the API, or the agent's own disengage().
For a script, or for one conversation you already know, routd answers directly:
# one conversation: which folder holds it, and until when
curl -H "Authorization: Bearer $TOKEN" \
'https://<host>/v1/engagement?jid=slack:T01/C02&topic='
# every window that is live right now, soonest to expire last
curl -H "Authorization: Bearer $TOKEN" https://<host>/v1/engagement
# end one early: any ttl_seconds of zero or less clears the window
curl -X POST -H "Authorization: Bearer $TOKEN" \
-d '{"jid":"slack:T01/C02","topic":"","folder":"sales","ttl_seconds":0}' \
https://<host>/v1/engagement
The single-conversation read also returns last_reply_id, the message the next reply threads under. That one outlives the window, so an idle conversation still reports it while engaged_until comes back empty.
You see the windows your token's folder owns, and no others. A token with no folder at all — a root or service token — sees every window on the instance; that is the only way to get the full list, and it is deliberate rather than a side-effect of how deep your folder sits.
The same bound applies to ending one. A live window belongs to the folder that claimed it, and that is what the POST checks — naming a different folder in the body does not widen it. So you can never end a window you could not have seen in the list, and only a root or service token can end anyone's.
openapi.json: these three are hand-written, not generated from the resource registry like the cold-tier tables. Engagement is live conversation state, not operator configuration, and it shares a seam with the engage/disengage agent tools above — registering it would create a second path into the same rows. /dash/engagement/ reads and writes the same three endpoints rather than the table.go deeper
The mention check, observe-wins ordering, and TTL math live in specs/5/G. The fork primitive used by the corrective pattern: topics. Routing modes that interact with engagement: routing modes.