Skip to content

02.6: Participants

The densest single contributor: list every conversation participant with per-user details (presence, roles, personal memories, reminders), mention aliases with conflict detection, and the closing channel/time-of-day footer.

File: src/utils/text/context/participants.ts:38-236

For every user ID in userList (collected from message history by the chat pipeline), emit a rich detail block: display name, mention aliases (unique-resolution computed), online/presence status, server roles, per-user personal memories (with tag-filtering against the conversation corpus, like server memories in stage 03), pending reminders, and public Physical Appearance tags for image generation. Then fold in Matrix bridge users and synthetic webhook users (persona-flavored). Close with channel name + current time-of-day (timezone-aware).

The output is one context item — all participants live in a single [System: The following users are having a conversation: ...] block.

Substantial — see signature in participants.ts:38-58. Notable:

  • userList: string[] — Discord user IDs from history
  • triggererName, botName, personaLineageId
  • tomoriState, tomoriConfig (provides personal_memories_enabled, timezone_offset)
  • isDMChannel, isUserImpersonation, impersonatedUserId, impersonatedIdentityName
  • matrixUsers: Map<string, string> — Matrix user ID → stripped display name
  • syntheticUsers: Map<string, { displayName, type: "persona" | "webhook" }>
  • conversationCorpus — for personal-memory tag filtering
  • snapshot, convertMentions

Promise<StructuredContextItem | null>null if userList is empty, otherwise one user-role item tagged KNOWLEDGE_USERS_IN_CONVERSATION.

Also populates conversationUsers: ConversationUserReference[] on the context item — a structured list used downstream for mention resolution by the streaming pipeline.

Content shape:

[System: The following users are having a conversation:
If {botName} wants to ping any of these users, prepend an "@" symbol to a unique
mention handle shown below (case-insensitive). [...]
{botName} (This is you!)
- Status: Online - Currently active and responding to messages
- Physical Appearance: blue hair, red eyes, white hoodie
UserA (Mention: @{UserA}; Aliases: @{aliceA}, @{alice_global})
- Physical Appearance: short white hair, red eyes
- Status: Online - Playing Stardew Valley
- Server Roles: Mod, Member
- Memories: [id:42] Likes cats (tags: pets, animals)
- Reminders:
- ID:42 "Take meds" (scheduled for Tue, May 21, 2026 10:00 AM (UTC-7), repeats every 24 hour(s))
Conversation context: #general (ID: 1234...).
Current time: May 21, 2026 18:30 UTC+09:00 (JST), evening.
]
  • DB / cache reads (per user):
    • userRepository.loadByDiscordId(userId) — load or null
    • If missing and the user is in the guild: userRepository.register(...) auto-registers them
    • userRepository.isBlacklisted (cached via userCache)
    • userRepository.getPrivacyLevel (cached)
    • personalMemoryRepository.loadForUserLineage if eligible
    • serverScheduleRepository.getPendingRemindersForUser for each user; pending reminders include ID:N so the LLM can target them with update_task for requester-scoped edits/deletes
  • Discord fetches:
    • guild.members.fetch(userId) for role / display-name resolution
    • client.users.fetch(userId) fallback for users not in guild
    • getUserPresenceDetails for online status + activities (requires GuildPresences intent)
  • Synthetic persona enrichmentgetCachedAllPersonas to populate image-appearance tags for synthetic persona entries.
  • Mention alias collection — addresses, server nicknames, global names, usernames, and custom nicknames are collected per user; aliasCounts tracks duplicates across users to detect conflicts.
  • Final mention conversion — assembled text passes through convertMentions.

After this stage runs:

  • Returns null only when userList is empty.
  • Every user entry has a displayName (falls back to <@id> for missing data).
  • Mention aliases marked as unique are exactly those that appear once across aliasCounts — duplicates are silently dropped from the mention handle list (the LLM is told “mention requires clarification” instead).
  • Each entry’s aliases (server nickname, global name, username, custom nickname) plus its displayLabel are emitted as conversationUsers metadata for tool-side user resolution (resolveUserTarget). The conversation stage of that resolver matches input against the full alias set, but breaks ties by preferring a single candidate whose displayLabel (primary name) equals the input over candidates that only matched a secondary alias — so one user’s server-nickname alias colliding with another user’s actual name no longer forces a needless clarify round-trip.
  • Personal memories are filtered by privacy (PrivacyLevel.MINIMAL required) AND blacklist AND personal_memories_enabled AND conversation-corpus tag match (if memory_tagging_enabled).
  • Physical Appearance tags are public for users/personas present in chat and remain separate from public attributes.
  • Matrix and synthetic users are appended after normal users and are marked non-mentionable (mentionable: false).
  • The closing footer always emits, even with one participant.
SourceFieldEffect
tomoriConfigpersonal_memories_enabledMaster switch for per-user memories + nickname usage
tomoriConfigmemory_tagging_enabled(Set upstream in nativeBuilder) Drives conversationCorpus tag filter for personal memories
tomoriConfigtimezone_offsetHours offset for current-time footer
Client intentGuildPresencesRequired for online/activity status; without it, only static info is shown
User rowpersonal_dtm, privacy_levelPrivacy controls — FULL users are skipped entirely from listing
User rowphysical_appearance_tagsPublic physical appearance image tags

This is the single richest plugin surface in the context-build pipeline. Multiple plugin-relevant seams:

SurfacePlugin-relevance
Personal memories per user (personalMemoryRepository.loadForUserLineage)The “personal memory type” plugin category — a sister to server memories (stage 03).
Matrix-user folding (matrixUsers map)A bridge plugin emits its users via this map; the contributor handles them uniformly. A Telegram/Slack bridge plugin would extend the same map.
Synthetic users (persona / webhook)The chat pipeline pre-populates syntheticUsers; a plugin shipping a new “fake participant” type would extend the map.
Pending reminders (serverScheduleRepository.getPendingRemindersForUser)Reminder system is core, not plugin — but a plugin adding “scheduled events” might want a parallel display block here. → plugin plan candidate.
Physical Appearance tags (physical_appearance_tags)Coupled to image-generation tooling; a plugin adding a different image-gen tag scheme would extend the normalizeImageAppearanceTags path.
Channel + time-of-day footerInternal — coupled to timezoneHelper.

A plugin extension for “alternate participant rendering” (e.g. collapse-when-many-users, show-roles-only-for-mods) would either:

  • (a) Wrap this contributor with a post-processor on the emitted text. Brittle — text format is not a contract.
  • (b) Replace this contributor entirely with the plugin’s own. Cleaner — if a “register contributor” mechanism is built. → plugin plan candidate.
  • Server memories (parallel): 03-server-memories.md
  • User presence (helper, history.ts): covered in native-assembly README.
  • Display-name resolution: → no dedicated doc; src/utils/discord/displayName.ts helper only
  • Reminder system: → no dedicated doc; serverScheduleRepository API only
  • Image-generation Physical Appearance tags: → no dedicated doc; physical_appearance_tags is documented inline in the persona/user schemas