fix(ingestion): resolve Slack authors by real_name, not display_name - #580
Conversation
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>
There was a problem hiding this comment.
-
Warning —
ingestion/src/zep_ingest/loaders/slack.py,_user_map()hunk around new line 408: users lackingreal_nameare only marked weak whendisplay_nameis a single token. Multiword display names are explicitly excluded from warnings, contradicting the README/changelog guarantee that authors withoutreal_nameare reported. This can silently retain aliases such as “Morgan L” that do not merge with canonical names. Mark every fallback fromreal_nameas weak, or narrow the documented guarantee. -
Warning —
ingestion/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 withusers.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.
… 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>
There was a problem hiding this comment.
- Critical —
ingestion/src/zep_ingest/loaders/slack.py,_user_map()hunk: users withoutreal_nameare only marked weak when their fallback name contains no whitespace. Thus a roster entry withdisplay_name: "Morgan Lee"and noreal_nameproduces no warning, contradicting the README/changelog guarantee that all authors lackingreal_nameare reported. The added test explicitly enshrines this incorrect behavior. Mark every fallback from a missingreal_nameas weak, or revise the documented contract and warning wording to describe the narrower heuristic.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ 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.
…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>
There was a problem hiding this comment.
- Warning —
ingestion/src/zep_ingest/loaders/slack.py,_user_map()hunk around new line 425: Users withoutreal_nameare marked weak only whendisplay_namecontains no whitespace. Thus a roster entry withdisplay_name: "Morgan Lee"and noreal_nameis omitted fromwarnings, contradicting the README/changelog guarantee that all authors lackingreal_nameare reported. Either mark every fallback from missingreal_nameas weak, or narrow the documentation and warning semantics to describe the handle heuristic accurately.
Slack messages store author ids, so speakers,
@mentions, and DM labels are resolved through the export'susers.jsonroster — but that lookup preferredprofile.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'asPersonnodes, where it now produces one. This change prefersreal_name(thendisplay_name, then the username, then the raw id), reports ingested authors whose roster entry has noreal_name, and exposesSlackMessage.user_idsoformatter=can substitute names from a directory of your own when the roster is thin. Both fixtures previously wrotedisplay_nameso 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 fromusers.jsonwithprofile.real_namebeforedisplay_name(then username, then raw id), so short handles likemorgando not land in the graph when a full name exists elsewhere.The loader adds
SlackMessage.user_idfor customformatter=callbacks, andresult.warningswhen ingested text actually uses a weak label (noreal_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.