Skip to content

fix(chat): stop an empty localize erasing a conv's topic name - #29537

Open
chrisnojima wants to merge 3 commits into
masterfrom
nojima/HOTPOT-emoji-cut-2
Open

fix(chat): stop an empty localize erasing a conv's topic name#29537
chrisnojima wants to merge 3 commits into
masterfrom
nojima/HOTPOT-emoji-cut-2

Conversation

@chrisnojima

@chrisnojima chrisnojima commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

A conv only ever learns its topic name from its max METADATA message (localizer.go:883). A delete-history purges that message's body, so the message survives as valid-but-empty, the type switch matches nothing, and the conv localizes with no name.

Two symptoms, one cause:

  • Blank channel name wherever the conv is shown — inbox row, conversation header.
  • A team's Emoji tab never loadsFindGeneralConvFromTeamID looks #general up by topic name, and a nameless conv can never match. Same for the bot install modal, which resolves its conversation the same way.

Evidence

Reproduced against a real team whose #general had a delete-history run over it years ago. Instrumenting the localizer to dump each max msg it walks:

msgID: 1  hdrType: METADATA  valid: true  bodyType: NONE   [VALID 1 h:METADATA != b:NONE supBy:1810]

The header still says METADATA; the body unboxes to NONE, because boxer.go:955 treats an empty BodyCiphertext as a deleted body. The message that superseded it is a deletehistory.

So the purge is server-side data: every member of that team localizes the conv nameless, not just a machine with a stale cache. A healthy team's #general localizes to "general" on the same build, which is the control for the harness.

Fix

One hunk, in the localizer: if a conv localizes with no topic name and is marked IsDefaultConv, it is #general whatever its messages say.

That is deliberately the only change. An earlier revision of this PR fixed it at two other sites — MergeLocalMetadata and FindGeneralConvFromTeamID — and measuring that version showed why neither belongs:

  • A topic-name query filters on the localized Info.TopicName (inboxsource.go:60), not on the cached copy. Repairing the cache could never make the #general lookup hit; every call still fell through to the IsDefaultConv retry.
  • The cached repair only ever fixed the inbox row, leaving the conversation header blank.

Naming the conv where it is localized fixes the lookup, the cached name, and the header at once, so both of those hunks are reverted here.

Scope: this covers a conv marked IsDefaultConv. A non-default channel whose METADATA was purged still localizes nameless, deliberately — its real name is genuinely unrecoverable, so nothing should be invented for it.

Not a regression — this code path is unchanged since v6.6.3, and the topic-name filter since 2020.

Testing

Verified live: services built from master and from this branch, run in turn against the same account, with the conv's inbox cache row deleted between runs so it had to re-localize from the server.

master this branch
localized Info.TopicName "" general
presented channel name "" general
FindGeneralConvFromTeamID ERROR: Not found resolves, channel="general"
team → Emoji tab renders empty full emoji list

Also checked the upgrade path, where the cache already holds the empty name an older client wrote. A master build was left to poison the cached copy (Not found, blank tab), then the branch build was swapped in without clearing the cache: the lookup resolved as general and the Emoji tab rendered. So no cache clear or re-sync is needed on upgrade — the cached empty name is not what the lookup filters on, since that query goes remote and localizes.

Unit test added but not run locally — every test in chat and chat/storage signs up a fake user against a dev API server on localhost:3000, so the whole package fails at signup on a dev machine. CI is its first real run.

  • TestChatSrvPurgedMetadataDefaultsTopicName — blanks a channel's METADATA BodyCiphertext the way a server-side purge does, re-localizes, and requires the team's default conv to come back as #general while an ordinary channel keeps an empty name rather than getting one invented for it. It then runs the #general lookup that the emoji list and the bot install modal wait on. It pokes IsDefaultConv onto the mock conv, since kbtest never sets that flag on creation; if this goes red, that's the line to look at.

🤖 Generated with Claude Code

@chrisnojima
chrisnojima requested a review from zoom-ua August 13, 2026 15:25
A conversation's topic name is only ever known from its max METADATA
message (localizer.go:883). A delete-history purges that message's body,
so the message survives as valid-but-empty, the type switch matches
nothing, and the conv localizes with an empty name. MergeLocalMetadata
then wrote that empty string over the name already cached, unconditionally.

Two symptoms, one cause. The channel renders blank in the inbox, since
inbox rows read LocalMetadata.TopicName. And FindGeneralConvFromTeamID
looks #general up by topic name, so the lookup misses the conv for good -
which is a team's Emoji tab staying empty forever and the bot install
modal never resolving a conversation. Both were reproduced on a real
account: chat.1.local.findGeneralConvFromTeamID returned SC_NOT_FOUND
while the conv itself held 901 custom emoji.

MergeLocalMetadata now keeps the cached name when a localize comes back
empty, and falls back to the default team topic for a conv marked
IsDefaultConv, which repairs a cache an older client already poisoned.
FindGeneralConvFromTeamID retries without the topic name and picks the
conv by IsDefaultConv, so an unrepaired cache resolves immediately rather
than waiting for the next localize.
The storage test read the inbox with a topic-name query that had never
been merged, and Inbox.Read answers only queries it has seen, so it would
have returned MissError in CI rather than the conv. Merge the query first,
the way TestInboxQueries already does, and look conversations up by
convID, which skips that gate entirely.

Two branches were also asserted but unreached: the cached-name-wins arm of
the merge switch, and the lookup finding no default conv at all - the one
path where the fallback could now return a conversation where the old code
refused. Both covered. The preserved name is no longer "general", so the
test cannot pass by writing the default in the wrong branch.

Also copy the query before clearing its topic name rather than mutating
the caller's, and guard the mock conv lookup instead of dereferencing it.
Replaces the two-site fix from the previous commits with the one line that
covers both symptoms.

A conv only ever learns its topic name from its max METADATA message. A
delete-history purges that message's body, so it survives as valid-but-empty,
the type switch matches nothing, and the conv localizes with no name. The purge
is server-side data, so this happens to every member of the team - not to one
poisoned cache.

Fixing it at the localizer fixes everything downstream at once. A query by
topic name filters on the localized name (inboxsource.go:60), which is why
repairing the cached copy could never make the #general lookup hit: it went on
matching an empty string. Naming the conv where it is localized fixes the
lookup, the cached name that the inbox rows read, and the conversation header.

So MergeLocalMetadata goes back to writing what it was given, and
FindGeneralConvFromTeamID goes back to a single query. Their tests go with
them, replaced by one that purges a channel's METADATA body the way the server
does and requires the default conv to come back as #general while an ordinary
channel keeps an empty name rather than getting one invented for it.
@chrisnojima
chrisnojima force-pushed the nojima/HOTPOT-emoji-cut-2 branch from 1126191 to 248a3e8 Compare August 13, 2026 20:42
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