Skip to content

feat: automatically record UMO names - #9909

Merged
Soulter merged 4 commits into
masterfrom
codex/auto-umo-aliases
Sep 1, 2026
Merged

feat: automatically record UMO names#9909
Soulter merged 4 commits into
masterfrom
codex/auto-umo-aliases

Conversation

@Soulter

@Soulter Soulter commented Sep 1, 2026

Copy link
Copy Markdown
Member

Summary

  • automatically persist group names and private sender names from events that wake AstrBot
  • record immediately in WakingCheckStage instead of waiting for the complete pipeline
  • keep WakingCheckStage concise by encapsulating cache and writer state in UmoAutoNameRecorder
  • use a bounded 10,000-entry LRU and a coalescing background writer per pipeline configuration
  • keep EventBus focused on dispatching scheduler tasks
  • use atomic SQLite upserts that preserve manual aliases and skip unchanged updates
  • keep Discord DMs on the sender-name path by avoiding a synthetic group for private channels

Behavior

  • ignored ambient messages do not create database rows or occupy the auto-name cache
  • group messages use group_name; private messages use sender_name
  • missing names are skipped instead of falling back to IDs
  • manual user_alias values always take precedence and are never overwritten by automatic updates
  • a changed name is synchronized on the next event that wakes the bot

Validation

  • uv run pytest -q tests/unit/test_event_bus.py tests/unit/test_waking_check_api_key_admin.py tests/unit/test_waking_check_umo_alias.py tests/test_umo_alias.py tests/test_discord_adapter.py tests/unit/test_core_lifecycle.py (74 passed)
  • uv run ruff format .
  • uv run ruff check .
  • broader uv run pytest -q tests: 2306 passed; 2 existing Dashboard log-capture tests failed because the expected warning was written to captured stdout instead of caplog

Summary by Sourcery

Automatically persist platform-provided UMO names when messages wake AstrBot while preserving manual aliases and keeping private-message metadata accurate.

New Features:

  • Automatically record group and private-sender display names for events that awaken AstrBot.

Bug Fixes:

  • Keep private Discord messages on the sender-name path instead of assigning synthetic group metadata.

Enhancements:

  • Persist discovered names immediately from the waking stage using bounded caching and coalesced background writes.
  • Preserve manual aliases while applying only changed automatic names through atomic database upserts.
  • Skip ambient events and records without usable platform-provided names.

Tests:

  • Add coverage for waking-stage filtering, name coalescing, cache bounds, retry behavior, non-blocking writes, alias preservation, and Discord private-message metadata.

Chores:

  • Remove the redundant explicit UMO alias index from initialized SQLite schemas.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/db/sqlite.py" line_range="76-78" />
<code_context>
             await self._ensure_platform_message_history_checkpoint_column(conn)
             await self._ensure_chatui_project_workspace_columns(conn)
             await self._ensure_conversation_indexes(conn)
+            # The table-level unique constraint already provides an index for UMO
+            # lookups. Older schemas also created this redundant explicit index.
+            await conn.execute(text("DROP INDEX IF EXISTS ix_umo_aliases_umo"))
             await conn.commit()

</code_context>
<issue_to_address>
**issue (broader_impact):** The initialization migration drops `ix_umo_aliases_umo` without creating the new table-level unique constraint on existing databases. For databases created by the previous schema, that explicit index can be the only uniqueness enforcement for `umo`; after it is dropped, the new `ON CONFLICT (umo)` upserts fail because SQLite has no matching unique constraint, and duplicate UMO rows can be inserted by other write paths.

**Triggers:** When upgrading an existing database whose `umo_aliases` table was created by the previous schema.

**Suggested fix:** Create or migrate the `uix_umo_alias_umo` unique constraint before dropping the legacy index, and only remove the old index after verifying that the constraint exists.
</issue_to_address>

### Comment 2
<location path="astrbot/core/umo_alias.py" line_range="24-42" />
<code_context>


-def get_event_auto_name(event: Any) -> str:
+def get_event_auto_name(event: Any, *, fallback_to_id: bool = True) -> str:
+    """Resolve an automatic display name from inbound event metadata.
+
+    Args:
+        event: Platform event containing group and sender metadata.
+        fallback_to_id: Whether to use the group or sender ID when no name exists.
+
+    Returns:
+        Normalized group or sender name, an optional ID fallback, or an empty string.
+    """
     group_id = event.get_group_id() if hasattr(event, "get_group_id") else ""
     message_obj = getattr(event, "message_obj", None)
     group = getattr(message_obj, "group", None)

     if group_id:
         group_name = normalize_umo_name(getattr(group, "group_name", None))
-        return group_name or normalize_umo_name(group_id)
+        if group_name:
+            return group_name
+        return normalize_umo_name(group_id) if fallback_to_id else ""

     sender_name = ""
</code_context>
<issue_to_address>
**issue (bug_risk):** The helper treats any non-empty `event.get_group_id()` as proof that the event is a group message, so private events from adapters that populate `group_id` with a conversation or channel ID enter the group branch and never inspect `sender_name`. Those private messages therefore skip automatic naming or receive a group ID fallback instead of the private sender name.

**Triggers:** When a platform represents a private conversation with a non-empty group or conversation ID.

**Suggested fix:** Use the event/message type to select the group-name branch, and use `sender_name` for `FRIEND_MESSAGE` events regardless of whether `group_id` is populated.
</issue_to_address>

### Comment 3
<location path="astrbot/core/event_bus.py" line_range="80-82" />
<code_context>
+        auto_name = get_event_auto_name(event, fallback_to_id=False)
+        if not auto_name:
+            return
+        if self._umo_auto_name_cache.get(umo) == auto_name:
+            self._umo_auto_name_cache.move_to_end(umo)
+            return
+
+        self._umo_auto_name_cache[umo] = auto_name
</code_context>
<issue_to_address>
**issue (bug_risk):** An in-flight write is represented only by the optimistic cache entry. If the writer pops an entry, a second event with the same UMO and name arrives while the database call is failing, that event is skipped because the cache still matches; the failure then removes the cache entry and no pending write remains, so the second event does not trigger a retry.

**Triggers:** When the database write fails while another event for the same UMO and unchanged automatic name is dispatched concurrently.

**Suggested fix:** Track in-flight names separately or requeue the failed item unless a newer successfully queued value supersedes it.

```suggestion
        if (
            self._umo_auto_name_cache.get(umo) == auto_name
            and umo in self._pending_umo_auto_names
        ):
            self._umo_auto_name_cache.move_to_end(umo)
            return
```
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 3 findings to address first, and this writes automatically discovered names into persistent UMO records and removes an existing SQLite index, so an incorrect name or schema assumption can survive a code revert. The names can be recomputed and records repaired, but dropping the index could also require a database fix if the expected uniqueness constraint is not present.

Blocking findings: astrbot/core/db/sqlite.py:78, astrbot/core/umo_alias.py:42, astrbot/core/event_bus.py:82


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread astrbot/core/db/sqlite.py
Comment on lines +76 to +78
# The table-level unique constraint already provides an index for UMO
# lookups. Older schemas also created this redundant explicit index.
await conn.execute(text("DROP INDEX IF EXISTS ix_umo_aliases_umo"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (broader_impact): The initialization migration drops ix_umo_aliases_umo without creating the new table-level unique constraint on existing databases. For databases created by the previous schema, that explicit index can be the only uniqueness enforcement for umo; after it is dropped, the new ON CONFLICT (umo) upserts fail because SQLite has no matching unique constraint, and duplicate UMO rows can be inserted by other write paths.

Triggers: When upgrading an existing database whose umo_aliases table was created by the previous schema.

Suggested fix: Create or migrate the uix_umo_alias_umo unique constraint before dropping the legacy index, and only remove the old index after verifying that the constraint exists.

Comment thread astrbot/core/umo_alias.py
Comment on lines +24 to +42
def get_event_auto_name(event: Any, *, fallback_to_id: bool = True) -> str:
"""Resolve an automatic display name from inbound event metadata.

Args:
event: Platform event containing group and sender metadata.
fallback_to_id: Whether to use the group or sender ID when no name exists.

Returns:
Normalized group or sender name, an optional ID fallback, or an empty string.
"""
group_id = event.get_group_id() if hasattr(event, "get_group_id") else ""
message_obj = getattr(event, "message_obj", None)
group = getattr(message_obj, "group", None)

if group_id:
group_name = normalize_umo_name(getattr(group, "group_name", None))
return group_name or normalize_umo_name(group_id)
if group_name:
return group_name
return normalize_umo_name(group_id) if fallback_to_id else ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The helper treats any non-empty event.get_group_id() as proof that the event is a group message, so private events from adapters that populate group_id with a conversation or channel ID enter the group branch and never inspect sender_name. Those private messages therefore skip automatic naming or receive a group ID fallback instead of the private sender name.

Triggers: When a platform represents a private conversation with a non-empty group or conversation ID.

Suggested fix: Use the event/message type to select the group-name branch, and use sender_name for FRIEND_MESSAGE events regardless of whether group_id is populated.

Comment thread astrbot/core/event_bus.py Outdated
Comment on lines +80 to +82
if self._umo_auto_name_cache.get(umo) == auto_name:
self._umo_auto_name_cache.move_to_end(umo)
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): An in-flight write is represented only by the optimistic cache entry. If the writer pops an entry, a second event with the same UMO and name arrives while the database call is failing, that event is skipped because the cache still matches; the failure then removes the cache entry and no pending write remains, so the second event does not trigger a retry.

Triggers: When the database write fails while another event for the same UMO and unchanged automatic name is dispatched concurrently.

Suggested fix: Track in-flight names separately or requeue the failed item unless a newer successfully queued value supersedes it.

Suggested change
if self._umo_auto_name_cache.get(umo) == auto_name:
self._umo_auto_name_cache.move_to_end(umo)
return
if (
self._umo_auto_name_cache.get(umo) == auto_name
and umo in self._pending_umo_auto_names
):
self._umo_auto_name_cache.move_to_end(umo)
return

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 1, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
astrbot-docs bfc1789 Commit Preview URL

Branch Preview URL
Sep 01 2026, 03:22 PM

@Soulter
Soulter merged commit f996f3f into master Sep 1, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant