Skip to content

fix(ingestion): resolve Slack authors by real_name, not display_name - #580

Merged
jackaldenryan merged 3 commits into
mainfrom
jack/create-pr
Jul 27, 2026
Merged

fix(ingestion): resolve Slack authors by real_name, not display_name#580
jackaldenryan merged 3 commits into
mainfrom
jack/create-pr

Conversation

@jackaldenryan

@jackaldenryan jackaldenryan commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Slack messages store author ids, so speakers, @mentions, and DM labels are resolved through the export's users.json roster — but that lookup preferred profile.display_name, which in a real export is frequently a short handle like "morgan". Zep merges entities by the names it sees in text, so a handle never merges with the same person written in full elsewhere: verified against a live graph, ingesting a handle-based export alongside full-name documents produced both 'morgan' and 'Morgan Lee' as Person nodes, where it now produces one. This change prefers real_name (then display_name, then the username, then the raw id), reports ingested authors whose roster entry has no real_name, and exposes SlackMessage.user_id so formatter= can substitute names from a directory of your own when the roster is thin. Both fixtures previously wrote display_name so it could never behave like a display name — the example roster left it empty and the test roster put a full name in it — which is why this went unnoticed; they now carry realistic handles, making them a regression guard, so reverting only the loader change fails nine tests.

🤖 Generated with Claude Code


Note

Medium Risk
Changes how all Slack speaker/mention/DM strings are labeled (behavior change for existing exports), but scope is limited to the ingestion loader with broad test coverage and no auth or API surface changes.

Overview
Slack export ingestion now resolves speakers, @mentions, and DM member labels from users.json with profile.real_name before display_name (then username, then raw id), so short handles like morgan do not land in the graph when a full name exists elsewhere.

The loader adds SlackMessage.user_id for custom formatter= callbacks, and result.warnings when ingested text actually uses a weak label (no real_name, handle-shaped display name, or username)—including DM labels and mentions, but only for messages/conversations that are kept (skipped/duplicate/invalid messages do not inflate warnings). Docs, changelog, example/fixture rosters, and regression tests were updated for realistic handles.

Reviewed by Cursor Bugbot for commit f1f483c. Bugbot is set up for automated code reviews on this repo. Configure here.

Slack messages store author ids, so every speaker, @mention, and DM label
is resolved through the export's users.json roster. That lookup preferred
profile.display_name, which in a real export is frequently a short handle
("morgan"). Zep merges entities by the names it sees in text, so a handle
never merges with the same person written in full ("Morgan Lee") in an
email or document: one person silently becomes two nodes, and half their
facts hang off each. Verified against a live graph — ingesting a
handle-based export alongside full-name documents produced both 'morgan'
and 'Morgan Lee' as Person nodes; after this change it produces one.

Prefer real_name, then display_name, then the username, then the raw id.
Slack's own precedence is the opposite, but it optimizes for how a name
reads in a chat client, not for entity resolution.

Report authors whose roster entry has no real_name, counting only those
whose content was actually ingested (including via an @mention) so a
handle-only user who never posted is not noise. Expose the raw Slack id
as SlackMessage.user_id so formatter= can substitute names from a
directory of your own when the roster is thin.

Both fixtures previously wrote display_name so it could never behave like
a display name — the example roster left it empty and the test roster put
a full name in it — which is why this went unnoticed. They now carry
realistic handles, making the fixtures a regression guard: reverting only
the loader change fails nine tests.

Co-Authored-By: Claude <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  • Warningingestion/src/zep_ingest/loaders/slack.py, _user_map() hunk around new line 408: users lacking real_name are only marked weak when display_name is a single token. Multiword display names are explicitly excluded from warnings, contradicting the README/changelog guarantee that authors without real_name are reported. This can silently retain aliases such as “Morgan L” that do not merge with canonical names. Mark every fallback from real_name as weak, or narrow the documented guarantee.

  • Warningingestion/src/zep_ingest/loaders/slack.py, _resolve() hunk around new line 626: weak-name usage is recorded only through _resolve(). DM/group-DM labels are built directly with users.get() in _label(), so a weakly named participant who does not author or receive a mention still enters episode text and metadata without the promised warning. Resolve/record member names when constructing labels, while limiting reporting to selected/ingested conversations.

Comment thread ingestion/src/zep_ingest/loaders/slack.py
… label

A DM or group DM is labeled by its members, and that label is written into
every episode's text and metadata. It is built straight from the roster in
_label(), never through _resolve(), so a member whose only name is a handle
was put into the graph untracked: if they never authored a message and were
never @mentioned, _weak_names stayed empty and no warning was emitted.

Carry the roster ids a label names on _Conversation and record the weak ones
in _load_conversation, which is the first point that knows the conversation
was both selected and non-empty. Recording them in _label() instead would
warn about members of a conversation the run skipped — a private DM excluded
by the default conversation_types — and recording them on selection alone
would warn for a selected folder that yielded no episodes.

The warning now says "named in ingested content" rather than "whose content
was ingested", which no longer fits a member who never posted.

Co-Authored-By: Claude <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  • Criticalingestion/src/zep_ingest/loaders/slack.py, _user_map() hunk: users without real_name are only marked weak when their fallback name contains no whitespace. Thus a roster entry with display_name: "Morgan Lee" and no real_name produces no warning, contradicting the README/changelog guarantee that all authors lacking real_name are reported. The added test explicitly enshrines this incorrect behavior. Mark every fallback from a missing real_name as weak, or revise the documented contract and warning wording to describe the narrower heuristic.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bdc8111. Configure here.

Comment thread ingestion/src/zep_ingest/loaders/slack.py Outdated
…kept

_normalize_text resolves @mentions, and it runs before a message is known to
be usable: the ts / thread_ts checks come after it, and the duplicate-ts drop
happens later still in _load_conversation. A handle mentioned only by a
message this run threw away therefore landed in _weak_names, and the warning
claimed it was named in ingested content when nothing of it reached the graph.

Buffer weak names per message in _parse and promote them in
_load_conversation once the message is accepted. Buffering rather than
reordering _parse keeps the existing skip precedence intact — moving the ts
checks above the empty-text check would start counting _invalid_ts for
messages dropped for having no text.

_unresolved_users deliberately keeps recording immediately: it reports ids
"referenced in messages", which is true whether or not the message survived,
whereas a weak name is reported as "named in ingested content".

Co-Authored-By: Claude <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

  • Warning — ingestion/src/zep_ingest/loaders/slack.py, _user_map() hunk around new line 425: Users without real_name are marked weak only when display_name contains no whitespace. Thus a roster entry with display_name: "Morgan Lee" and no real_name is omitted from warnings, contradicting the README/changelog guarantee that all authors lacking real_name are reported. Either mark every fallback from missing real_name as weak, or narrow the documentation and warning semantics to describe the handle heuristic accurately.

@jackaldenryan
jackaldenryan merged commit 0436eb3 into main Jul 27, 2026
18 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