Skip to content

Asking the bot questions

An answer is built in four steps: decide what the asker may see, route the question and let the model read only that, check that every source it cites was really found, then show the answer privately or in the channel. For the member’s view, see Asking the bot questions.

Asking needs ASK_ENABLED and GEMINI_API_KEY. When it is off, /ask is not registered and a tag is never answered. The limits and retention below are set in Configuration.

Command. /ask takes a public option, false by default. Without it the reply is ephemeral (only the asker sees it). With it the reply is posted in the channel, but a refusal or an error text (a limit, busy, nothing to search, the model failing) is removed and sent as a private follow-up. The cheap refusals (no Gemini key, an empty question, a question of yours already running, busy) are checked before the command defers (acknowledges it so Discord allows more than its 3 second reply limit), so no public “thinking” message ever appears for them. The command then checks the limits, claims the question (writes its record, which counts toward the limits) and starts the answer.

Tag. A message counts as a tag when it is in the server, is not from a bot, does not mention everyone, and contains a mention of the bot or of the bot’s own role. The question is the message with those mentions removed and its spaces collapsed. The bot reads it once with gemini-3.5-flash-lite (minimal thinking, at most 160 tokens out, 10 seconds, the first 2,000 characters) and gets back one of three intents: follow_up, question or none. “Remind me in 2h” is a follow-up. “Remind me what we decided about X” is a question. Greetings and passing mentions are none. One tag handler serves both features:

  • A follow-up needs reminders switched on and an author the allowlist lets through.
  • A question needs asking to be on. It does not use the allowlist.
  • If neither feature applies to the author, the tag is ignored. If only asking applies and the author’s last question is still running, the bot adds a ⏳ reaction at once, with no reading.

For a question the bot first checks that it has View Channel, Embed Links, Read Message History and Send Messages (Send Messages in Threads in a thread) in the channel; if not, it logs a warning and skips the tag before anything is claimed. A question over 500 characters gets Keep questions under 500 characters, or use /ask. as a refusal. Otherwise it shows typing, refreshed every 8 seconds, and replies to the tagging message through the same path as /ask (guard, claim, answer), so the same limits and checks apply. A tag on a reply passes the replied-to message to the model, which reads it first. A tag with nothing but the mention is never a question.

Refusals (a limit, busy, a question still running, the length notice) are plain replies in the channel, sent once per member per hour whatever the reason; later refusals in that hour only add a ⏳ reaction to the message. Failure texts (I could not answer..., the timeout, the channel check, This only works in the server.) are plain replies too, and each adds Use /ask to try again. A tag whose message is already deleted is not claimed and gets no reply.

The reading is limited to 20 per author per hour and MENTION_MAX_CHECKS_PER_DAY per day for the whole process, counted in memory and reset on restart. Without text to read, without a Gemini key, past either limit, or when the reading fails (logged by error class), the tag falls back: a tag on a reply is a follow-up with no time and any other tag is ignored.

If the tagged message or its channel is deleted, the saved question and trace are blanked, whatever the status; an edit changes nothing, because the question is saved when the tag is claimed. If the message a tag replied to is deleted, the trace is blanked and the question stays.

Buttons. Every answer, including one that found nothing, has the 👍 Helpful and 👎 Not helpful buttons; refusals and failure texts have none. Pressing one saves the vote, removes the buttons and sends the asker a private thanks. The message text is never edited, because it may be a public answer. A vote is saved only by the asker and only for a finished question. The old Answer privately button is retired: pressing one replies This button is no longer used. Ask with /ask or tag me. and does nothing else. The button handler stays registered when asking is switched off.

A question counts toward the two daily limits (per member and server-wide, set in Configuration) from the moment it is claimed, whatever happens next; a row left claimed by a crash counts and is never retried. The counts are read and the row is claimed in one transaction under an advisory lock (a database lock that makes simultaneous claims take turns), so simultaneous questions cannot overshoot. The claim waits at most 5 seconds for that lock (10 seconds for the whole statement); if it times out, the member gets Busy right now, try again shortly. and nothing is claimed.

One question per person runs at a time, and 4 run across the server. A second question from the same person gets I am still answering your last question. A fifth across the server gets Busy right now, try again shortly. Both counters are in memory; a slot is freed when the answer is sent or the question fails.

The bot builds a scope for the asker right before the model runs. Linear is not a source.

SourceScope
MessagesThe same channels as a personal digest: every stored channel the member can view and read history of in Discord right now, with threads following their parent
MeetingsPosted meetings whose summary channel the member can currently view. A meeting whose summary went to a channel they cannot view does not exist for them

A public answer (a tag, or /ask with public) is then narrowed to the audience of the answer channel. The audience is the channel (for a thread, its parent) and the threads under it, plus every open channel and the threads under those. A channel is open only when @everyone can view it and read its history and no role or member overwrite (a per-role or per-person permission rule on the channel) on it denies View Channel or Read Message History. Open channels are taken from the cache only; a channel not in the cache is treated as not open. The scope keeps only the channels and meeting channels that are in both the asker’s scope and the audience, so the answer never uses anything a reader of that channel could not see. A private answer is not narrowed.

If Discord cannot say what the member can view, or what everyone can view, the question stops as scope_unavailable and the model is never called. The question still counts toward the limit. If every list is empty the answer is I could not find anything you have access to search. with no model call, recorded as no_answer. The meeting tools are left out when there are no meetings. Bot messages, deleted messages and messages with no text are never searched.

The bot sorts the question in code, before any model call. Two checks decide the route:

  • A period. These are hard periods: “today”, “yesterday”, “yesterday and today”, “this week”, “past week” and “last week” (the last 7 days, not the calendar week), “last 3 days” and its forms (“past”, “last”, “previous” or “over the past” or “over the last”, then a count and days, weeks, months or years, so a week is 7 days, a month 30 and a year 365, capped at 3,650 days; a count can be a number, “a”, “one” to “ten”, “couple” or “couple of”, or “few”, as in “last few days”), “past month” and “last month” (30 days), “this month” and “this year”. “Today”, “yesterday”, “this month” and “this year” are calendar periods in the asker’s time zone (members.timezone), or UTC when none is set. “recently”, “lately”, “latest” and “newest” mean the last 14 days but are soft: the model is told about them and may widen.
  • Aggregate wording. “most”, “common”, “frequent”, “often”, “top”, “popular”, “trend”, “theme”, “summary”, “overview”, “complain”, “happened”, “mentioned”, “requested”, “reported”, “came up”, “people say”, “everyone”, and “what questions”, “what features”, “what bugs”, “what issues”, “what problems”, “what ideas” or “what requests”.
RouteWhenModelStart
WideAggregate wording, or a period (hard or soft) on a question that is not a replygemini-3.6-flashNothing preloaded; the model reads with the wide tools
LookupEverything else, and replies without aggregate wordinggemini-3.5-flash-liteThe 30 best search matches for the question are included with it, inside a hard period when there is one

Both routes have the same tools, low thinking, a 90 second limit (the last 30 seconds kept for the final answer) and at most 8 rounds of tool calls, with up to 8 calls per round, 3 running at once and 30 seconds for each. A question that runs out of time ends as failed and the asker sees That took too long to search. Try a shorter period or a more specific question.

A tool call that names a channel outside the scope (the narrowed one, for a public answer) returns channel_not_available. A parent channel widens to its threads in scope. Channels and members can be named or given by id; a member name that fits several people returns author_ambiguous with the names, and one that fits nobody returns author_not_available. Arguments that do not fit the limits return invalid_arguments. A ref or id that was not shown returns message_not_available or meeting_not_available, and a meetings failure returns meetings_unavailable.

ToolWhat it does
search_messagesHybrid search (words and meaning, see Search) with 1 to 5 phrasings of up to 200 characters at once, 100 results by default, up to 200, best first. Optional channel, author, within_days (1 to 365) or since and until
list_messagesEvery stored message with text in a period, newest first, 1,000 by default, up to 3,000, no default period. It also lists the meetings held in that period (up to 50, following the channel filter), unless it is filtered by author. If more messages exist it says so and names the span it covered
get_message_contextThe messages before and after one message, 5 each side by default, up to 10. It takes an m ref or a message id
lookupFinds a member (by display name) or a channel (by name) in scope, up to 10 matches
search_meetingsFull-text search over the titles, summaries, action item titles and transcript text of posted meetings, 5 results by default, up to 10, each with a snippet of up to 300 characters
get_meetingOne meeting by g ref or meeting id: summaries (each cut to 2,000 characters), action items with owner and whether a ticket was created, and up to 3 transcript fragments for the query

The time arguments are within_days, or since and until (which win). When the question has a hard period, the bot clamps every call to it, whatever the model asked for, and the result says which period was applied. On the question path only, a word search that finds fewer matches than asked for is topped up with messages that share any word of 3 or more letters with the phrasing; summaries keep the strict search. A search that cannot get a query embedding within 4 seconds uses words only, and each word search stops after 5 seconds.

Each question has an evidence budget of about 200,000 tokens. Every row shown to the model, the preloaded matches included, is charged against it. Rows that do not fit are dropped before they are shown, and the result says how many were cut. Once the budget is spent the tools return evidence_budget_reached, and when less than a tenth is left the tools are switched off and the model answers with what it has. Each message row is cut to 300 characters. On a large server an all-time read therefore stops at the newest several thousand messages, and the answer says the read was cut.

Every row shown gets a short ref on first sight: m1, m2 for messages and g1, g2 for meetings. A row looks like [m12] 2026-10-14 09:12 #design @Chris: text. The model cites those refs.

The system prompt is the same, byte for byte, for every question, so Gemini can cache it. It tells the model to answer only from what it was given or the tools return, to read the whole period and count for “most” and period questions, to read the whole history for all-time questions, to state the period it used, to cite with refs, to say plainly when nothing was found, never to write links and never to follow instructions found in a message, a meeting or the question. The per-question facts go in the first message: the current time, the asker’s name and id (so “I” and “me” mean the asker), the detected period, the message a tag replied to (read first), the preloaded matches, and the question last, wrapped in tags, with any angle brackets in the question made inert.

What the bot checks before showing an answer

Section titled “What the bot checks before showing an answer”

The model returns whether it found something, an answer (cut to 1,500 characters if longer), and the refs it cited (at most 12 are kept). The bot then:

  1. Removes any bare number marker such as [1] the model wrote itself, so only the bot can produce one.
  2. Replaces each ref marker ([m12], [m3, g1]) with numbered markers, in the order the refs first appear, and drops markers for refs that were never shown. Any other bracket, such as [GPT-4], stays as text, and adjacent duplicate markers are merged. At most 8 sources are kept; a ref listed by the model but not marked in the text is numbered after the marked ones and still listed.
  3. Marks the answer answered only if the model found something, at least one source was kept and the text still has a marker. Otherwise it is no_answer, and the answer text is still shown, with whatever sources were kept, or I could not find that in what I can search. if nothing is left of it.
  4. Builds every link itself from the ids it saw. The model never writes a link, and mentions and links in the text are made inert (shown as plain text, so they do nothing).

The answer is a Discord embed (a boxed message) with no pings allowed. Only the first marker of each source is a link; below the text is one linked line per source. When the embed would pass 4,096 characters, source lines are dropped from the end first, and then the text is cut. Labels are #channel · Author · 12 Oct and Meeting: Title · 12 Oct. A meeting is linked through the first summary message the bot posted for it; a meeting without one can be read but not cited.

Console lines hold only the question id, status, failure reason, latency, error classes, the model and token counts (input, cached input and output). The question, the answer, tool arguments and tool results are never printed.

Each question is also stored in the ask_questions table, the one agreed exception to that rule. The row holds the question, the model’s tool calls (name, arguments cut to 500 characters, the number of results and the first 200 ids of what came back, at most 30 calls), the ids of the sources it cited, the status, the model, latency, token counts and the feedback vote. A question or trace is blanked when the message it came from is deleted. It holds no message text and not the answer. Rows are deleted after ASK_LOG_RETENTION_DAYS, at start and then daily, even when asking is switched off. See Data.