Skip to content

feat(user-service): cross-site thread-unread badge RPC#436

Open
yenta wants to merge 1 commit into
mainfrom
claude/thread-unread-status-rpc-70zmfx
Open

feat(user-service): cross-site thread-unread badge RPC#436
yenta wants to merge 1 commit into
mainfrom
claude/thread-unread-status-rpc-70zmfx

Conversation

@yenta

@yenta yenta commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a client-facing RPC that returns a per-user thread-unread badge aggregated across every federation site the user participates in, and removes the superseded (callerless) ThreadUnreadSummary leaf.

New client RPC: chat.user.{account}.request.user.{siteID}.thread.unreadThreadUnreadResponse{ unread, unreadDirectMessage, unreadMention, lastMessageAt?, unavailableSites? }.

How it works

It mirrors the existing subscription-unread pattern (one unread() helper, two data sources):

  1. user-service reads all of the user's thread-subscriptions from the home-site replica (ThreadSubscriptionRepo.ListByAccount) — these are federated to the home site, each tagged with the room's home siteId.
  2. It groups them by siteId and, per site (uncapped fan-out, bounded in practice by the user's distinct thread-sub sites, chunked by maxBatchSize), calls a new room-service RPC ThreadRoomInfoBatch (chat.server.request.room.{siteID}.thread.info.batch) which returns each thread room's lastMsgAt + parent roomType.
  3. It folds the results with the existing unread() helper: OR the booleans across all rows, max lastMsgAt over found rows, unreadDirectMessage = unread && roomType==dm, unreadMention = hasMention. Per-site RPC failures degrade into unavailableSites instead of failing the request.

Grouping by siteId (one thread-sub row per (threadRoomId, account)) routes each thread to exactly one owning site, so nothing is double-counted.

Removed

The ThreadUnreadSummary room-service leaf (chat.server.request.room.{siteID}.thread.unread.summary) had no runtime caller and is superseded by this design, so its handler, store method, subject builders, model types, and tests are deleted. The thread_rooms collection handle and the {userAccount, siteId} index are retained (reused by GetThreadRoomInfos / ListByAccount).

Notable design decisions (accepted trade-offs)

  • Single unread rule reused for local + cross-site (no separate local path).
  • ThreadRoomInfoBatch returns roomType (via a rooms lookup at the room's home site) so unreadDirectMessage uses authoritative data; the store method uses two projected finds joined in Go, no $lookup.
  • A transient false-"unread" is possible for a cross-site thread immediately after it's read, until the InboxThreadRead event replicates the lastSeenAt to the home replica (self-heals within the inbox lag). This matches how room-unread already behaves.

Changes

  • pkg/subject: UserThreadUnread/Pattern, ThreadRoomInfoBatch/Subscribe.
  • pkg/model: ThreadUnreadRequest/Response, ThreadRoomInfoBatchRequest/Response, ThreadRoomInfo, ThreadSubRef.
  • room-service: GetThreadRoomInfos store method + threadRoomInfoBatch handler.
  • user-service: roomclient.GetThreadRoomInfoBatch, mongorepo.ThreadSubscriptionRepo.ListByAccount, RoomClient/ThreadSubscriptionRepository interfaces, and the GetThreadUnread aggregator handler.
  • docs/client-api.md: documents the new thread.unread RPC.

Testing

  • Unit tests (TDD, -race) for the subject builders, model round-trips, the room-service handler (happy path, empty, over-limit, store-error), the GetThreadUnread aggregation (multi-site OR/max, null-lastSeen⇒unread, DM fold, mention, not-found skip, site-failure degrade, empty-subs, list error), and chunkStrings boundaries.
  • Integration tests added for GetThreadRoomInfos, GetThreadRoomInfoBatch (roomclient), and ListByAccount.
  • Full make test (race) and make lint pass; gosec passes. Integration tests and govulncheck/semgrep were network-blocked in the build environment and run in CI.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BocBvJn2N9YYMmciA5EWJX


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added a client-facing thread-unread endpoint that returns a cross-site unread badge, including direct message and @mention states.
    • Added a server-to-server batch lookup for thread-room metadata to support efficient badge computation.
    • Responses can include the newest unread activity timestamp and optionally list sites that could not be checked.
  • Bug Fixes
    • Per-site metadata failures now degrade gracefully without failing the entire request.
    • Thread-room metadata lookups preserve requested order and treat missing rooms cleanly.
  • Tests & Documentation
    • Updated models, subject routing, handlers, and tests for the new unread and batch room-info behavior; refreshed API documentation and added integration coverage.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73fbdd73-6f46-49c3-8fc2-141e7809fb8c

📥 Commits

Reviewing files that changed from the base of the PR and between dc0c03e and ef0346d.

📒 Files selected for processing (25)
  • docs/client-api.md
  • docs/superpowers/plans/2026-07-01-user-thread-unread-rpc.md
  • docs/superpowers/specs/2026-07-01-user-thread-unread-rpc-design.md
  • pkg/model/model_test.go
  • pkg/model/threadsubscription.go
  • pkg/subject/subject.go
  • pkg/subject/subject_test.go
  • room-service/handler.go
  • room-service/handler_test.go
  • room-service/integration_test.go
  • room-service/mock_store_test.go
  • room-service/store.go
  • room-service/store_mongo.go
  • user-service/main.go
  • user-service/mongorepo/threadsubscriptions.go
  • user-service/mongorepo/threadsubscriptions_test.go
  • user-service/roomclient/client.go
  • user-service/roomclient/client_integration_test.go
  • user-service/service/mocks/mock_repository.go
  • user-service/service/service.go
  • user-service/service/service_test.go
  • user-service/service/status_test.go
  • user-service/service/threads_test.go
  • user-service/service/threadunread.go
  • user-service/service/threadunread_test.go
 _______________________________________________________________________________________
< If you don't fail at least 90% of the time, you're not aiming high enough. - Alan Kay >
 ---------------------------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
📝 Walkthrough

Walkthrough

Adds a user-service thread unread RPC, a room-service batch thread-room info RPC, and a Mongo-backed thread-subscription read path. The old ThreadUnreadSummary flow is removed, and related docs, tests, and wiring are updated.

Changes

Thread Unread RPC Implementation

Layer / File(s) Summary
Docs and API contract
docs/client-api.md, docs/superpowers/plans/2026-07-01-user-thread-unread-rpc.md, docs/superpowers/specs/2026-07-01-user-thread-unread-rpc-design.md
Documents the thread unread client RPC, its request/response shape, error handling, and implementation plan/spec.
Models and subjects
pkg/model/threadsubscription.go, pkg/model/model_test.go, pkg/subject/subject.go, pkg/subject/subject_test.go
Replaces ThreadUnreadSummary types and subjects with empty-request unread summary, batch room-info, and thread-subscription projection types, with round-trip and builder tests.
Room-service batch RPC
room-service/handler.go, room-service/store.go, room-service/store_mongo.go, room-service/mock_store_test.go, room-service/handler_test.go, room-service/integration_test.go
Replaces the old unread-summary RPC with threadRoomInfoBatch and the GetThreadRoomInfos store path, plus mock and test coverage.
User-service repo and room client
user-service/roomclient/client.go, user-service/roomclient/client_integration_test.go, user-service/mongorepo/threadsubscriptions.go, user-service/mongorepo/threadsubscriptions_test.go
Adds the room-service batch client and a Mongo repository for listing thread subscriptions by account.
Service wiring
user-service/main.go, user-service/service/service.go, user-service/service/mocks/mock_repository.go, user-service/service/service_test.go, user-service/service/status_test.go, user-service/service/threads_test.go
Adds the new repository dependency, wires it through main and UserService, registers the new route, and updates generated mocks and constructor tests.
GetThreadUnread handler and tests
user-service/service/threadunread.go, user-service/service/threadunread_test.go
Implements per-site fan-out, chunking, degradation reporting, and unread aggregation with scenario-based unit tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant UserService
  participant ThreadSubscriptionRepo
  participant RoomClient
  participant RoomService

  Client->>UserService: thread.unread request
  UserService->>ThreadSubscriptionRepo: ListByAccount(account)
  ThreadSubscriptionRepo-->>UserService: []ThreadSubRef
  UserService->>RoomClient: GetThreadRoomInfoBatch(siteID, chunked threadRoomIDs)
  RoomClient->>RoomService: ThreadRoomInfoBatch RPC
  RoomService-->>RoomClient: ThreadRoomInfoBatchResponse
  RoomClient-->>UserService: []ThreadRoomInfo or error
  UserService-->>Client: ThreadUnreadResponse
Loading

Possibly related PRs

  • hmchangw/chat#218: Updates per-thread read progress fields that this PR reads through ThreadSubRef.
  • hmchangw/chat#289: Replaces the room-service unread-summary path with the new cross-site unread flow.

Suggested labels: ready

Suggested reviewers: mliu33

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a new cross-site thread-unread badge RPC in user-service.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/thread-unread-status-rpc-70zmfx

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 1

🧹 Nitpick comments (2)
user-service/roomclient/client_integration_test.go (1)

125-159: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add missing test parity with GetRoomsInfo coverage.

TestGetRoomsInfo_Integration also covers "no responder" (RPC failure) and "cross-site siteID routing" scenarios, but TestGetThreadRoomInfoBatch_Integration only covers happy path and errcode relay. Since GetThreadRoomInfoBatch has the identical siteID-routing pattern, mirroring the missing subtests would close a coverage gap on a newly introduced fan-out path used by GetThreadUnread.

✅ Suggested additional subtests
 	t.Run("errcode reply relayed", func(t *testing.T) {
 		nc := dial(t)
 		sub, err := nc.Subscribe(subject.ThreadRoomInfoBatch("site-a"), func(m otelnats.Msg) {
 			data, _ := json.Marshal(errcode.BadRequest("bad"))
 			_ = m.Msg.Respond(data)
 		})
 		require.NoError(t, err)
 		t.Cleanup(func() { _ = sub.Unsubscribe() })

 		_, err = New(nc, "site-a").GetThreadRoomInfoBatch(context.Background(), "site-a", []string{"tr1"})
 		var e *errcode.Error
 		require.True(t, errors.As(err, &e))
 		assert.Equal(t, errcode.CodeBadRequest, e.Code)
 	})
+
+	t.Run("no responder — returns error wrapping thread-room-info rpc", func(t *testing.T) {
+		nc := dial(t)
+		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+		defer cancel()
+
+		_, err := New(nc, "site-a").GetThreadRoomInfoBatch(ctx, "site-a", []string{"tr1"})
+		require.Error(t, err)
+		assert.Contains(t, err.Error(), "thread-room-info rpc")
+	})
+
+	t.Run("cross-site siteID routing — uses siteID param not c.siteID", func(t *testing.T) {
+		nc := dial(t)
+		sub, err := nc.Subscribe(subject.ThreadRoomInfoBatch("site-b"), func(m otelnats.Msg) {
+			out, _ := json.Marshal(model.ThreadRoomInfoBatchResponse{
+				Threads: []model.ThreadRoomInfo{{ThreadRoomID: "tr2", Found: true, LastMsgAt: 10, RoomType: model.RoomTypeChannel}},
+			})
+			_ = m.Msg.Respond(out)
+		})
+		require.NoError(t, err)
+		t.Cleanup(func() { _ = sub.Unsubscribe() })
+
+		got, err := New(nc, "site-a").GetThreadRoomInfoBatch(context.Background(), "site-b", []string{"tr2"})
+		require.NoError(t, err)
+		require.Len(t, got, 1)
+	})
🤖 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 `@user-service/roomclient/client_integration_test.go` around lines 125 - 159,
`TestGetThreadRoomInfoBatch_Integration` is missing the same coverage that
`TestGetRoomsInfo_Integration` already has: add subtests for the no-responder
RPC failure case and for cross-site `siteID` routing. Use
`New(...).GetThreadRoomInfoBatch`, `subject.ThreadRoomInfoBatch`, and the
existing `dial`/`Subscribe` test pattern to verify the request fails when no
subscriber exists and that a request sent with one site ID does not hit a
subscriber for another site ID.
user-service/service/threadunread_test.go (1)

50-66: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a multi-site (2+ sites, mixed success/failure) test case run with -race.

Current failure test only exercises a single site. A test with ≥2 sites (one succeeding, one failing) run under -race would directly validate the per-goroutine results[i] indexing and rule out any loop-variable-capture regression, independent of Go version.

🤖 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 `@user-service/service/threadunread_test.go` around lines 50 - 66, The current
GetThreadUnread_SiteFailureDegrades test in threadunread_test.go only covers one
site, so add a multi-site case that exercises both a successful and a failing
site in the same call. Use threadUnreadService.GetThreadUnread with multiple
ThreadSubRef entries and stub RoomClient.GetThreadRoomInfoBatch for at least two
site IDs so one returns data and one returns an error, then assert the mixed
results and unavailable sites. Run the new test under -race to validate the
per-goroutine results[i] handling and guard against loop-variable-capture
issues.
🤖 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 `@room-service/store_mongo.go`:
- Around line 1778-1787: The GetThreadRoomInfos lookup is using ListRoomsByIDs,
but that shared helper must keep returning full room documents for other
callers. Update GetThreadRoomInfos to query room ids directly with a projection
that only fetches _id and type, then populate typeByRoom from that result; keep
the bson field name as type to match model.Room.Type.

---

Nitpick comments:
In `@user-service/roomclient/client_integration_test.go`:
- Around line 125-159: `TestGetThreadRoomInfoBatch_Integration` is missing the
same coverage that `TestGetRoomsInfo_Integration` already has: add subtests for
the no-responder RPC failure case and for cross-site `siteID` routing. Use
`New(...).GetThreadRoomInfoBatch`, `subject.ThreadRoomInfoBatch`, and the
existing `dial`/`Subscribe` test pattern to verify the request fails when no
subscriber exists and that a request sent with one site ID does not hit a
subscriber for another site ID.

In `@user-service/service/threadunread_test.go`:
- Around line 50-66: The current GetThreadUnread_SiteFailureDegrades test in
threadunread_test.go only covers one site, so add a multi-site case that
exercises both a successful and a failing site in the same call. Use
threadUnreadService.GetThreadUnread with multiple ThreadSubRef entries and stub
RoomClient.GetThreadRoomInfoBatch for at least two site IDs so one returns data
and one returns an error, then assert the mixed results and unavailable sites.
Run the new test under -race to validate the per-goroutine results[i] handling
and guard against loop-variable-capture issues.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2074dd2f-ca73-463c-b098-26ee954b480c

📥 Commits

Reviewing files that changed from the base of the PR and between 4bba9cd and 6aac04d.

📒 Files selected for processing (25)
  • docs/client-api.md
  • docs/superpowers/plans/2026-07-01-user-thread-unread-rpc.md
  • docs/superpowers/specs/2026-07-01-user-thread-unread-rpc-design.md
  • pkg/model/model_test.go
  • pkg/model/threadsubscription.go
  • pkg/subject/subject.go
  • pkg/subject/subject_test.go
  • room-service/handler.go
  • room-service/handler_test.go
  • room-service/integration_test.go
  • room-service/mock_store_test.go
  • room-service/store.go
  • room-service/store_mongo.go
  • user-service/main.go
  • user-service/mongorepo/threadsubscriptions.go
  • user-service/mongorepo/threadsubscriptions_test.go
  • user-service/roomclient/client.go
  • user-service/roomclient/client_integration_test.go
  • user-service/service/mocks/mock_repository.go
  • user-service/service/service.go
  • user-service/service/service_test.go
  • user-service/service/status_test.go
  • user-service/service/threads_test.go
  • user-service/service/threadunread.go
  • user-service/service/threadunread_test.go

Comment thread room-service/store_mongo.go Outdated
@yenta yenta force-pushed the claude/thread-unread-status-rpc-70zmfx branch from 5959db3 to 93912e2 Compare July 1, 2026 15:33

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

🧹 Nitpick comments (2)
user-service/service/threadunread_test.go (1)

132-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a size <= 0 case to cover chunkStrings' invalid-input branch. The if size <= 0 { size = len(ids) } path in chunkStrings is untested; a case like {name: "non-positive size", ids: []string{"a","b"}, size: 0, expected: [][]string{{"a","b"}}} closes the gap. As per coding guidelines, "Tests must cover: happy path, error paths, edge cases ..., and invalid input".

🤖 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 `@user-service/service/threadunread_test.go` around lines 132 - 163, Add a test
case in TestChunkStrings to cover the invalid-input branch in chunkStrings where
size <= 0, using a non-positive size and asserting it falls back to returning
the full ids slice as a single chunk. Locate the fix in the chunkStrings test
table and ensure the new case exercises the existing size normalization behavior
alongside the current happy-path cases.

Source: Coding guidelines

docs/superpowers/specs/2026-07-01-user-thread-unread-rpc-design.md (1)

136-150: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tag the data-flow fence with a language.

The fenced diagram currently has no language tag, which is what markdownlint is flagging. text (or a plain list) will keep the docs lint-clean.

🤖 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 `@docs/superpowers/specs/2026-07-01-user-thread-unread-rpc-design.md` around
lines 136 - 150, The fenced data-flow diagram under the thread.unread RPC flow
is missing a language tag, causing markdownlint to fail. Update the fence around
the diagram in the spec so it uses an explicit language like text, or convert it
to a plain list, and keep the content unchanged while ensuring the markdown
remains lint-clean.

Source: Linters/SAST tools

🤖 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.

Nitpick comments:
In `@docs/superpowers/specs/2026-07-01-user-thread-unread-rpc-design.md`:
- Around line 136-150: The fenced data-flow diagram under the thread.unread RPC
flow is missing a language tag, causing markdownlint to fail. Update the fence
around the diagram in the spec so it uses an explicit language like text, or
convert it to a plain list, and keep the content unchanged while ensuring the
markdown remains lint-clean.

In `@user-service/service/threadunread_test.go`:
- Around line 132-163: Add a test case in TestChunkStrings to cover the
invalid-input branch in chunkStrings where size <= 0, using a non-positive size
and asserting it falls back to returning the full ids slice as a single chunk.
Locate the fix in the chunkStrings test table and ensure the new case exercises
the existing size normalization behavior alongside the current happy-path cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3c2e645c-89e4-4db2-a194-bc7ee6a853c5

📥 Commits

Reviewing files that changed from the base of the PR and between 6aac04d and 93912e2.

📒 Files selected for processing (25)
  • docs/client-api.md
  • docs/superpowers/plans/2026-07-01-user-thread-unread-rpc.md
  • docs/superpowers/specs/2026-07-01-user-thread-unread-rpc-design.md
  • pkg/model/model_test.go
  • pkg/model/threadsubscription.go
  • pkg/subject/subject.go
  • pkg/subject/subject_test.go
  • room-service/handler.go
  • room-service/handler_test.go
  • room-service/integration_test.go
  • room-service/mock_store_test.go
  • room-service/store.go
  • room-service/store_mongo.go
  • user-service/main.go
  • user-service/mongorepo/threadsubscriptions.go
  • user-service/mongorepo/threadsubscriptions_test.go
  • user-service/roomclient/client.go
  • user-service/roomclient/client_integration_test.go
  • user-service/service/mocks/mock_repository.go
  • user-service/service/service.go
  • user-service/service/service_test.go
  • user-service/service/status_test.go
  • user-service/service/threads_test.go
  • user-service/service/threadunread.go
  • user-service/service/threadunread_test.go
✅ Files skipped from review due to trivial changes (3)
  • docs/client-api.md
  • user-service/service/mocks/mock_repository.go
  • room-service/mock_store_test.go
🚧 Files skipped from review as they are similar to previous changes (18)
  • user-service/main.go
  • user-service/service/service_test.go
  • user-service/service/status_test.go
  • user-service/service/threads_test.go
  • user-service/mongorepo/threadsubscriptions_test.go
  • user-service/roomclient/client.go
  • user-service/roomclient/client_integration_test.go
  • user-service/mongorepo/threadsubscriptions.go
  • room-service/handler_test.go
  • pkg/subject/subject_test.go
  • room-service/integration_test.go
  • user-service/service/service.go
  • user-service/service/threadunread.go
  • room-service/store.go
  • pkg/subject/subject.go
  • pkg/model/model_test.go
  • pkg/model/threadsubscription.go
  • room-service/handler.go

@yenta yenta force-pushed the claude/thread-unread-status-rpc-70zmfx branch 2 times, most recently from 41b96e3 to e9ff324 Compare July 2, 2026 01:06

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

🧹 Nitpick comments (1)
docs/superpowers/plans/2026-07-01-user-thread-unread-rpc.md (1)

27-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unify the subject builder shape.

This plan still introduces ThreadRoomInfoBatchSubscribe, but the spec says to reuse ThreadRoomInfoBatch(siteID) for both publishing and handler registration. Keeping both names will drift the public subject API.

🤖 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 `@docs/superpowers/plans/2026-07-01-user-thread-unread-rpc.md` around lines 27
- 30, The subject API is inconsistent because the plan adds both
ThreadRoomInfoBatch and ThreadRoomInfoBatchSubscribe, but the spec requires a
single ThreadRoomInfoBatch(siteID) shape for both publishing and handler
registration. Update pkg/subject/subject.go to expose only the unified
ThreadRoomInfoBatch builder and have room-service/handler.go registration use
that same symbol, while removing the extra ThreadRoomInfoBatchSubscribe
reference from the plan and any related call sites.
🤖 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.

Nitpick comments:
In `@docs/superpowers/plans/2026-07-01-user-thread-unread-rpc.md`:
- Around line 27-30: The subject API is inconsistent because the plan adds both
ThreadRoomInfoBatch and ThreadRoomInfoBatchSubscribe, but the spec requires a
single ThreadRoomInfoBatch(siteID) shape for both publishing and handler
registration. Update pkg/subject/subject.go to expose only the unified
ThreadRoomInfoBatch builder and have room-service/handler.go registration use
that same symbol, while removing the extra ThreadRoomInfoBatchSubscribe
reference from the plan and any related call sites.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6e91b84f-0fa4-4d47-ae5d-fae0ed2fc79f

📥 Commits

Reviewing files that changed from the base of the PR and between 93912e2 and e9ff324.

📒 Files selected for processing (25)
  • docs/client-api.md
  • docs/superpowers/plans/2026-07-01-user-thread-unread-rpc.md
  • docs/superpowers/specs/2026-07-01-user-thread-unread-rpc-design.md
  • pkg/model/model_test.go
  • pkg/model/threadsubscription.go
  • pkg/subject/subject.go
  • pkg/subject/subject_test.go
  • room-service/handler.go
  • room-service/handler_test.go
  • room-service/integration_test.go
  • room-service/mock_store_test.go
  • room-service/store.go
  • room-service/store_mongo.go
  • user-service/main.go
  • user-service/mongorepo/threadsubscriptions.go
  • user-service/mongorepo/threadsubscriptions_test.go
  • user-service/roomclient/client.go
  • user-service/roomclient/client_integration_test.go
  • user-service/service/mocks/mock_repository.go
  • user-service/service/service.go
  • user-service/service/service_test.go
  • user-service/service/status_test.go
  • user-service/service/threads_test.go
  • user-service/service/threadunread.go
  • user-service/service/threadunread_test.go
✅ Files skipped from review due to trivial changes (2)
  • room-service/mock_store_test.go
  • user-service/service/mocks/mock_repository.go
🚧 Files skipped from review as they are similar to previous changes (20)
  • user-service/mongorepo/threadsubscriptions_test.go
  • user-service/mongorepo/threadsubscriptions.go
  • user-service/main.go
  • user-service/service/service_test.go
  • user-service/service/status_test.go
  • user-service/service/threads_test.go
  • user-service/roomclient/client.go
  • user-service/service/threadunread_test.go
  • pkg/subject/subject_test.go
  • user-service/service/threadunread.go
  • room-service/store.go
  • docs/client-api.md
  • room-service/integration_test.go
  • user-service/roomclient/client_integration_test.go
  • user-service/service/service.go
  • pkg/model/threadsubscription.go
  • room-service/handler.go
  • room-service/handler_test.go
  • pkg/subject/subject.go
  • pkg/model/model_test.go

@yenta yenta force-pushed the claude/thread-unread-status-rpc-70zmfx branch 2 times, most recently from b7a3391 to dc0c03e Compare July 2, 2026 02:48

@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: 2

🧹 Nitpick comments (1)
user-service/service/threadunread.go (1)

14-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Magic batch-size constant duplicated across services without a shared source of truth.

threadInfoBatchChunk = 500 is a local guess at room-service's MAX_BATCH_SIZE (documented as "default 1000" in the comment). If room-service's configured batch limit is lowered below 500 in some deployment, this constant silently becomes wrong with no compile-time or config-time signal. Consider sourcing this limit from a shared pkg constant (or having the room-service batch RPC report/enforce it back to callers) rather than a comment-documented assumption.

🤖 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 `@user-service/service/threadunread.go` around lines 14 - 16, The
threadInfoBatchChunk constant in threadunread.go is a duplicated batch-size
assumption with no shared source of truth. Update the ThreadRoomInfoBatch call
path to use a shared pkg constant or a value provided/enforced by room-service
instead of hardcoding 500, and make the batch sizing logic in the relevant
thread unread helper(s) derive from that centralized limit so it stays
consistent across deployments.
🤖 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 `@docs/superpowers/specs/2026-07-01-user-thread-unread-rpc-design.md`:
- Around line 136-147: The ThreadUnreadSummary flow currently limits
thread_subscriptions reads to the newest 500 rows, which can undercount unread
state for accounts with more subscriptions. Update the ListByAccount-based
aggregation in the spec to read all subscriptions for the account, and remove
the repeated 500-row cap wherever it appears in the later components section,
keeping the rest of the unread/group-by-site logic unchanged.
- Around line 63-89: The RPC contract naming in this spec is inconsistent with
the implementation plan: update the subject helpers and model/request-response
names from ThreadUnreadSummary* and thread.unread.summary to ThreadUnread* and
thread.unread throughout the referenced spec sections. Use the existing
identifiers like UserThreadUnreadSummary, UserThreadUnreadSummaryPattern,
ThreadUnreadSummaryRequest, and ThreadUnreadSummaryResponse as the places to
rename so the docs point to one contract only.

---

Nitpick comments:
In `@user-service/service/threadunread.go`:
- Around line 14-16: The threadInfoBatchChunk constant in threadunread.go is a
duplicated batch-size assumption with no shared source of truth. Update the
ThreadRoomInfoBatch call path to use a shared pkg constant or a value
provided/enforced by room-service instead of hardcoding 500, and make the batch
sizing logic in the relevant thread unread helper(s) derive from that
centralized limit so it stays consistent across deployments.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3aae2764-4f4c-4d93-9b50-095f24cd503f

📥 Commits

Reviewing files that changed from the base of the PR and between e9ff324 and dc0c03e.

📒 Files selected for processing (25)
  • docs/client-api.md
  • docs/superpowers/plans/2026-07-01-user-thread-unread-rpc.md
  • docs/superpowers/specs/2026-07-01-user-thread-unread-rpc-design.md
  • pkg/model/model_test.go
  • pkg/model/threadsubscription.go
  • pkg/subject/subject.go
  • pkg/subject/subject_test.go
  • room-service/handler.go
  • room-service/handler_test.go
  • room-service/integration_test.go
  • room-service/mock_store_test.go
  • room-service/store.go
  • room-service/store_mongo.go
  • user-service/main.go
  • user-service/mongorepo/threadsubscriptions.go
  • user-service/mongorepo/threadsubscriptions_test.go
  • user-service/roomclient/client.go
  • user-service/roomclient/client_integration_test.go
  • user-service/service/mocks/mock_repository.go
  • user-service/service/service.go
  • user-service/service/service_test.go
  • user-service/service/status_test.go
  • user-service/service/threads_test.go
  • user-service/service/threadunread.go
  • user-service/service/threadunread_test.go
✅ Files skipped from review due to trivial changes (1)
  • user-service/service/mocks/mock_repository.go
🚧 Files skipped from review as they are similar to previous changes (14)
  • user-service/service/service_test.go
  • user-service/mongorepo/threadsubscriptions.go
  • user-service/service/threads_test.go
  • user-service/main.go
  • user-service/roomclient/client.go
  • room-service/mock_store_test.go
  • user-service/service/status_test.go
  • room-service/integration_test.go
  • room-service/handler_test.go
  • user-service/service/threadunread_test.go
  • room-service/handler.go
  • user-service/roomclient/client_integration_test.go
  • room-service/store.go
  • room-service/store_mongo.go

Comment thread docs/superpowers/specs/2026-07-01-user-thread-unread-rpc-design.md
Comment on lines +136 to +147
1. threadSubs.ListByAccount(account) (local Mongo thread_subscriptions replica — ALL sites; newest 500 by _id desc)
→ rows: {threadRoomId, siteId, lastSeenAt, hasMention}
2. group rows by siteId; concurrently (no fan-out cap), per site, chunked by maxBatchSize:
rooms.GetThreadRoomInfoBatch(site, threadRoomIds) → map[threadRoomId]ThreadRoomInfo
compute per row (shared unread() helper):
unread = info.Found && (lastSeenAt == nil || info.LastMsgAt > lastSeenAt(UnixMilli))
dmUnread = unread && info.RoomType == dm
mention = hasMention
track max(info.LastMsgAt) where Found
3. aggregate: OR unread / dmUnread / mention across all rows;
LastMessageAt = max lastMsgAt; failed sites → UnavailableSites
→ ThreadUnreadSummaryResponse

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove the 500-row cap from ListByAccount.

The badge is supposed to aggregate every thread subscription the user has, but this spec limits the replica read to the newest 500 rows. That will silently undercount unread state for anyone with more than 500 thread subs, and the same cap is repeated later in the components section. Based on the implementation plan and spec.

Also applies to: 211-223

🤖 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 `@docs/superpowers/specs/2026-07-01-user-thread-unread-rpc-design.md` around
lines 136 - 147, The ThreadUnreadSummary flow currently limits
thread_subscriptions reads to the newest 500 rows, which can undercount unread
state for accounts with more subscriptions. Update the ListByAccount-based
aggregation in the spec to read all subscriptions for the account, and remove
the repeated 500-row cap wherever it appears in the later components section,
keeping the rest of the unread/group-by-site logic unchanged.

@yenta yenta force-pushed the claude/thread-unread-status-rpc-70zmfx branch 2 times, most recently from 17292d7 to 38d1e5b Compare July 2, 2026 03:09
@yenta yenta added the ready label Jul 2, 2026
@yenta yenta force-pushed the claude/thread-unread-status-rpc-70zmfx branch from 38d1e5b to ef0346d Compare July 2, 2026 06:02
Adds a client-facing RPC that returns a per-user thread-unread badge
aggregated across every federation site the user participates in, and
removes the superseded (callerless) ThreadUnreadSummary leaf.

New client RPC: chat.user.{account}.request.user.{siteID}.thread.unread
-> ThreadUnreadResponse{ unread, unreadDirectMessage, unreadMention,
lastMessageAt?, unavailableSites? }.

How it works (mirrors the existing subscription-unread pattern, one
unread() helper): user-service reads all of the user's thread-subs from
the home-site replica (ThreadSubscriptionRepo.ListByAccount), groups them
by the room's home siteId, and per site (uncapped fan-out, chunked by
maxBatchSize) calls the new room-service ThreadRoomInfoBatch RPC
(chat.server.request.room.{siteID}.thread.info.batch) for each thread
room's lastMsgAt + parent roomType. Results fold via unread(): OR the
booleans, max lastMsgAt over found rows, unreadDirectMessage = unread &&
roomType==dm, unreadMention = hasMention. Per-site RPC failures degrade
into unavailableSites. Grouping by siteId counts each thread exactly once.

- pkg/subject: UserThreadUnread/Pattern, ThreadRoomInfoBatch/Subscribe.
- pkg/model: ThreadUnreadRequest/Response, ThreadRoomInfoBatchRequest/
  Response, ThreadRoomInfo, ThreadSubRef.
- room-service: GetThreadRoomInfos store method (two projected finds,
  no $lookup) + threadRoomInfoBatch handler.
- user-service: roomclient.GetThreadRoomInfoBatch,
  mongorepo.ThreadSubscriptionRepo.ListByAccount, RoomClient/
  ThreadSubscriptionRepository interfaces, GetThreadUnread aggregator.
- docs/client-api.md: documents the new thread.unread RPC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BocBvJn2N9YYMmciA5EWJX
@yenta yenta force-pushed the claude/thread-unread-status-rpc-70zmfx branch from ef0346d to 8553a89 Compare July 2, 2026 06:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants