4.5 KiB
SPEC-010 — Human-behavior layer
The bot should feel like a considerate participant, not an instant wall of text: it decides whether to speak with a cheap classifier instead of trusting the main model's self-report, paces its replies, splits long answers, and sometimes just reacts. All knobs are per-deployment TOML; every feature degrades to the previous behavior when its knob is unset (config-off = v3.0.0 semantics).
BEH-01 — Classifier gates non-direct replies (coverage: test)
With classifier-model configured, every non-direct user message
first passes a cheap classification call (reply yes/no, factual
yes/no, optional reaction emoji). reply=false means no main-model
call happens at all — this is the boreness suppressor and the
butting-into-conversations fix (replaces trusting answer_needed
alone; the envelope flag still applies afterwards as second gate).
BEH-02 — Direct messages bypass the gate (coverage: test)
Mentions and DMs never go through the classifier — someone addressing the bot always reaches the main model. Welcome and bot-initiated flows do not pass the gate either.
BEH-03 — Classifier failure fails open (coverage: test)
A failed or unparseable classification (API error, budget refusal) falls through to the main model. Availability beats savings; the budget gate still protects spend.
BEH-04 — Reply pacing is typing-proportional (coverage: test)
With typing-chars-per-second set (recommended 30), the typing
indicator is held for len(part) / cps seconds per message part,
capped at typing-max-seconds (default 8), before sending. Unset or
0 = no pacing (v3.0.0 behavior).
BEH-05 — Factual answers skip the artificial delay (coverage: test)
Messages the classifier tagged factual (opening hours, prices,
addresses) are answered without the BEH-04 delay — utility beats
theater exactly where users are waiting for information.
BEH-06 — Long answers are split (coverage: test)
Answers longer than split-threshold chars (default 1200) are split
at paragraph (then sentence) boundaries into at most
split-max-parts (default 3) sequential messages, each under the
Discord 2000-char limit (which unsplit answers would crash into
today). Attached images go with the last part.
BEH-07 — Sometimes a reaction is the reply (coverage: test)
When the classifier returns reply=false plus a reaction emoji, the
bot adds that emoji to the user's message instead of staying fully
silent. Zero main-model cost, human touch.
BEH-08 — Quiet hours stop bot-initiated posts (coverage: test)
Within quiet-hours = "HH:MM-HH:MM" (host-local, may wrap midnight)
bot_initiated_allowed() is false: no boreness, later no scheduler
posts. Replies to users stay unaffected — a guest asking at 23:30
still gets an answer.
BEH-09 — Ignored channels are fully silent (coverage: test)
Channels matching ignore-channels get neither replies nor
classifier emoji reactions: the message handler returns before the
classifier gate, so no model call, no reaction, no history entry.
Entries are fnmatch patterns (todo* matches todo, todo-lists);
plain names keep matching exactly as before. DMs are never ignored.
channel_by_name resolution honors the same patterns. (Previously
the ignore check sat only in respond(), after the classifier —
emoji reactions leaked into ignored channels, and matching was
exact-name only.)
BEH-10 — Factual questions may use a stronger model (coverage: test)
With factual-model configured, a message the classifier tagged
factual (BEH-05) is answered by that model instead of model —
opening hours, release dates, news lookups get the stronger tier
while small talk stays on the cheap default. Unset = no change. The
retry-model override still wins on retry, and vision inputs keep
using model-vision.
BEH-11 — Addressed-only channels answer only when spoken to (coverage: test)
Channels matching addressed-only-channels (fnmatch patterns like
BEH-09) never get spontaneous participation: the handler returns
before the classifier gate unless the message addresses the bot — an
@mention or DM, a Discord reply to one of the bot's messages, or the
bot's name appearing in the message text (case-insensitive). No
model call, no emoji reaction otherwise. Scheduled tasks
(idle-impulse, follow-up) targeting such a channel are skipped at
execution time — the bot never posts there unprompted, whatever a
generator proposes. Unlike BEH-09 the bot still answers when
addressed; DMs are unaffected.