Pipeline

One message, from arrival to delivery

A message first lands in a durable queue, then deterministic gates and a reply rating decide whether to answer. If it does, Diana assembles context, generates with the model or the agent, and only after pre-send review sends the reply back to its source channel in chunks. Every stop leaves an outcome code and a reason.

Overview

Five stages

StepDecisionEnd Stopped / silentContinues elsewhereGoes back to an earlier step
  1. Platform message

    OneBot / Telegram / QQ / DingTalk / Feishu / WeCom, normalised into a MessageEvent.

  2. Durable inbound queue

    Backfill after disconnects, gap detection and retries; survives a restart.

  3. Reply trigger

    Deterministic gates → direct triggers → reply rating.

    No replyreason recorded
  4. Assemble context

    SOUL.md, history, memory, media; the stable part first so the prefix cache hits.

  5. Generate

    With the agent on, a tool loop (possibly using a browser); otherwise a single model call.

  6. Pre-send review

    Silence, dedup, the audit model, group stop.

    Stoppedreason recorded
  7. Send in chunks

    Send-time guards, idempotency, backoff and retries.

  8. Record the outcome

    Outcome code and reason in inbound_events, shown in the WebUI event center.

01 Reply triggers

Deciding whether to answer

HandleEvent does not generate a reply directly. It writes the message into a SQLite inbound queue and wakes a worker, so a crash, a restart or a stuck model does not lose it: a worker claims an item with a 10-minute lease, and an expired lease is claimed again.

  1. HandleEvent

    The bot's own messages only go into history; pokes, joins, friend adds and recalls follow their own switches and cooldowns and never enter the queue.

  2. Private-chat admission

    Everyone / owner only / allowlist.

    Deniedignored_private_admission
  3. Register the direct call

    noteDirectedInbound runs before enqueueing and is later used to interrupt the same person's older reply.

  4. Write to the inbound queue

    Priority: direct trigger 100, quote reply 80, resolvable link 60, everything else 0; ties by time.

    Write failsretry journal replayed with 2–30 s backoff, plus a backfill
  5. Worker claims it

    By default 3 items per group and 2 per private chat in flight, under the global reply concurrency limit.

  6. A backfilled message?
    History onlybackfill_history_onlyOlder than the replay windowignored_stale
  7. Speech-to-text → route

    routeMessageEvent. A transient transcription failure retries the item later.

  8. Deterministic gates

    12 checks that do not call a model; see the table below.

    Stoppedignored_*
  9. Directly triggered?

    Private chat, a mention, a quote of the bot, an alias, an owner command, a resolvable link or a plugin command — after passing the reply gate (owner bypass → blocked → allowlist → exempt → active hours → member level).

    Nounprompted reply rating
  10. Is a reply to the same person generating?

    A message added within 2 minutes is classified by a model as a repeat, a supplement, a correction or an independent question.

    Supplement or correction above the thresholdthe old reply is regenerated merged_into_reply
  11. Into the reply flow

Processing errors retry from 5 seconds, doubling up to 5 minutes, at most 5 times; after that the item is recorded as dropped_retries_exhausted. A definitive platform rejection is recorded as dropped_send_rejected and not retried.

01 Reply triggers

Backfill and gap detection

Messages sent while the bot was offline never arrive as pushes; they can only be pulled from the platform afterwards. runInboundCoordinator does that.

  1. Process start

    Releases expired leases and reads the last online checkpoint. While online the checkpoint is saved every 30 seconds.

  2. Heartbeat sees a disconnect or offline account

    Stops claiming and saves the checkpoint.

  3. Reconnected

    Replay cutoff = disconnect time − 30 minutes, looking back at most 24 hours.

  4. Three backfills

    After 1 second, then again at +1 and +5 minutes. Walks the group list and recent contacts, 100 per page and at most 200 per session, one session at a time.

  5. Deduplicate

    Compared with queued and stored messages, so nothing is processed twice.

  6. Should this one take part in replies?

    Only the newest N per session (history_backfill_message_limit, default 3) that would have triggered a reply directly are queued normally.

    The restfill in history only, no replies
  7. Queued normally
Why only the newest few

Coming back after an hour offline and answering a hundred stored group messages one by one would flood the chat. The recovered history still goes into context; it just does not trigger replies.

While online there is also gap detection: the first live message per group after a reconnect, and any jump in seq numbers, triggers verifyGroupSeqGap — a targeted refetch once backfill finishes, rechecked at 15 seconds, 1 minute and 3 minutes. It logs backfill_gap_detected / resolved / unresolved.

01 Reply triggers

Deterministic gates

Checked in order; any stop ends with a specific reason. None of these calls a model (except checks 12 and 13), so results are stable and reproducible.

#CheckOutcome when stopped
1The bot left the group or the group is unavailableignored_unavailable_group
2Resolve quotes, forwards and images; plugin observers; store history—
3Group not enabled or not admitted (owner suppression commands excepted). A disabled group is either dormant (default: local history only, no memory extraction, context summaries, semantic indexing or voice transcription) or silently observing (all of those keep running, it just never replies)ignored_policy; blocked users record ignored_user_blocked
4The bot is muted in the groupignored_bot_muted; with "judge while muted" on, the judgement runs and records ignored_bot_muted_judged
5Group model quota exhaustedignored_model_quota
6Event triggers (keyword / regex tasks)Not a stop; tasks run in the background
7The owner's clear-context commandHandled immediately
8Backlog hand-over (see "Concurrency")merged_into_backlog_turn
9Member level threshold (OneBot; mentions, owner and admins exempt)ignored_member_level
10Reply suppression is activeignored_response_suppression
11Video only, no textignored_video
12The sender is a marked bot or a Telegram bot and did not mention or quote this bot: a model decides whether it is talking to usignored_bot_message
13Reply fatigue is full (many replies to the same person whose messages were long, repetitive and aimless; a fast and a slow trace decaying over 10 minutes and 6 hours, total = fast + 0.2 × slow): after deciding to reply, the accumulated judgement comes first: the purpose scores the send audit gave recent turns are blended into a value that decays over 30 minutes, and a person who keeps asking, correcting or requesting things still gets a reply. Otherwise a model reads the same time-ordered transcript as intent recognition and checks whether this message brings anything new or asks/requests something; only when it does neither is there no reply, even for mentions and quotes. No model call while fatigue is below the limit or the accumulated purpose is high enoughignored_reply_fatigue

01 Reply triggers

Rating unprompted replies

Ordinary group messages that were not directly triggered first pass the group's random sampling rate (owner exempt), then go to the router model. For each candidate it answers two things with a short reason each: relevance.directed (is this clearly addressed to the bot — yes or no) and chat_in.score (0–1, how natural it would be to chime in).

  1. Both switches off, or someone told the bot to stop in the last 10 minutes?
    Yessilent, no model call
  2. Build the rating input

    Addressing info, a time-ordered transcript and the group's extra criteria.

  3. Call the router model

    Purpose proactive_reply_router, bound to a chat model or the Jev decision model; waits at most 60 seconds.

    Timeout / errorsilentUnparsableask once more with a reminder, then silent
  4. directed = false and chat_in = 0?

    Means someone is asking it to stop, or a mechanical loop.

    Yessilent, even at "no limit"
  5. Answer-questions switch on and directed = true?
    Yespass replied_proactive
  6. Chat score meets the level, and the cooldown has passed?

    Cooldown defaults to 30 seconds and starts only after a chat reply is actually sent.

    Noignored
  7. Is the bot talking too much?

    Over 25% of messages in 10 minutes, at least 3, with more than one other person talking.

    Yes, below "no limit"silent
  8. Pass

    replied_proactive, into the reply flow.

LevelChat score needed
offNever chimes in
minimal≥ 0.90 (strictest)
low≥ 0.70
medium≥ 0.50
high≥ 0.30
extreme≥ 0.10 (loosest)
alwaysIgnores the score; cooldown and bot share still apply

"Is this addressed to the bot" is not scored: a 0–1 score makes the model waver between 0.45 and 0.55, and the same kind of message gets an answer one time and not the next. Any model failure means silence — timeouts used to fall back to a keyword rule (a question mark meant "public question"), which erred towards speaking when it should not. Rating details are logged as proactive_reply_route.

01 Reply triggers

Concurrency, merging and stop

When one session gets messages quickly, three mechanisms avoid answering each one separately or answering the wrong thing:

Backlog hand-over

  1. Queued ≥ 30 seconds

    With newer messages behind it in the session; first attempt only, never the owner's.

  2. Held in a backlog

    At most 20 messages, kept 10 minutes.

  3. Taken by the next message

    The newest direct trigger becomes the reply target; without one, eligible candidates go to the rating together.

Merge while generating

  1. Same bot, session and sender within 2 minutes

    Admission opens only after choosing a dialogue path that can regenerate. Resolvers, fixed plugin responses and local commands leave new questions to their own turns.

  2. Repeat / supplement / correction / independent?

    Merged only at or above the confidence threshold (default 75%).

  3. Repeats share a draft; supplements and corrections regenerate

    At most 3 generations. Admission closes before the final attempt, before sending, or after an external side effect. A question asked after the answer starts a new reply.

Interrupt

  1. The same person addresses the bot again in a group
  2. The old reply is dropped before sending

    superseded_follow_up; a recall only removes the quote, it never cancels a reply.

Merged requests still need settlement

Merging creates a durable pending handoff. It is finalized when the complete answer covering those requests is delivered. Silence, generation failure, cancellation and partial delivery requeue unanswered requests. Restart recovery releases pending handoffs without a live owner. An attempted answer with an unconfirmed outcome is not automatically resent; supplements absent from that answer are still recovered. Plugin facts retain independent delivery, while optional model commentary obeys the follow-up send gate.

Group stop

Mentioning, quoting or naming the bot in a group with "shut up" or "stop" is caught by the pre-send review, which opens a 10-minute window for that group: the rating is skipped and in-flight unprompted replies are dropped. Mentions, quotes and names are still answered — "stop" means "stop butting in", not "ignore me".

02 Reply flow

After deciding to answer

replyAndRecord runs replyTo up to 3 times: when the same person adds a supplement or correction during generation, the old draft is discarded and regenerated with it; before the third attempt the turn is sealed against further merges.

  1. Prepare

    Current images, history, identity aliasing, request timeout, the typing indicator (Telegram and OneBot private chats).

  2. Owner or status command? Only a resolvable link?
    Commandsent directly, no reviewLinkresolved only, deduplicated by resource
  3. Plugins

    May start background tasks or answer directly.

    Direct plugin replyreviewed and sent without the chat model
  4. Assemble context and tools

    Prompt order below. Reply rules can pin this turn's model profile or turn the reply into voice.

  5. Agent on and allowed tools for this relationship?
    Yesagent runNoone model call; images or audio use the vision group
  6. Shape the reply

    Take out control markers; stop tool calls written as prose; convert to plain text where the platform has no rich text; split long replies at natural boundaries, then split code blocks, then compress with a model (30 seconds, at most 2 calls per reply).

  7. Pre-send review
    Stoppedthe matching ignored_*
  8. Send in chunks

    Next section.

    A supplement arrivedback to Prepare, regenerate
  9. Record

    History, message IDs, delivery stages, outcome; chat cooldown starts; relationship evaluation is queued.

Prompt order

System head (SOUL.md, fixed rules, tool rules) → summary checkpoint → stable history → cache breakpoint → per-turn blocks (plugin context, session state, memory, notebook, world book, self-notes, media index, recent sources, recent tool calls) → same-turn candidates → dependency images → system tail (speaker) → clock → decoration rules → current message. Everything before the breakpoint stays the same between turns in a group, so upstream prefix caches hit. Real user IDs and group numbers are replaced by aliases in model requests and restored in replies and tool calls.

When generation comes back empty, markers decide the fallback: a pause sends one in-character line and goes quiet; a refusal sends fixed text; a queued image job turns "started generating" into the reply; if say already spoke this turn it stays silent; otherwise "I didn't produce a usable reply."

02 Reply flow

Sending in chunks

  1. Split

    On [diana-msg] / [diana-line], trailing full stops removed; replies in the same session queue instead of interleaving.

  2. OneBot and over the forward threshold?
    Yessend a merged-forward card (its text gets its own account-safety check); if that check stops it nothing is sent, other failures before the timeout fall back to chunks
  3. Chunk by chunk

    Only the first chunk quotes and mentions; no quote if the trigger was recalled or the reply targets another participant.

  4. Pacing

    From the second chunk: max(300 ms interval, 100 ms per character), capped at 6 seconds, with the typing indicator kept on.

  5. Single exit: sendOutgoingWithResult

    Mask secrets → prepare local images and audio → resolve [diana-reply] / [diana-at] → send-time guards.

  6. Already sent this step?

    Fingerprints are kept in the turn's delivery ledger.

    Yesskip, reuse the message ID
  7. Lease the media share link

    Renewed to at least ten minutes ahead before every retry.

  8. executeOutboundCall
    Kicked / mutedgive upConnection offlinefail fast; the queue retries the whole turn laterGroup send failsseparate text and media lanes back off exponentially, then a drop cooldown
  9. Success

    Record Acknowledged, write chat history, keep the message ID, pause the typing indicator.

A chat reply is attempted once per call; platform media-validation rejections retry briefly, at most 3 attempts in all. send_retry_attempts is for non-reply sends such as scheduled pushes. When the queue retries a whole turn, chunks already delivered are skipped via the ledger and never sent twice.

03 Agent run

One thing per step

With the agent on and tools allowed for this relationship, Runner.Run produces the turn: every step the model either calls one tool or finishes with agent_finalize.

  1. Configure

    20 steps by default (max 32); the owner gets 32 steps and 20,000 characters of final reply, everyone else 8,000. 60 seconds per tool, 20 seconds reserved for finishing, 3 protocol repairs.

  2. Assemble tools

    Core tools are always callable; the rest appear as one catalog line and need tools_load, then tools_execute. Trimmed by the permission allowlist and bot / group extension switches; a tool without permission answers "no permission" instead of "does not exist". In groups, loaded tools are restored from the group session snapshot on later turns.

  3. Runner.Run starts

    Protocol prompt and deferred-tool catalog; Skills injected by trigger words; a trusted clock; the carry-over archive if the last turn was forced to finish.

  4. Steps left, and enough time to finish?
    Noforced finish
  5. Call the model

    Planning deadline = request deadline − finish reserve (the configured value or a third of the remaining time, whichever is smaller).

    Planning deadline hitforced finishOther errorhandled by the reply flowPlain text / agent_finalizefinish checks
  6. Run exactly one tool call

    Other calls in the batch get "not executed", so every step sees the previous result.

  7. Pre-execution checks

    Deferred tools must be loaded and pass schema checks → unknown or denied → input coercion → extension changes need a confirmation code → identical to the previous call is skipped → quotas (web_search 3; introspection 6 and tools_load 4 without using steps).

    Failsone protocol repair, back to the step check; forced finish when repairs run out
  8. Run the tool

    Per-tool timeout and output budget; run_command runs in a sandbox as configured; MCP has its own 60-second timeout.

  9. Shape the result

    Secrets masked, long output truncated; search and render results go into the evidence ledger; remaining budget attached. Tool errors are not fatal — they become observations for the model.

    Next stepback to the step check

Finish checks

  1. silent = true?
    Yes, no image job queuedno reply this turnImage job queuedsilence refused, one repair
  2. Real line breaks in the text?

    Chunks must use [diana-msg] / [diana-line].

    Yesone repair
  3. Evidence required but no search? A link never fetched this run nor seen in the conversation?
    Yesone repair
  4. Internal protocol words leaked? Empty text?
    Yesone repair
  5. Return the reply

A forced finish happens when steps or repairs run out, the finish reserve is reached, or evidence was required and not gathered: a finish instruction and the evidence digest are appended, and only agent_finalize is offered, forced. Afterwards a forced finish keeps up to 12 step summaries as a carry-over archive (valid 10 minutes); the last 8 tool calls are kept for 30 minutes and injected into later turns.

Interim say

At most 3 per turn, 80 characters each, no line breaks; sent immediately and the typing indicator comes back. After it, the turn is no longer merged and regenerated, and it skips pre-send review.

subtask

A synchronous model call with no tools and no history: at most 4 per reply, 6,000 characters of material, 90-second timeout.

Context over budget

Summarise the longest text messages (at most 2 calls), replace images with descriptions, downsize images, and finally drop them.

Model errors

The runner does not retry. The provider stack retries a transient error once after 700 ms, halves an oversized context up to 4 times, then fails over to the next profile in order, keeping completed tool steps.

04 Browsers

Which browser this turn uses

The disposable headless renderer browser_render is registered for everyone, always. An interactive browser with login state is owner-only, and only one set is registered per turn, so the model never gets two and guesses.

  1. Is the sender the owner?
    Noonly browser_render
  2. Pick a source by priority

    browsersource.Pick: built-in first, extension second by default; the first usable one wins.

  3. Built-in: enabled and Chrome found

    1. Built-in browser turned off for this bot?

      agent_browser_box_disabled

      Yesbrowser_* point at the external CDP address instead
    2. This bot's built-in browser

      Registers browser_*.

    Extension: enabled, connected, not taken over

    1. Extension control on for this bot?

      agent_browser_control_enabled, off by default.

      Nosame as "none usable" — no fallback to built-in
    2. Your own Chrome

      Registers browser_ext_*.

    None usable

    1. Custom external CDP address on this bot?

      Anything other than the default 127.0.0.1:9222.

      Nono interactive browser
    2. That CDP address

      Registers browser_*.

A taken-over built-in browser does not switch to the extension

The built-in browser counts as usable whenever it is enabled and Chrome is found; during a takeover browser_* calls fail with "the user is controlling the built-in browser". The extension is the opposite: when every connection is taken over it counts as unusable and the next source is tried.

04 Browsers

Disposable headless rendering

  1. browser_render or the web-render plugin

    The plugin renders at most 2 links per message and injects the text as an untrusted block.

  2. Public address check

    http/https only; private, loopback, link-local, localhost and .local addresses are refused.

    FailsrefusedGitHub /releases linkthe official API first
  3. Take a slot

    3 at once by default, queueing at most 20 seconds; queue time does not count against the timeout.

    No slottoo many disposable browsers running
  4. Start Chrome

    A temporary 0700 profile, HOME / XDG redirected into it, local host names blocked in resolution.

  5. Navigate

    Every main-document request is re-validated; a download aborts the render.

  6. Wait until the page settles

    DOM, network and timers quiet; 25 seconds by default, 60 at most.

    Timeoutuse the last non-empty snapshot; error only if there is nothing
  7. Return URL, title, text

    The process ends and the temporary directory is deleted.

  • Only main-document requests are intercepted and re-checked; sub-resources such as XHR and images are not validated one by one.
  • HTML to image: simple pages use Chrome --screenshot (8 MiB output cap, takes a slot); scripted or animated pages use html_capture on a fake diana-render.invalid origin where every request is answered locally or blocked, with a strict CSP and a virtual clock, at most 600 frames and 4096 px — it does not take a slot today.
  • Finding Chrome: configured path → DIANA_HEADLESS_BROWSER_EXECUTABLE → DIANA_AGENT_BROWSER_EXECUTABLE → common install locations (including a downloaded Chrome for Testing) → PATH. Installation tries the system package manager first; on glibc Linux it falls back to downloading Chrome for Testing into DIANA_BROWSER_DIR.

04 Browsers

Interactive browsers

  1. browser_open

    Only checks for http/https or about:blank, plus the built-in browser's denied_hosts.

  2. This conversation has a current tab and no new_tab?
    Yesreuse it
  3. Already 8 tabs opened by the bot?

    A tab counts as held by a conversation for 10 minutes after use.

    Some are freeclose the least recently usedAll heldrefused; use the current tab or close one
  4. Navigate

    Timed by agent_browser_timeout_ms (15 seconds by default): on timeout stopLoading and an error; a body that never finishes is stopped after 8 seconds.

  5. Actions

    click / type / key / wait / eval / screenshot…; clicks and keys are real input events, falling back to el.click() when covered; cookie values in browser_eval results are masked.

    Page hung or crashedterminate the script → reset to about:blank → close the tab
  6. Return the page state

    Screenshots go to .agent-browser/ in the workspace (one file per conversation, cleaned after 7 days) and are attached as images; every call writes a browser_action log.

Built-inExternal CDPExtension (your Chrome)
Toolsbrowser_*browser_*browser_ext_*
Login stateWhat you logged in there, per botThat Chrome's ownYour everyday browser's; Diana never touches cookies
Network boundaryScheme check + denied_hosts; LAN reachableScheme checkSite allow / deny lists, read-write level, rate limit
TakeoverClicking the live view takes over; returned after 5 idle minutes—Toggled in the extension or WebUI
ScreenshotsYesYesNo (it would widen extension permissions)

05 Pre-send review

Checks before anything goes out

After a reply is generated and before it is sent, a series of checks can drop, rewrite or delay it. Only one of them is a dedicated audit model call; the rest are program checks and semantic dedup.

  1. Dependency images attached, but the model says it cannot see them?
    Yes, no side effect yetregenerate once from image descriptions
  2. The model chose silence?
    Allowedignored_model_silentSide effect / factual plugin result / image queuedsilence refused, empty-reply fallback
  3. Chat reply empty or marked as a refusal?
    Yesignored_no_natural_reply
  4. Pause or refusal marker?

    [[DIANA_IGNORE_CURRENT_USER_30M]] / [[DIANA_REFUSE_CURRENT]]

    Pause, empty bodyone in-character line, then silenceRefusal, empty bodyfixed refusal text
  5. Same as a line already said with say?
    Yesnot sent twice
  6. In parallel: semantic dedup + audit model

    Their inputs are nearly the same, so starting both saves seconds; if dedup rewrites the reply, the audit runs again on the new text.

  7. Semantic dedup

    Runs only if the session replied within 2 minutes; confidence below 0.9, errors and timeouts all pass.

    drop, unprompted replyignored_duplicate_replydrop, direct replysent anyway (being addressed means you answer)rewritereshaped and formatted again
  8. Apply the audit verdict

    Next section.

    Stoppedthe matching ignored_* or a public notice
  9. Group stop in the last 10 minutes, and this is unprompted?
    Yesignored_stop_requested
  10. Into sending

    Every message then passes the send-time guards.

05 Pre-send review

The audit model

The audit is simply on or off (on by default, overridable per group). One call judges all of the following; if none applies it is skipped entirely. Verdicts apply in this order:

  1. Audit call failed?
    Unprompted replysilent: when unsure, don't butt inDirect replysent: a bot that goes mute whenever a model is down is worse
  2. Private closing / stop

    Judged only within 3 minutes of the bot's last private message; only verdicts at 0.90 confidence or higher count.

    Stopignored_stop_requested; non-owners paused 30 minutesGoodbyes past the grace countignored_conversation_closed
  3. Group stop

    Judged on group messages that mention, quote or name the bot; opens a 10-minute window, and this reply is still sent.

  4. Loops

    Bot-to-bot loop detection is on by default. An exchange with no content counts as one loop. When the other side is marked as a bot, a dense exchange with no purpose counts too. Banter from unmarked accounts does not.

    3 hits in 30 minutes (count and pause length are configurable, and temporary suppression can be turned off), not the ownerpause with a notice ignored_ai_reply_loop
  5. Account safety score < 0.10?

    Deliberately low: suspected risk still passes; only a near-certain unsafe verdict stops it.

    Yesa neutral public notice instead (can be rewritten in persona)
  6. Quality (unprompted replies only)

    Contradiction, off topic, truncated, harmful advice, or send_confidence below proactive_reply_threshold.

    Failsignored_proactive_reply_quality
  7. Pass

    A refusal (confidence ≥ 0.90) is still sent; 4 refusals within 30 minutes start a pause.

The order is deliberate: once someone says "stop replying", whether this reply is accurate no longer matters, so closing comes first. The quality gate only applies to unprompted replies — going silent on someone who addressed you because of a score looks like ignoring them. Direct plugin replies go through the same audit but not semantic dedup.

Send-time guards

GuardConditionResult
Secret maskingA registered credential appears in the textReplaced with a masked value
Mention and quote markers[diana-at] / [diana-reply] malformed or pointing at a missing messageRewritten or removed
Group unavailableConfirmed left or disabledignored_unavailable_group
PausedWithin a pause for this personignored_response_suppression
SupplementThe person added a message that was merged into this turnThis draft is discarded and regenerated
SupersededSame person addressed the bot again in a group; merged into another turn; unprompted batch changedsuperseded_follow_up / superseded_reply_turn / superseded_proactive
IdempotencyThis step was already sentSkipped, message ID reused
Mute / offline / backoffMuted; connection offline; group backoffMuted drops; offline retries via the queue; backoff waits, then drops

Interrupts and supersession only apply before the first chunk is sent and before any external side effect; a reply that has started sending finishes. Identity aliasing is not a send-time check: real IDs are replaced before the request reaches the model.

Troubleshooting

Which stage an outcome code comes from

Every message, answered or not, ends with a record: decision is the outcome code and decision_reason the human-readable reason. When troubleshooting, find which stage it stopped at, then go back to that section.

StageOutcome codes
Inbound queueignored_stale backfill_history_only ignored_private_admission dropped_retries_exhausted
Deterministic gatesignored_policy ignored_user_blocked ignored_bot_muted ignored_model_quota ignored_member_level ignored_response_suppression ignored_video ignored_bot_message ignored_reply_fatigue
Reply ratingignored replied_proactive
Concurrencymerged_into_reply merged_into_backlog_turn superseded_follow_up superseded_reply_turn superseded_proactive
Pre-send reviewignored_model_silent ignored_no_natural_reply ignored_duplicate_reply ignored_proactive_reply_quality ignored_stop_requested ignored_conversation_closed ignored_ai_reply_loop
Sendingignored_unavailable_group dropped_send_rejected

Where the code lives

AreaFiles
Inbound and backfillmodel/assistant/runtime_inbound.go inbound_queue.go inbound_gap.go inbound_retry.go inbound_backlog_merge.go
Gates and ratingmodel/assistant/runtime.go (routeMessageEvent, routeProactiveReplyBatch) participation_single_score.go direct_reply_merge.go reply_interrupt.go group_stop.go
Reply and sendingmodel/assistant/runtime.go (replyTo, sendDecorated) reply_compression.go runtime_outbound.go group_send_guard.go outbound_idempotency.go
Agentmodel/agent/runner.go finalize.go deferred_tools.go claim_evidence.go; model/assistant/agent_scope.go
Browsersmodel/browsersource/ model/browserbox/ model/browserctl/ model/agent/headless_browser*.go browser_tools.go html_capture.go
Pre-send reviewmodel/assistant/proactive_reply_quality.go reply_semantic_gate.go private_closing.go reply_suppression.go outbound_forward_gate.go
Outcome recordsmodel/storage/inbound_queue.go event_audit.go