Skip to content

fix: classify Matrix direct-message rooms - #7

Open
glifocat wants to merge 3 commits into
beeper:mainfrom
glifocat:agent/fix-dm-classification
Open

fix: classify Matrix direct-message rooms#7
glifocat wants to merge 3 commits into
beeper:mainfrom
glifocat:agent/fix-dm-classification

Conversation

@glifocat

@glifocat glifocat commented Jul 9, 2026

Copy link
Copy Markdown

What this does

Implements Chat SDK's synchronous isDM(threadId) hook for Matrix rooms.

The adapter now:

  • treats only rooms listed in Matrix m.direct account data as direct messages;
  • primes the synchronous cache from cached or server-side m.direct data during startup;
  • refreshes the cache when m.direct account data changes;
  • marks rooms created by openDM() immediately; and
  • fails closed when account data is unavailable, leaving unmarked rooms classified as channels.

Closes #6

Why

Without Adapter.isDM(), Chat SDK defaults every Matrix thread to isDM === false. Direct messages then go through channel handlers instead of onDirectMessage().

Matrix room size is not a reliable DM signal: an unmarked room with exactly two joined members is still a channel. m.direct is the explicit per-account classification used here.

How I verified it

  • pnpm typecheck
  • pnpm test (80 tests passed)
  • pnpm build
  • Added coverage for cold-start server priming, m.direct updates, unmarked two-person rooms, invalid thread IDs, newly created DMs, and homeserver failure fallback.

Assisted by Claude; reviewed and verified by a human before submission.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fa783f94-ea8e-459e-a9e3-0ccc323abb7c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The adapter synchronously classifies Matrix rooms as DMs using cached m.direct mappings, refreshes the cache during initialization and account-data updates, and fails closed when server priming fails. Tests cover classification, cache replacement, updates, invalid IDs, and uncached direct threads.

Changes

Direct message classification

Layer / File(s) Summary
DM cache initialization and refresh
src/index.ts
The adapter primes and rebuilds its direct-room cache from local and server m.direct data, refreshes it on account-data events, and clears it during shutdown.
Synchronous classification and cache updates
src/index.ts
isDM() decodes thread IDs and checks cached room IDs, while newly persisted direct mappings update the cache immediately.
DM behavior validation and changelog
src/index.test.ts, CHANGELOG.md
Tests cover mapped rooms, invalid IDs, openDM(), account-data reclassification, failed priming, stale-cache replacement, refreshed account-data sequencing, and the documented DM behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MatrixClient
  participant MatrixAdapter
  participant ChatSDK
  MatrixClient->>MatrixAdapter: provide m.direct account data
  MatrixAdapter->>MatrixClient: request latest m.direct data
  MatrixClient-->>MatrixAdapter: return account data
  MatrixAdapter->>MatrixAdapter: populate directRoomIDs
  ChatSDK->>MatrixAdapter: call isDM(threadId)
  MatrixAdapter-->>ChatSDK: return cached DM classification
  MatrixClient->>MatrixAdapter: emit m.direct account-data update
  MatrixAdapter->>MatrixAdapter: rebuild directRoomIDs
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements synchronous isDM caching from m.direct, refreshes on updates, and keeps unmarked rooms as channels.
Out of Scope Changes check ✅ Passed The added tests and changelog entry are in scope and support the DM-classification work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly summarizes the main change: classifying Matrix direct-message rooms.
Description check ✅ Passed The description matches the changeset and explains the DM classification, cache priming, refresh, and fallback behavior.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@glifocat
glifocat marked this pull request as ready for review July 9, 2026 21:59

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/index.ts`:
- Around line 305-308: The `RoomMemberEvent.Membership` handler in
`src/index.ts` is triggering unbounded `refreshTwoPersonRoom(roomId)` calls for
every membership change, which can spam `getJoinedRoomMembers` and race on the
same room. Update the handler and `refreshTwoPersonRoom` flow to dedupe
in-flight refreshes per `roomId` (and optionally debounce or cap concurrency) so
repeated events for the same room reuse one pending request and only the latest
result updates `twoPersonRoomIDs`.
- Around line 370-378: The DM detection paths are inconsistent because isDM()
already treats two-person rooms as DMs while isDirectRoom() still only checks
m.direct account data. Update isDirectRoom() to also consult the cached
twoPersonRoomIDs set, mirroring the logic in isDM(), so fetchThread and
fetchChannelInfo produce the same ThreadInfo.isDM/ChannelInfo.isDM result for
the same room.
- Around line 300-311: `primeDMRoomIDs()` is only using the local account-data
cache, so DM IDs can be missing on cold start before `ClientEvent.AccountData`
fires. Update the startup flow in `src/index.ts` to prime direct room IDs via
`loadDirectAccountData()` instead of relying solely on the cache, and keep the
existing `replaceDirectRoomIDs(...)` path so `isDM()` sees existing `m.direct`
rooms immediately after initialization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a1c35a86-f4cb-456b-bb93-28d676e5e7be

📥 Commits

Reviewing files that changed from the base of the PR and between 632fb14 and 98a9bb3.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/index.test.ts
  • src/index.ts
📜 Review details
🔇 Additional comments (5)
src/index.ts (3)

41-41: LGTM!

Also applies to: 212-213


338-339: LGTM!

Also applies to: 1225-1225


1084-1091: LGTM! Concurrency-capped priming (Math.min(4, roomIDs.length)) and the exact-two-member fallback logic are correctly implemented and match the test coverage.

Also applies to: 1093-1132

src/index.test.ts (1)

5-12: LGTM! Solid coverage of m.direct mapping, authoritative two-person fallback, invalid thread IDs, openDM() immediate classification, and reclassification on both AccountData and RoomMember.membership events. Traced against the implementation and the assertions correctly reflect the expected cache transitions.

Also applies to: 235-246, 479-547

CHANGELOG.md (1)

15-15: LGTM!

Comment thread src/index.ts Outdated
Comment thread src/index.ts Outdated
Comment thread src/index.ts

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/index.ts (1)

1072-1095: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Warm restarts can still reuse stale m.direct data here.

  • loadDirectAccountData() returns the cached snapshot whenever it is non-empty, so primeDirectRoomIDs() does not force a homeserver refresh in the persisted-store case.
  • That leaves isDM() able to misclassify rooms until the first ClientEvent.AccountData update arrives.
  • The “member count” wording is stale; this path is now m.direct-only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` around lines 1072 - 1095, Update primeDirectRoomIDs() to force
a fresh homeserver m.direct fetch on startup instead of relying on the non-empty
cached snapshot returned by loadDirectAccountData(), then replace directRoomIDs
with the refreshed data while retaining cached data only as an appropriate
fallback on fetch failure. Also update the warning text to describe unmarked
rooms as non-DMs or channels without mentioning member count.
🧹 Nitpick comments (1)
src/index.test.ts (1)

508-525: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a companion test for the "non-empty but stale" cache case.

This test covers the empty-cache/server-unavailable path well, but there's no test where getAccountData() (local cache) already returns a non-empty, stale m.direct mapping at startup — the scenario where loadDirectAccountData()'s cache-shortcut (see src/index.ts around primeDirectRoomIDs/loadDirectAccountData) causes priming to skip the server call entirely. Adding a test that seeds getAccountData with stale data and asserts whether getAccountDataFromServer is (not) called would make the freshness contract explicit and catch regressions either way.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.test.ts` around lines 508 - 525, Add a companion test for stale
non-empty m.direct cache data, using the existing client setup and
initialization flow around primeDirectRoomIDs/loadDirectAccountData. Mock
getAccountData to return a non-empty stale mapping, control
getAccountDataFromServer, and assert whether the server method is called
according to the intended freshness contract, along with verifying the resulting
DM behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/index.ts`:
- Around line 1072-1095: Update primeDirectRoomIDs() to force a fresh homeserver
m.direct fetch on startup instead of relying on the non-empty cached snapshot
returned by loadDirectAccountData(), then replace directRoomIDs with the
refreshed data while retaining cached data only as an appropriate fallback on
fetch failure. Also update the warning text to describe unmarked rooms as
non-DMs or channels without mentioning member count.

---

Nitpick comments:
In `@src/index.test.ts`:
- Around line 508-525: Add a companion test for stale non-empty m.direct cache
data, using the existing client setup and initialization flow around
primeDirectRoomIDs/loadDirectAccountData. Mock getAccountData to return a
non-empty stale mapping, control getAccountDataFromServer, and assert whether
the server method is called according to the intended freshness contract, along
with verifying the resulting DM behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e613d00d-affa-4ce1-b357-90bcc78f59a5

📥 Commits

Reviewing files that changed from the base of the PR and between 98a9bb3 and e700b58.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/index.test.ts
  • src/index.ts
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
📜 Review details
🔇 Additional comments (7)
src/index.ts (4)

211-211: LGTM!

Also applies to: 319-339


282-306: Listener-before-startClient ordering is correct; isDM() classification depends on it.

Registering the ClientEvent.AccountData listener (Lines 298-302) before primeDirectRoomIDs()/startClient() ensures no m.direct sync deltas are missed once syncing begins. Good sequencing.


363-370: LGTM!

Synchronous, decode-and-lookup implementation matches the Chat SDK contract and the m.direct-only classification policy agreed in prior review discussion.


1188-1188: LGTM!

Immediately adding the newly-created DM room to directRoomIDs after persisting the m.direct update correctly makes openDM()-created rooms classifiable right away, matching the PR objective.

src/index.test.ts (3)

5-11: LGTM!

Also applies to: 234-237


470-506: LGTM!

Good coverage: m.direct-mapped room, unmarked two-person room (regression guard for the removed heuristic), invalid thread id, openDM()-created room, and account-data-driven reclassification.


2746-2761: LGTM!

@glifocat

Copy link
Copy Markdown
Author

Revision is pushed, as discussed above:

  • e700b58 removes the two-person fallback entirely (twoPersonRoomIDs, the joined-member priming, and the membership listener). m.direct account data is now the only DM signal, so isDM() and isDirectRoom() agree. Includes the regression test for the unmarked two-person room case.
  • 483b1cc primes m.direct from the server before startClient() so cold starts classify correctly, keeping the cached copy as fallback when the server call fails.

Full suite passes locally (80 tests).

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

@glifocat Thanks for the update. I’ll review the revised implementation, with particular attention to the m.direct-only invariant, startup ordering, fallback behavior, and the new regression coverage.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Direct messages are dispatched as channel messages

1 participant