docs/diagrams/message-event-flow.mmd

sequenceDiagram autonumber participant Discord as Discord participant Bot as ClankerBot participant Store as Store participant Memory as MemoryManager participant Search as Search/Web runtime participant LLM as LLMService

Discord->>Bot: messageCreate(message)
Bot->>Store: recordMessage(incoming)
Bot->>Store: getSettings()
Bot->>Bot: channel/user/bot guards

alt memory enabled
  Bot->>Memory: ingestMessage()
  Memory->>Memory: append entry to memory/YYYY-MM-DD.md
  Memory->>LLM: embed message for conversation recall (best effort)
  LLM->>Store: upsert message_vectors_native
  Memory->>Memory: queue MEMORY.md refresh + text micro-reflection
end

Bot->>Bot: getReplyAddressSignal()
Bot->>Bot: evaluateReplyAdmissionDecision()
Bot->>Store: logAction(text_runtime reply_admission_decision)

alt admitted for model turn
  Bot->>Bot: enqueueReplyJob()
  Bot->>Memory: loadConversationContinuityContext()
  Bot->>LLM: generate() structured reply + tool plan
  LLM->>Store: logAction(llm_call or llm_error)
  loop bounded reply tool loop
    opt model emits tool call(s)
      alt web_search / web_scrape
        Bot->>Search: execute search or page read
        Search->>Store: logAction(search_call or search_error)
      else memory_search
        Bot->>Memory: searchDurableFacts()
      else image_lookup
        Bot->>Store: getRecentMessages() for image candidates
      else other reply tool
        Bot->>Bot: execute tool and collect result
      end
      Bot->>LLM: generate() continuation from tool results
      LLM->>Store: logAction(llm_call or llm_error)
    end
  end

  opt structured output requests reaction
    LLM->>Store: logAction(llm_call or llm_error)
    Bot->>Discord: message.react(emoji)
    Bot->>Store: logAction(reacted)
  end

  Bot->>Discord: reply() or send()
  Bot->>Store: recordMessage(outgoing)
  Bot->>Store: logAction(sent_reply or sent_message)
else not admitted
  Bot-->>Discord: no reply
end