Skip to content

A community learned from another device kept its token as the title - #2202

Merged
mpretty-cyro merged 2 commits into
session-foundation:devfrom
mpretty-cyro:fix/linked-device-community-title
Aug 31, 2026
Merged

A community learned from another device kept its token as the title#2202
mpretty-cyro merged 2 commits into
session-foundation:devfrom
mpretty-cyro:fix/linked-device-community-title

Conversation

@mpretty-cyro

Copy link
Copy Markdown
Collaborator

A community that reaches a device by config sync shows its raw room token as the conversation title, permanently.

What the user sees

Join a community on one device. On every other device the user owns, the conversation is titled qa-0797141d-423a-484e-801f-d0fc7737b48a-w0-1 instead of its name. The device that joined shows the name correctly. Messages sync fine — only the title is wrong, and nothing the user can do in the app changes it.

This is not limited to freshly created test rooms. PySOGS's own tests assert that a room created with a name has info_updates == 0 (tests/test_rooms.py:86-101); only a later metadata edit moves it to 1. So any community whose metadata has not been edited since it was created — the normal state of a small community — reproduces this.

Why

The community's name is not part of the synced config record. The record carries base URL, room token and pubkey; the name is fetched from the SOGS by each device and stored locally, and the title is roomInfo?.details?.name ?: room (RecipientNames.kt:22).

The joining device fetches it: OpenGroupManager.add() calls the room-details API before writing the community into config. A device that learns of the community from that config can never reach that fetch — add() short-circuits on alreadyJoined, and the config arriving is how it learned about the community at all.

That leaves the poller, which is gated:

OpenGroupPoller.kt:120   val infoUpdates = latestRoomPollInfo?.details?.infoUpdates ?: 0
PySOGS rooms.py:856      if room.info_updates != info_updated:
                             result['details'] = get_room_info(room)

With nothing cached the device sends 0; the room is also at 0; the server reports no change; the poll writes a row with no details; the device keeps sending 0. Forever, and unaffected by time or by opening the conversation.

The change

While the poller has no usable name for a room, ask for the room outright instead of polling it. One branch, at the point that already knows what it has cached.

The predicate is "no usable name", not "no row" and not "no details object" — neither of those can express this state:

  • patchRoomInfo is an unconditional INSERT OR REPLACE, so a row exists after the first poll whether details came back or not. A row check fires once and then goes quiet, leaving every already-affected device stuck.
  • RoomInfo.details is non-nullable with an empty default (OpenGroupApi.kt:102), so a details-less response deserialises to a present-but-blank object. A details == null check compiles, reads as if it does something, and never fires.

It also matches the render: a blank name is exactly the state that falls back to the token, so the refetch condition and the visible symptom are the same condition. It repairs devices already holding a nameless record, and stops permanently once a name arrives. A failed request never reaches patchRoomInfo, so nothing is written and the next poll retries.

Verification

Join community test @android passes twice at 28s, against a 148s timeout before. Confirmed against a build grepped for the change. The room's counter was read directly from the SOGS database mid-run to confirm info_updates=0 on a room created with its name.

No unit test. There is no existing test harness for OpenGroupPoller or CommunityDatabase, and the change is one predicate inside a poll loop needing the API executor, config factory, database and a server. The Appium spec is what covers it.

For comparison

iOS has the same gate in its poller and would hit the same dead end; it never depends on it, because a community arriving from config triggers performInitialRequestsAfterAdd, which does a full room fetch (LibSession+UserGroups.swift:73-94). That is fire-and-forget (Task.detached + try?), so an iOS device whose fetch fails is stuck exactly as Android was, with no retry. This fix retries until a name arrives.

A community that syncs to a linked device showed its raw room token as the
conversation title, permanently. The device that joined showed the name.

Joining fetches the room outright and stores its details before writing the
community into config (OpenGroupManager.add). A linked device only ever receives
that config, so it never runs that fetch -- and add() cannot help it, because the
config arriving is how it learned about the community, so the already-joined
short-circuit is always taken.

That leaves the poller, which asks for pollInfo/{infoUpdates} and therefore gets
the room's details ONLY if the room's counter differs from the number we send.
With nothing cached we send 0, and a room whose metadata has not been edited
since it was created still has a counter of 0 -- setting a name at creation does
not bump it, so this is not limited to freshly minted test rooms. The server
answers "no change", we store a record with no details, we keep sending 0, and
the title never changes. The blank name then falls back to the room token, which
RecipientNames marks as a last resort.

So while we have no usable name for a room, ask for the room outright instead of
polling it.

The condition is "no name", not "no row" and not "no details object", because
neither of those can express this: patchRoomInfo is an unconditional
INSERT OR REPLACE, so a row exists after the first poll regardless, and
RoomInfo.details is non-nullable with an empty default, so a response carrying no
details deserialises to a present-but-blank object rather than to null. A row
check would fire once and then go quiet; a null-details check would never fire at
all. Keying on the name also matches the render exactly -- a blank name is
precisely the state that shows the token -- so devices already holding a nameless
record are repaired rather than left as they are.

Cost is one extra request per poll cycle for a room that has never yielded a
name, including the edge case of a room genuinely named "". A room with a name
makes the request once and then never again.
@mpretty-cyro
mpretty-cyro marked this pull request as ready for review August 31, 2026 06:12
@mpretty-cyro
mpretty-cyro merged commit 5dbe072 into session-foundation:dev Aug 31, 2026
5 checks passed
@mpretty-cyro
mpretty-cyro deleted the fix/linked-device-community-title branch August 31, 2026 06:12
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