Skip to content

Fix member-add bug + dept-aware org matching + Cassandra cleanups#212

Merged
mliu33 merged 36 commits into
mainfrom
claude/fix-member-subscription-bug-QZhjc
May 21, 2026
Merged

Fix member-add bug + dept-aware org matching + Cassandra cleanups#212
mliu33 merged 36 commits into
mainfrom
claude/fix-member-subscription-bug-QZhjc

Conversation

@vjauhari-work
Copy link
Copy Markdown
Collaborator

@vjauhari-work vjauhari-work commented May 20, 2026

Summary

  • Original bug: handleAddMembers silently dropped phantom orgs/users via CountNewMembers's $or filter; replaces with explicit validateOrgIDs / validateAccountsExist gates that return sentinel errors before publishing to the canonical stream. Same gates wired into handleCreateRoomChannel.
  • New feature: org membership now matches both sectId and deptId (with dept-first tiebreak in display formatting). pkg/pipelines.GetNewMembersPipeline updated; new (deptId, account) index mirrors the existing (sectId, account).
  • Schema cleanups: drop Room.CreatedBy and the unused Cassandra target_user column.
  • Encryption-keys follow-ups from PR feat: room encryption keys end-to-end #171: reconcileRoomOnDuplicateKey extracted (drops requester-must-be-member check so mid-write crashes recover via JetStream replay); pair-passing refactor in buildAndFanOutRoomKey; shouldRotate comment tightened.
  • Review-driven hardening: pkg/pipelines panic → error on empty roomID; backfill query errors now fail-hard instead of slog.Warn-and-continue (silent corruption risk in room_members); requestID propagated on the relocated member-add publish-error logs; _orgRaw BSON tag renamed to orgRaw with omitempty siblings.
  • Test discipline: AddMembers phantom validation consolidated into a table-driven test, parallel CreateRoomChannel phantom table added, store-error branch coverage added for both validators, backfill fail-hard tests added.

Test plan

  • make test — all unit tests pass with -race
  • make lint — 0 issues
  • make test-integration — verify against real Mongo/Cassandra/NATS
  • make sast — note: govulncheck currently fails on pre-existing history-service mock drift (internal/models import); not introduced by this branch
  • Manual smoke-test of chat.user.{account}.room.{roomID}.{siteID}.msg.add-members with a phantom org — should reject synchronously with errInvalidOrg

Notes

  • docs/client-api.md updated in the same diff: removes createdBy from POST /rooms and Room shape examples; adds error envelopes for Add Members and Create Room; documents dept-aware org-resolution for chat.user.{account}.request.orgs.{orgID}.members.
  • Skipped from review with rationale: CombineWithFallback rename (semantic naming lives in call-site wrappers); folding validators into CountNewMembers (defeats the bug-fix's point); MemberRemoved.SectNameOrgName rename (tracking item, wire-stable); ListByRoom projection (cascades through interface + mocks for a low finding).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Department-aware org matching for room membership and member discovery
  • Improvements

    • Faster DM creation deduplication — frontend navigates immediately when a DM already exists
    • Upfront validation to reject invalid org IDs and missing user accounts during room creation/add-members
    • Removed an unused message target field and removed creator metadata from room objects (storage/model cleanup)
  • Documentation

    • Updated API and Cassandra model docs to match these behavior and schema changes

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements dept-aware org matching and org→individual membership upgrades: new Mongo aggregation pipelines and store APIs, room-service request-time validators, room-worker candidate-driven add-members/backfill flow, Cassandra schema & Message model cleanup, frontend DM dedup shortcut, key-fanout refactor, and supporting tests/docs.

Changes

Org-to-individual membership and department-aware support

Layer / File(s) Summary
Model contracts and helper functions
pkg/model/user.go, pkg/model/room.go, pkg/model/member.go, room-worker/store.go, pkg/displayfmt/combine.go, pkg/displayfmt/combine_test.go, pkg/model/model_test.go
User model extended with dept fields; Room.CreatedBy removed; RemoveMemberRequest updated; AddMemberCandidate and OrgMemberStatus introduced; CombineWithFallback added and tested.
Member candidate and capacity aggregation pipelines
pkg/pipelines/member.go, pkg/pipelines/member_test.go
Added GetCapacityCheckPipeline and GetAddMemberCandidatesPipeline with shared matching helper and tests validating shapes and error behavior.
Store interface and MongoStore dept-aware changes
room-service/store.go, room-service/store_mongo.go, room-worker/store_mongo.go
RoomStore gains FindExistingOrgIDs/FindExistingAccounts; SubscriptionStore replaces ListNewMembers with ListAddMemberCandidates; MongoStore/MongoStore methods updated to match sectId OR deptId, add index, implement FindExisting* and ListAddMemberCandidates, and change enrichment to use display.orgRaw.
Room-service validation gates & handler changes
room-service/handler.go, room-service/handler_test.go
Added validateOrgIDs/validateAccountsExist called before capacity/count logic in AddMembers and CreateRoomChannel; removed roomkeymetrics KeyGenerated increment; tests updated with gomock preflight helpers and phantom validation cases.
Room-worker membership processing
room-worker/handler.go, room-worker/store.go, room-worker/store_mongo.go, room-worker/handler_test.go
processAddMembers refactored to partition candidates (needSub vs needIRM), minimal lookups, bulk writes/backfill for org→individual upgrades, gated fanout/events only on visible membership changes; many tests updated to candidate API and new behaviors.
Duplicate-key reconciliation & key fanout
room-worker/handler.go, room-worker/handler_test.go, pkg/roomkeymetrics/metrics.go
Added reconcileRoomOnDuplicateKey, removed CreatedBy on created rooms, changed buildAndFanOutRoomKey to accept fetched key pair, tightened rotate logic, and removed KeyGenerated/KeyRotated counters; tests updated accordingly.
System message display formatting
room-worker/sysmsg.go, room-worker/sysmsg_test.go
Delegated name combination to CombineWithFallback via new displayOrg helper; updated formatRemovedOrg signature and tests for dept/sect/fallback behavior.
Cassandra schema & history-service cleanup
docker-local/cassandra/init/*.cql, history-service/internal/cassrepo/*.go, pkg/model/cassandra/message.go, pkg/model/cassandra/message_test.go, docs/cassandra_message_model.md
Removed target_user column from message tables and related Message struct/fixtures; updated queries/tests/docs to match new schema.
Frontend DM creation dedup & types/docs
chat-frontend/src/api/types.ts, chat-frontend/src/api/fetchSidebarBuckets/index.ts, chat-frontend/src/components/.../CreateRoomDialog*
Removed createdBy from Room types/docs; CreateRoomDialog now short-circuits on "DM already exists" reply and navigates immediately; tests updated to assert immediate navigation.
Tests, mocks, images, docs, Makefile
various *_test.go/.jsx, room-service/mock_store_test.go, room-worker/mock_store_test.go, pkg/testutil/testimages/testimages.go, docker-local/compose.deps.yaml, Makefile, docs/*
Added/updated unit and integration tests, mock methods for new store APIs, pinned test images (mongo/valkey), Makefile govulncheck uses pinned GOTOOLCHAIN, and comprehensive plan/spec/doc updates.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • hmchangw/chat#84: Related to add-member pipeline/refactor and processAddMembers evolution.
  • hmchangw/chat#171: Related to room-key fanout and key-generation/metrics changes.
  • hmchangw/chat#142: Related DM/channel create-room idempotency and dedup flows.

Suggested reviewers

  • mliu33
  • ngangwar962
  • yenta

🐰 "I stitched names with a gentle hop,
Dept and sect together, then stop.
Phantom gates guard the tree,
DM dedup skips the wait for me,
Clean schemas make the meadow pop!"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-member-subscription-bug-QZhjc

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (1)
room-worker/store.go (1)

86-88: ⚡ Quick win

Update stale method-reference comment to the new contract.

The comment still says “ListNewMembers”, but this interface now uses ListAddMemberCandidates. Keeping this stale name will mislead future maintenance.

Suggested diff
-	// ListNewMembersForNewRoom is the empty-roomID variant of
-	// ListNewMembers — same dedup + bot filter, no "already-subscribed"
+	// ListNewMembersForNewRoom is the empty-roomID variant of
+	// ListAddMemberCandidates candidate resolution — same dedup + bot filter, no "already-subscribed"
🤖 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 `@room-worker/store.go` around lines 86 - 88, The method comment for
ListNewMembersForNewRoom is stale: it references "ListNewMembers" but the
interface now uses ListAddMemberCandidates; update the comment to mention
ListAddMemberCandidates as the related contract and adjust the description to
match the current behavior (dedup + bot filter, no already-subscribed pruning
for non-existent rooms, and note excludeAccount semantics) so future readers are
not misled by the old name.
🤖 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/plans/2026-05-19-member-add-improvements-plan.md`:
- Around line 1581-1583: The plan snippet still uses the stale key "_orgRaw"
(e.g., in the BSON projection) and mentions display._orgRaw; update the snippet
to use the new field name "orgRaw" instead (replace "_orgRaw" with "orgRaw") and
update any documentation references from display._orgRaw to display.orgRaw so
the plan matches the PR hardening notes and current schema.
- Around line 136-139: The unlabeled fenced code blocks containing command lines
like "make build SERVICE=room-worker" and "make build SERVICE=room-service"
should be updated to include a language identifier (e.g., ```sh or ```bash) so
markdownlint MD040 warnings go away and snippets are easier to scan; scan the
file for other unlabeled fences (notably ones showing shell, Go, or JSON
examples) and add the appropriate labels (```sh/```bash for shell commands,
```go for Go code, ```json for JSON) to each fenced block.

In
`@docs/superpowers/specs/2026-05-19-org-to-individual-membership-upgrade-design.md`:
- Around line 38-44: Several fenced code blocks in the spec are missing language
annotations which triggers markdownlint MD040; update each unlabeled fenced
block that contains Go pipeline signatures (notably the blocks with
GetCapacityCheckPipeline and GetAddMemberCandidatesPipeline) and the other
examples referenced (lines ~60-72, ~172-187, ~195-200) by adding the appropriate
language identifier (e.g., ```go for Go snippets, ```json or ```shell where
applicable) so the fences are annotated and linting/readability are fixed.

In `@Makefile`:
- Around line 118-121: Update the Makefile lines that invoke docker compose so
COMPOSE_BAKE is enabled: change the environment variable COMPOSE_BAKE from false
to true in both occurrences that prefix the docker compose commands (the lines
setting "COMPOSE_BAKE=false COMPOSE_PARALLEL_LIMIT=1 docker compose ..."),
keeping COMPOSE_PARALLEL_LIMIT=1 unchanged so builds remain serialized while
enabling Buildx Bake for reproducible builds.

In `@pkg/displayfmt/combine_test.go`:
- Around line 1-9: The test file currently declares package displayfmt_test
which violates the repo rule to use the same package for *_test.go files; change
the package declaration in combine_test.go from displayfmt_test to displayfmt so
the tests run in-package and can access unexported identifiers, and then update
any test references/imports (remove the local import of
github.com/hmchangw/chat/pkg/displayfmt if it's only used because of the _test
package) to match the new package.

In `@room-service/integration_test.go`:
- Around line 895-903: The test "matches dept users when orgId equals deptId
without parent sect match" is accidentally using SectID == "dept-x" for
dept-scoped users so the test can pass via sect match; update the test data
created with insertUser (model.User instances) so that the user meant to
exercise dept-only matching has a non-matching SectID (e.g., set SectID to
something other than the DeptID like "sect-other" or empty) while keeping DeptID
== "dept-x", and ensure the other user in the same test still reflects the
intended non-dept user; apply the same change to the similar test case covering
lines ~949-956 to remove SectID == DeptID coupling.

In `@room-worker/handler.go`:
- Line 1657: Replace the bare return of reconcileErr with a wrapped error that
preserves context and the original error (e.g., use fmt.Errorf("reconcile sync
failed: %w", reconcileErr)); locate the return that currently reads "return nil,
reconcileErr" and change it to return nil, fmt.Errorf("reconcile sync failed:
%w", reconcileErr) (ensure fmt is imported or use errors.Wrap-equivalent), so
call-site context is preserved in logs/traces.

---

Nitpick comments:
In `@room-worker/store.go`:
- Around line 86-88: The method comment for ListNewMembersForNewRoom is stale:
it references "ListNewMembers" but the interface now uses
ListAddMemberCandidates; update the comment to mention ListAddMemberCandidates
as the related contract and adjust the description to match the current behavior
(dedup + bot filter, no already-subscribed pruning for non-existent rooms, and
note excludeAccount semantics) so future readers are not misled by the old name.
🪄 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: 67166e5b-f66f-4b79-98db-5bc3dd4c5c2a

📥 Commits

Reviewing files that changed from the base of the PR and between fa2a5e0 and 6864cb3.

📒 Files selected for processing (48)
  • Makefile
  • chat-frontend/src/api/fetchSidebarBuckets/index.ts
  • chat-frontend/src/api/types.ts
  • chat-frontend/src/components/MainApp/Sidebar/CreateRoomDialog/CreateRoomDialog.jsx
  • chat-frontend/src/components/MainApp/Sidebar/CreateRoomDialog/CreateRoomDialog.test.jsx
  • docker-local/cassandra/init/10-table-messages_by_room.cql
  • docker-local/cassandra/init/11-table-thread_messages_by_room.cql
  • docker-local/cassandra/init/12-table-pinned_messages_by_room.cql
  • docker-local/cassandra/init/13-table-messages_by_id.cql
  • docs/cassandra_message_model.md
  • docs/client-api.md
  • docs/superpowers/plans/2026-05-19-member-add-improvements-plan.md
  • docs/superpowers/specs/2026-05-19-org-to-individual-membership-upgrade-design.md
  • history-service/docker-local/docker-compose.yml
  • history-service/internal/cassrepo/integration_test.go
  • history-service/internal/cassrepo/messages_by_id_integration_test.go
  • history-service/internal/cassrepo/messages_by_room.go
  • history-service/internal/cassrepo/thread_messages.go
  • history-service/internal/cassrepo/thread_messages_integration_test.go
  • history-service/internal/service/integration_test.go
  • inbox-worker/handler_test.go
  • pkg/displayfmt/combine.go
  • pkg/displayfmt/combine_test.go
  • pkg/model/cassandra/message.go
  • pkg/model/cassandra/message_test.go
  • pkg/model/model_test.go
  • pkg/model/room.go
  • pkg/model/user.go
  • pkg/pipelines/member.go
  • pkg/pipelines/member_test.go
  • pkg/roomkeymetrics/metrics.go
  • pkg/roomkeystore/integration_test.go
  • pkg/roomsubcache/integration_test.go
  • pkg/testutil/testimages/testimages.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
  • room-worker/handler.go
  • room-worker/handler_test.go
  • room-worker/integration_test.go
  • room-worker/mock_store_test.go
  • room-worker/store.go
  • room-worker/store_mongo.go
  • room-worker/sysmsg.go
  • room-worker/sysmsg_test.go
💤 Files with no reviewable changes (13)
  • docker-local/cassandra/init/10-table-messages_by_room.cql
  • pkg/model/room.go
  • history-service/internal/cassrepo/integration_test.go
  • docker-local/cassandra/init/11-table-thread_messages_by_room.cql
  • pkg/model/cassandra/message_test.go
  • chat-frontend/src/api/types.ts
  • pkg/model/cassandra/message.go
  • docs/cassandra_message_model.md
  • docker-local/cassandra/init/12-table-pinned_messages_by_room.cql
  • docker-local/cassandra/init/13-table-messages_by_id.cql
  • chat-frontend/src/api/fetchSidebarBuckets/index.ts
  • history-service/docker-local/docker-compose.yml
  • pkg/roomkeymetrics/metrics.go

Comment thread docs/superpowers/plans/2026-05-19-member-add-improvements-plan.md Outdated
Comment thread docs/superpowers/plans/2026-05-19-member-add-improvements-plan.md Outdated
Comment thread Makefile Outdated
Comment thread pkg/displayfmt/combine_test.go Outdated
Comment thread room-service/integration_test.go
Comment thread room-worker/handler.go Outdated
@vjauhari-work vjauhari-work force-pushed the claude/fix-member-subscription-bug-QZhjc branch from 6864cb3 to 3df2b19 Compare May 20, 2026 10:25
vjauhari-work added a commit that referenced this pull request May 20, 2026
- pkg/displayfmt/combine_test.go: switch to in-package tests (`package
  displayfmt`) per CLAUDE.md §4 rule; drop the now-unused local import.
- room-worker/handler.go: wrap bare `return nil, reconcileErr` in
  processSyncCreateDM's dup-key reconcile branch with sync-DM context
  (CLAUDE.md §3 "Never return bare err").
- room-service/integration_test.go: make two dept-only test cases actually
  dept-only by setting SectID to a non-matching value (was `dept-x`, so
  the tests would have passed via sectId match alone — defeating the
  intent of "dept-only branch is doing its job").
- room-worker/store.go: refresh stale doc on ListNewMembersForNewRoom —
  refer to ListAddMemberCandidates (the current contract) instead of the
  removed ListNewMembers.
- docs/superpowers: rename stale `_orgRaw` → `orgRaw` in both the plan
  snippet and the spec narrative to match the renamed BSON tag; add
  language identifiers (```sh) to the four shell-command fenced blocks
  the reviewer called out in the plan.

Skipped intentionally:
- Makefile COMPOSE_BAKE finding: the user explicitly asked to REMOVE
  COMPOSE_BAKE/COMPOSE_PARALLEL_LIMIT (commit 3df2b19); bot's suggestion
  to set COMPOSE_BAKE=true contradicts that directive.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@vjauhari-work
Copy link
Copy Markdown
Collaborator Author

@coderabbitai full-review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

@vjauhari-work Sure! I'll kick off a full review of PR #212 right away.

✅ Actions performed

Full review triggered.

@vjauhari-work vjauhari-work requested a review from ngangwar962 May 20, 2026 10:48
vjauhari-work added a commit that referenced this pull request May 21, 2026
- pkg/displayfmt/combine_test.go: switch to in-package tests (`package
  displayfmt`) per CLAUDE.md §4 rule; drop the now-unused local import.
- room-worker/handler.go: wrap bare `return nil, reconcileErr` in
  processSyncCreateDM's dup-key reconcile branch with sync-DM context
  (CLAUDE.md §3 "Never return bare err").
- room-service/integration_test.go: make two dept-only test cases actually
  dept-only by setting SectID to a non-matching value (was `dept-x`, so
  the tests would have passed via sectId match alone — defeating the
  intent of "dept-only branch is doing its job").
- room-worker/store.go: refresh stale doc on ListNewMembersForNewRoom —
  refer to ListAddMemberCandidates (the current contract) instead of the
  removed ListNewMembers.
- docs/superpowers: rename stale `_orgRaw` → `orgRaw` in both the plan
  snippet and the spec narrative to match the renamed BSON tag; add
  language identifiers (```sh) to the four shell-command fenced blocks
  the reviewer called out in the plan.

Skipped intentionally:
- Makefile COMPOSE_BAKE finding: the user explicitly asked to REMOVE
  COMPOSE_BAKE/COMPOSE_PARALLEL_LIMIT (commit 3df2b19); bot's suggestion
  to set COMPOSE_BAKE=true contradicts that directive.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@vjauhari-work vjauhari-work force-pushed the claude/fix-member-subscription-bug-QZhjc branch from 2f1bad1 to a4730cc Compare May 21, 2026 01:50
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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)
docs/client-api.md (1)

196-203: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Create Room docs now reference undocumented request fields (orgs/users).

Line 249 introduces "invalid org" / "user not found" for channel-create inputs, but the request schema above doesn’t document orgs or users. Please update the Create Room request-body table/example to include these fields (or remove that error note if they are not accepted) so the contract is self-consistent.

Also applies to: 249-249

🤖 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/client-api.md` around lines 196 - 203, The Create Room docs refer to
errors for "invalid org" / "user not found" but the request-body table for the
Create Room endpoint omits the `orgs` and `users` fields; either add `orgs` and
`users` to the table with their types (e.g., `orgs`: string[] optional, `users`:
string[] optional) and notes on validation (when `type=channel`/membership
checks) or remove the error text that references them; update the Create Room
request-body example and the table entries near the `members`, `type`, and
`createdByAccount` fields so the schema and the error notes are consistent with
each other.
🧹 Nitpick comments (1)
pkg/testutil/testimages/testimages.go (1)

23-32: ⚡ Quick win

Prefer pinning immutable container image tags for integration-test determinism.

pkg/testutil/testimages/testimages.go and docker-local/compose.deps.yaml both use floating tags (mongo:8, valkey/valkey:8-alpine). Pin exact patch versions (or image digests) to prevent CI drift across time.

🤖 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 `@pkg/testutil/testimages/testimages.go` around lines 23 - 32, The test images
are using floating tags causing CI drift; update the Mongo constant and any
other floating test image references (e.g., the Mongo variable in
pkg/testutil/testimages/testimages.go and the valkey image in
docker-local/compose.deps.yaml) to pinned immutable identifiers — preferably
exact patch tags or image digests — by replacing "mongo:8" with a specific
pinned tag (or digest) for the current CI baseline and doing the same for
"valkey/valkey:8-alpine" in compose.deps.yaml; ensure you document the chosen
pinned versions in a comment near the Mongo constant and update any CI/README
notes that reference the floating tags.
🤖 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 `@docs/client-api.md`:
- Around line 196-203: The Create Room docs refer to errors for "invalid org" /
"user not found" but the request-body table for the Create Room endpoint omits
the `orgs` and `users` fields; either add `orgs` and `users` to the table with
their types (e.g., `orgs`: string[] optional, `users`: string[] optional) and
notes on validation (when `type=channel`/membership checks) or remove the error
text that references them; update the Create Room request-body example and the
table entries near the `members`, `type`, and `createdByAccount` fields so the
schema and the error notes are consistent with each other.

---

Nitpick comments:
In `@pkg/testutil/testimages/testimages.go`:
- Around line 23-32: The test images are using floating tags causing CI drift;
update the Mongo constant and any other floating test image references (e.g.,
the Mongo variable in pkg/testutil/testimages/testimages.go and the valkey image
in docker-local/compose.deps.yaml) to pinned immutable identifiers — preferably
exact patch tags or image digests — by replacing "mongo:8" with a specific
pinned tag (or digest) for the current CI baseline and doing the same for
"valkey/valkey:8-alpine" in compose.deps.yaml; ensure you document the chosen
pinned versions in a comment near the Mongo constant and update any CI/README
notes that reference the floating tags.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d5d4360-7578-41c7-812a-6e02ad97ec29

📥 Commits

Reviewing files that changed from the base of the PR and between 6864cb3 and a4730cc.

📒 Files selected for processing (46)
  • Makefile
  • chat-frontend/src/api/fetchSidebarBuckets/index.ts
  • chat-frontend/src/api/types.ts
  • chat-frontend/src/components/MainApp/Sidebar/CreateRoomDialog/CreateRoomDialog.jsx
  • chat-frontend/src/components/MainApp/Sidebar/CreateRoomDialog/CreateRoomDialog.test.jsx
  • docker-local/cassandra/init/10-table-messages_by_room.cql
  • docker-local/cassandra/init/11-table-thread_messages_by_room.cql
  • docker-local/cassandra/init/12-table-pinned_messages_by_room.cql
  • docker-local/cassandra/init/13-table-messages_by_id.cql
  • docs/cassandra_message_model.md
  • docs/client-api.md
  • docs/superpowers/plans/2026-05-19-member-add-improvements-plan.md
  • docs/superpowers/specs/2026-05-19-org-to-individual-membership-upgrade-design.md
  • history-service/docker-local/docker-compose.yml
  • history-service/internal/cassrepo/integration_test.go
  • history-service/internal/cassrepo/messages_by_id_integration_test.go
  • history-service/internal/cassrepo/messages_by_room.go
  • history-service/internal/cassrepo/thread_messages.go
  • history-service/internal/cassrepo/thread_messages_integration_test.go
  • history-service/internal/service/integration_test.go
  • inbox-worker/handler_test.go
  • pkg/displayfmt/combine.go
  • pkg/displayfmt/combine_test.go
  • pkg/model/cassandra/message.go
  • pkg/model/cassandra/message_test.go
  • pkg/model/model_test.go
  • pkg/model/room.go
  • pkg/model/user.go
  • pkg/pipelines/member.go
  • pkg/pipelines/member_test.go
  • pkg/roomkeymetrics/metrics.go
  • pkg/testutil/testimages/testimages.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
  • room-worker/handler.go
  • room-worker/handler_test.go
  • room-worker/integration_test.go
  • room-worker/mock_store_test.go
  • room-worker/store.go
  • room-worker/store_mongo.go
  • room-worker/sysmsg.go
  • room-worker/sysmsg_test.go
💤 Files with no reviewable changes (13)
  • pkg/model/cassandra/message.go
  • docker-local/cassandra/init/13-table-messages_by_id.cql
  • docker-local/cassandra/init/12-table-pinned_messages_by_room.cql
  • history-service/docker-local/docker-compose.yml
  • pkg/model/cassandra/message_test.go
  • history-service/internal/cassrepo/integration_test.go
  • pkg/model/room.go
  • pkg/roomkeymetrics/metrics.go
  • docker-local/cassandra/init/11-table-thread_messages_by_room.cql
  • chat-frontend/src/api/types.ts
  • docs/cassandra_message_model.md
  • docker-local/cassandra/init/10-table-messages_by_room.cql
  • chat-frontend/src/api/fetchSidebarBuckets/index.ts
✅ Files skipped from review due to trivial changes (2)
  • room-worker/mock_store_test.go
  • room-service/mock_store_test.go

vjauhari-work pushed a commit that referenced this pull request May 21, 2026
Close the consistency gap CodeRabbit flagged on PR #212: the Create
Room request schema was missing users/orgs/channels even though the
handler accepts them and the same section's error envelope mentions
"invalid org" / "user not found" rejections for those exact fields.
Add them to the request-body table (marked channel-only and optional,
matching the gate the worker enforces) and update the example JSON so
the schema, example, and error envelope agree.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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/client-api.md`:
- Around line 203-205: Update the create-room documentation so its behavior
matches the new request schema: replace the sentence that currently claims
creation "only enrolls the owner" with a description that creation will enroll
members provided via the users, orgs, and channels fields (listing each field
name and briefly how they are processed and their error responses: users =>
internal user IDs, rejects with "user not found"; orgs => org IDs expanded to
members, rejects with "invalid org" if zero users; channels => ChannelRef copies
members from specified room/site). Ensure the create-room section references the
same enrollment semantics and error messages described in the
users/orgs/channels table so clients implement the correct behavior.
- Around line 200-201: The docs incorrectly mark createdByAccount as required;
update the API table and any create-room documentation to mark
`createdByAccount` as optional (or "no") and add a short note that the
create-room flow derives the creator from the request subject (e.g.,
"create-room uses subject account `chat.user.{account}.request...` so
`createdByAccount` is not required for backend integration requests"); reference
the `createdByAccount` field and the `create-room` flow in the update so clients
know the server uses the subject account and that `siteId` remains required.
🪄 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: 15c7557d-1463-4181-982e-6a71601991ee

📥 Commits

Reviewing files that changed from the base of the PR and between a4730cc and d531215.

📒 Files selected for processing (10)
  • docs/client-api.md
  • room-service/integration_test.go
  • room-service/mock_store_test.go
  • room-service/store.go
  • room-service/store_mongo.go
  • room-worker/integration_test.go
  • room-worker/mock_store_test.go
  • room-worker/store.go
  • room-worker/store_mongo.go
  • tools/loadgen/main_test.go
💤 Files with no reviewable changes (6)
  • room-service/store.go
  • room-service/mock_store_test.go
  • room-worker/store_mongo.go
  • room-worker/store.go
  • room-worker/mock_store_test.go
  • room-service/store_mongo.go

Comment thread docs/client-api.md Outdated
Comment thread docs/client-api.md
vjauhari-work added a commit that referenced this pull request May 21, 2026
- pkg/displayfmt/combine_test.go: switch to in-package tests (`package
  displayfmt`) per CLAUDE.md §4 rule; drop the now-unused local import.
- room-worker/handler.go: wrap bare `return nil, reconcileErr` in
  processSyncCreateDM's dup-key reconcile branch with sync-DM context
  (CLAUDE.md §3 "Never return bare err").
- room-service/integration_test.go: make two dept-only test cases actually
  dept-only by setting SectID to a non-matching value (was `dept-x`, so
  the tests would have passed via sectId match alone — defeating the
  intent of "dept-only branch is doing its job").
- room-worker/store.go: refresh stale doc on ListNewMembersForNewRoom —
  refer to ListAddMemberCandidates (the current contract) instead of the
  removed ListNewMembers.
- docs/superpowers: rename stale `_orgRaw` → `orgRaw` in both the plan
  snippet and the spec narrative to match the renamed BSON tag; add
  language identifiers (```sh) to the four shell-command fenced blocks
  the reviewer called out in the plan.

Skipped intentionally:
- Makefile COMPOSE_BAKE finding: the user explicitly asked to REMOVE
  COMPOSE_BAKE/COMPOSE_PARALLEL_LIMIT (commit 3df2b19); bot's suggestion
  to set COMPOSE_BAKE=true contradicts that directive.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
vjauhari-work pushed a commit that referenced this pull request May 21, 2026
Close the consistency gap CodeRabbit flagged on PR #212: the Create
Room request schema was missing users/orgs/channels even though the
handler accepts them and the same section's error envelope mentions
"invalid org" / "user not found" rejections for those exact fields.
Add them to the request-body table (marked channel-only and optional,
matching the gate the worker enforces) and update the example JSON so
the schema, example, and error envelope agree.
@vjauhari-work vjauhari-work force-pushed the claude/fix-member-subscription-bug-QZhjc branch from d531215 to c1e1e94 Compare May 21, 2026 03:08
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
docs/client-api.md (1)

202-206: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Create Room behavior text is still internally inconsistent.

Lines 202–206 and Line 255 now document member enrollment during create, but Line 263 still says “creating a room only enrolls the owner.” Please align that sentence with the updated request semantics.

Also applies to: 255-255

🤖 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/client-api.md` around lines 202 - 206, Update the outdated sentence that
reads "creating a room only enrolls the owner." to match the new Create Room
semantics described earlier (the `users`, `orgs`, and `channels` body fields and
the `{account}` subject segment). Replace that sentence with a concise statement
that creating a room enrolls the owner (from chat.user.{account} subject) and
any additional members specified via the `users`, `orgs`, or `channels` fields
(with the same rejection behaviors already documented).
🤖 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-05-19-org-to-individual-membership-upgrade-design.md`:
- Around line 259-260: The doc contains conflicting statements about the
`_orgMatch` $lookup inner pipeline stage count (one place says 3 stages:
match→addFields→group, another says 6 stages); decide which is correct and make
both references consistent: update the description at the earlier mention (the
paragraph that claims "stays at 3 stages — match → addFields → group") and the
later mention (the paragraph that claims "grows to 6 stages") to reflect the
true pipeline shape used by the `_orgMatch` $lookup, and if the pipeline has
changed, also update the emitted shape note (`{name, tcName, memberCount}`) and
the statement about Go decode using `combineWithFallback` so all three places
describe the same stage count and output shape.

In `@room-worker/handler.go`:
- Around line 935-950: The backfill branch assumes FindUsersByAccounts(ctx,
backfillAccounts) returns every requested account but it can return a subset;
modify the logic in the handler where backfillUsers and backfillAccounts are
used (around the loop that appends to roomMembers) to detect missing accounts
and return an error instead of proceeding: after obtaining backfillUsers, build
a set of returned accounts/IDs, compare its size or membership against
backfillAccounts, and if any requested account is missing return a clear error
(e.g., "missing backfill user(s) for accounts ...") so BulkCreateRoomMembers and
later hadOrgsBefore cannot commit a partial backfill that becomes irrecoverable.
Ensure you reference FindUsersByAccounts, backfillAccounts, backfillUsers,
roomMembers, BulkCreateRoomMembers and fail fast before appending or committing.

---

Duplicate comments:
In `@docs/client-api.md`:
- Around line 202-206: Update the outdated sentence that reads "creating a room
only enrolls the owner." to match the new Create Room semantics described
earlier (the `users`, `orgs`, and `channels` body fields and the `{account}`
subject segment). Replace that sentence with a concise statement that creating a
room enrolls the owner (from chat.user.{account} subject) and any additional
members specified via the `users`, `orgs`, or `channels` fields (with the same
rejection behaviors already documented).
🪄 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: aa202e5c-0c58-468f-bc66-0526f423fd6e

📥 Commits

Reviewing files that changed from the base of the PR and between d531215 and 5a8a0d6.

📒 Files selected for processing (47)
  • Makefile
  • chat-frontend/src/api/fetchSidebarBuckets/index.ts
  • chat-frontend/src/api/types.ts
  • chat-frontend/src/components/MainApp/Sidebar/CreateRoomDialog/CreateRoomDialog.jsx
  • chat-frontend/src/components/MainApp/Sidebar/CreateRoomDialog/CreateRoomDialog.test.jsx
  • docker-local/cassandra/init/10-table-messages_by_room.cql
  • docker-local/cassandra/init/11-table-thread_messages_by_room.cql
  • docker-local/cassandra/init/12-table-pinned_messages_by_room.cql
  • docker-local/cassandra/init/13-table-messages_by_id.cql
  • docs/cassandra_message_model.md
  • docs/client-api.md
  • docs/superpowers/plans/2026-05-19-member-add-improvements-plan.md
  • docs/superpowers/spec.md
  • docs/superpowers/specs/2026-05-19-org-to-individual-membership-upgrade-design.md
  • history-service/internal/cassrepo/integration_test.go
  • history-service/internal/cassrepo/messages_by_id_integration_test.go
  • history-service/internal/cassrepo/messages_by_room.go
  • history-service/internal/cassrepo/thread_messages.go
  • history-service/internal/cassrepo/thread_messages_integration_test.go
  • history-service/internal/service/integration_test.go
  • inbox-worker/handler_test.go
  • pkg/displayfmt/combine.go
  • pkg/displayfmt/combine_test.go
  • pkg/model/cassandra/message.go
  • pkg/model/cassandra/message_test.go
  • pkg/model/member.go
  • pkg/model/model_test.go
  • pkg/model/room.go
  • pkg/model/user.go
  • pkg/pipelines/member.go
  • pkg/pipelines/member_test.go
  • pkg/roomkeymetrics/metrics.go
  • pkg/testutil/testimages/testimages.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
  • room-worker/handler.go
  • room-worker/handler_test.go
  • room-worker/integration_test.go
  • room-worker/mock_store_test.go
  • room-worker/store.go
  • room-worker/store_mongo.go
  • room-worker/sysmsg.go
  • room-worker/sysmsg_test.go
💤 Files with no reviewable changes (14)
  • docs/superpowers/spec.md
  • pkg/model/cassandra/message.go
  • docker-local/cassandra/init/12-table-pinned_messages_by_room.cql
  • docs/cassandra_message_model.md
  • pkg/model/cassandra/message_test.go
  • docker-local/cassandra/init/13-table-messages_by_id.cql
  • docker-local/cassandra/init/10-table-messages_by_room.cql
  • pkg/model/member.go
  • history-service/internal/cassrepo/integration_test.go
  • chat-frontend/src/api/fetchSidebarBuckets/index.ts
  • pkg/model/room.go
  • chat-frontend/src/api/types.ts
  • pkg/roomkeymetrics/metrics.go
  • docker-local/cassandra/init/11-table-thread_messages_by_room.cql
✅ Files skipped from review due to trivial changes (3)
  • Makefile
  • history-service/internal/cassrepo/thread_messages.go
  • room-worker/mock_store_test.go

Comment thread room-worker/handler.go
vjauhari-work pushed a commit that referenced this pull request May 21, 2026
room-worker/handler.go (Major):
  Backfill in processAddMembers fed FindUsersByAccounts the existing
  subscriber accounts and assumed the result covered every requested
  one. When a stale subscription points at a deleted user document the
  call returns a subset; the buggy code committed room_members for the
  rows it got, BulkCreateRoomMembers flipped hadOrgsBefore=true, and
  no future redelivery could ever repair the missing IRM rows (backfill
  only fires on the first-org transition). Add a fail-hard guard that
  compares returned accounts against the requested set and returns
  newPermanent("backfill user %s not found ...") before any write. New
  regression test in handler_test.go locks in the contract.

docs/client-api.md:
  Replace the stale "creating a room only enrolls the owner" line — it
  contradicts the request schema introduced earlier this PR, which now
  documents users/orgs/channels enrollment on create.

docs/superpowers/specs/2026-05-19-...-upgrade-design.md:
  Reconcile contradictory stage-count claims for the _orgMatch $lookup
  inner pipeline. The later "grows to 6 stages" sentence was wrong;
  verified against room-service/store_mongo.go:520-552 — the pipeline
  is 3 stages ($match → $addFields → $group), same as before. The
  $group merely widens its accumulator set.

pkg/testutil/testimages/testimages.go + docker-local/compose.deps.yaml:
  Replace the floating "mongo:8" / "valkey/valkey:8-alpine" tags with
  patch-pinned mongo:8.2.9 / valkey/valkey:8.1.7-alpine so testcontainers
  can't drift across patch releases between CI runs.
vjauhari-work added a commit that referenced this pull request May 21, 2026
- pkg/displayfmt/combine_test.go: switch to in-package tests (`package
  displayfmt`) per CLAUDE.md §4 rule; drop the now-unused local import.
- room-worker/handler.go: wrap bare `return nil, reconcileErr` in
  processSyncCreateDM's dup-key reconcile branch with sync-DM context
  (CLAUDE.md §3 "Never return bare err").
- room-service/integration_test.go: make two dept-only test cases actually
  dept-only by setting SectID to a non-matching value (was `dept-x`, so
  the tests would have passed via sectId match alone — defeating the
  intent of "dept-only branch is doing its job").
- room-worker/store.go: refresh stale doc on ListNewMembersForNewRoom —
  refer to ListAddMemberCandidates (the current contract) instead of the
  removed ListNewMembers.
- docs/superpowers: rename stale `_orgRaw` → `orgRaw` in both the plan
  snippet and the spec narrative to match the renamed BSON tag; add
  language identifiers (```sh) to the four shell-command fenced blocks
  the reviewer called out in the plan.

Skipped intentionally:
- Makefile COMPOSE_BAKE finding: the user explicitly asked to REMOVE
  COMPOSE_BAKE/COMPOSE_PARALLEL_LIMIT (commit 3df2b19); bot's suggestion
  to set COMPOSE_BAKE=true contradicts that directive.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
vjauhari-work pushed a commit that referenced this pull request May 21, 2026
Close the consistency gap CodeRabbit flagged on PR #212: the Create
Room request schema was missing users/orgs/channels even though the
handler accepts them and the same section's error envelope mentions
"invalid org" / "user not found" rejections for those exact fields.
Add them to the request-body table (marked channel-only and optional,
matching the gate the worker enforces) and update the example JSON so
the schema, example, and error envelope agree.
vjauhari-work pushed a commit that referenced this pull request May 21, 2026
room-worker/handler.go (Major):
  Backfill in processAddMembers fed FindUsersByAccounts the existing
  subscriber accounts and assumed the result covered every requested
  one. When a stale subscription points at a deleted user document the
  call returns a subset; the buggy code committed room_members for the
  rows it got, BulkCreateRoomMembers flipped hadOrgsBefore=true, and
  no future redelivery could ever repair the missing IRM rows (backfill
  only fires on the first-org transition). Add a fail-hard guard that
  compares returned accounts against the requested set and returns
  newPermanent("backfill user %s not found ...") before any write. New
  regression test in handler_test.go locks in the contract.

docs/client-api.md:
  Replace the stale "creating a room only enrolls the owner" line — it
  contradicts the request schema introduced earlier this PR, which now
  documents users/orgs/channels enrollment on create.

docs/superpowers/specs/2026-05-19-...-upgrade-design.md:
  Reconcile contradictory stage-count claims for the _orgMatch $lookup
  inner pipeline. The later "grows to 6 stages" sentence was wrong;
  verified against room-service/store_mongo.go:520-552 — the pipeline
  is 3 stages ($match → $addFields → $group), same as before. The
  $group merely widens its accumulator set.

pkg/testutil/testimages/testimages.go + docker-local/compose.deps.yaml:
  Replace the floating "mongo:8" / "valkey/valkey:8-alpine" tags with
  patch-pinned mongo:8.2.9 / valkey/valkey:8.1.7-alpine so testcontainers
  can't drift across patch releases between CI runs.
@vjauhari-work vjauhari-work force-pushed the claude/fix-member-subscription-bug-QZhjc branch from 109bbc5 to d963eb8 Compare May 21, 2026 06:29
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/client-api.md`:
- Line 262: Update the Room schema documentation: change the userCount field
note (in the Room schema table) to indicate that userCount defaults to 1 for
owner-only creates but may be greater than 1 when create-room includes initial
members via users, orgs, or channels; reference the create-room behavior and the
userCount field so the table text explicitly states "1 (owner) or higher if
initial members were provided at creation."
🪄 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: 0e9d88b7-d31b-4cf0-beb0-99153f8c047c

📥 Commits

Reviewing files that changed from the base of the PR and between 5a8a0d6 and d963eb8.

📒 Files selected for processing (48)
  • Makefile
  • chat-frontend/src/api/fetchSidebarBuckets/index.ts
  • chat-frontend/src/api/types.ts
  • chat-frontend/src/components/MainApp/Sidebar/CreateRoomDialog/CreateRoomDialog.jsx
  • chat-frontend/src/components/MainApp/Sidebar/CreateRoomDialog/CreateRoomDialog.test.jsx
  • docker-local/cassandra/init/10-table-messages_by_room.cql
  • docker-local/cassandra/init/11-table-thread_messages_by_room.cql
  • docker-local/cassandra/init/12-table-pinned_messages_by_room.cql
  • docker-local/cassandra/init/13-table-messages_by_id.cql
  • docker-local/compose.deps.yaml
  • docs/cassandra_message_model.md
  • docs/client-api.md
  • docs/superpowers/plans/2026-05-19-member-add-improvements-plan.md
  • docs/superpowers/spec.md
  • docs/superpowers/specs/2026-05-19-org-to-individual-membership-upgrade-design.md
  • history-service/internal/cassrepo/integration_test.go
  • history-service/internal/cassrepo/messages_by_id_integration_test.go
  • history-service/internal/cassrepo/messages_by_room.go
  • history-service/internal/cassrepo/thread_messages.go
  • history-service/internal/cassrepo/thread_messages_integration_test.go
  • history-service/internal/service/integration_test.go
  • inbox-worker/handler_test.go
  • pkg/displayfmt/combine.go
  • pkg/displayfmt/combine_test.go
  • pkg/model/cassandra/message.go
  • pkg/model/cassandra/message_test.go
  • pkg/model/member.go
  • pkg/model/model_test.go
  • pkg/model/room.go
  • pkg/model/user.go
  • pkg/pipelines/member.go
  • pkg/pipelines/member_test.go
  • pkg/roomkeymetrics/metrics.go
  • pkg/testutil/testimages/testimages.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
  • room-worker/handler.go
  • room-worker/handler_test.go
  • room-worker/integration_test.go
  • room-worker/mock_store_test.go
  • room-worker/store.go
  • room-worker/store_mongo.go
  • room-worker/sysmsg.go
  • room-worker/sysmsg_test.go
💤 Files with no reviewable changes (14)
  • pkg/model/room.go
  • docker-local/cassandra/init/10-table-messages_by_room.cql
  • chat-frontend/src/api/types.ts
  • pkg/model/cassandra/message.go
  • docs/superpowers/spec.md
  • docker-local/cassandra/init/13-table-messages_by_id.cql
  • pkg/model/cassandra/message_test.go
  • docker-local/cassandra/init/12-table-pinned_messages_by_room.cql
  • history-service/internal/cassrepo/integration_test.go
  • docker-local/cassandra/init/11-table-thread_messages_by_room.cql
  • pkg/model/member.go
  • chat-frontend/src/api/fetchSidebarBuckets/index.ts
  • docs/cassandra_message_model.md
  • pkg/roomkeymetrics/metrics.go
✅ Files skipped from review due to trivial changes (4)
  • pkg/displayfmt/combine.go
  • room-worker/mock_store_test.go
  • inbox-worker/handler_test.go
  • room-service/mock_store_test.go

Comment thread docs/client-api.md
Comment thread room-service/store_mongo.go Outdated
claude added 7 commits May 21, 2026 09:39
… cleanups

Five-part spec, perf-hardened per second /simplify pass.

Parts 1-3: org→individual silent no-op fix, deptId support with
prefer-dept-on-overlap, and CreateRoomDialog dedup-reply short-circuit.
New GetCapacityCheckPipeline + GetAddMemberCandidatesPipeline split
capacity-counting (3 stages, no room_members lookup) from full-candidate
resolution (4 stages, room_members lookup via member.id covered by the
existing unique index). New (deptId, account) index keeps both branches
of the extended $or index-covered. Enrichment _orgMatch pipeline stays
at 3 inner stages by emitting raw {name, tcName} and combining in Go;
combineWithFallback moves to pkg/displayfmt as the single source of
truth for both sys-msg and member-list display.

Part 4: drops Room.CreatedBy and rewrites the JetStream replay-equivalence
check to use a reusable reconcileRoomOnDuplicateKey helper. The new check
verifies (Type, SiteID) structural match and requester-sub-exists; this
fixes DM concurrent-create (today incorrectly rejected as "room ID
collision") while still catching real ID collisions (requester has no sub
in the unrelated existing room).

Part 5: drops never-written target_user column from all four Cassandra
message tables, the Go struct, history-service read-side baseColumns,
and the docs.

All new queries verified index-covered; performance/indexes table in
Part 2 enumerates every read path.
14 tasks across five parts mirroring the spec, hardened per the second
/simplify pass.

Notable updates over the first draft:

- pkg/displayfmt shared package introduced in Task 7 — single source of
  truth for CombineWithFallback, consumed by both room-worker (sys-msgs)
  and room-service (member-list enrichment).

- Task 11 enrichment pipeline kept at 3 inner stages (vs. the earlier
  6-stage draft) by emitting raw {name, tcName} and applying the
  combine in Go after cursor.All.

- Task 13 (Room.CreatedBy removal) extracts the duplicate-key recovery
  into a single Handler.reconcileRoomOnDuplicateKey helper used by both
  processCreateRoom and the sync-DM path. Adds a "no extra writes"
  pre/post snapshot assertion to guard against silent-dedup false
  passes. Frontend and docs/client-api.md grep uses word-boundary regex
  to avoid stripping the unrelated createdByAccount field.

- Task 14 (target_user removal) adds explicit test-fixture cleanup
  steps covering pkg/model/cassandra/message_test.go and the two
  history-service integration tests that today assert on .TargetUser.

TDD per task: failing test → minimal implementation → green → commit.
Each task is independently committable and reviewable.
Adds AddMemberCandidate type, ListAddMemberCandidates to SubscriptionStore
interface and MongoStore implementation backed by GetAddMemberCandidatesPipeline,
returning per-user {hasSubscription, hasIndividualRoomMember} flags so the
handler can split candidates into needSub vs needIRM upgrade paths. Regenerates
mock and adds integration test covering the bug scenario (sub-without-IRM) plus
new-member and full-member states. Also includes room-service CountNewMembers
integration test from the preceding task that was not yet committed.

https://claude.ai/code/session_01Wn4D4EcfZ5nKAuF6vbSBsA
…wait

When the server replies with "dm already exists", the handleSubmit now
calls onCreated+onClose immediately and returns, bypassing setPendingRoom
entirely. This prevents the 3-second "taking longer than expected" banner
from firing on a BUCKETS_LOADED race where summaries is still empty at
the moment the dedup response lands.

https://claude.ai/code/session_01Wn4D4EcfZ5nKAuF6vbSBsA
vjauhari-work and others added 14 commits May 21, 2026 09:41
Both member.add and channel-create silently accepted orgs/users with no
backing user document. The candidates aggregation dropped them, so the
async-job reply reported success while the worker either wrote a
room_members row for a zero-user org (firing a spurious members_added
sys-msg) or skipped the targeted user entirely.

Gate at the room-service request boundary with two new store methods
(FindExistingOrgIDs, FindExistingAccounts) and two handler helpers
(validateOrgIDs, validateAccountsExist). errInvalidOrg / errUserNotFound
sentinels were already in sanitizeError's allow-list; clients now see a
clean synchronous error envelope and nothing reaches the canonical stream.

Also folds in two follow-ups from the prior review pass:
- Mongo testimage comment now records that the retired 4.4.15 guard
  against partialFilterExpression operator-allow-list regressions has
  been intentionally given up under the mongo:8 bump.
- ListOrgMembers integration test gains a "neither sectId nor deptId
  matches" sub-test so dedupe across the broadened \$or is exercised.

Spec: docs/superpowers/specs/2026-05-19-org-to-individual-membership-upgrade-design.md Part 6
Plan: docs/superpowers/plans/2026-05-19-member-add-improvements-plan.md Tasks 15

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three unresolved threads from @mliu33 on the merged room-encryption-keys PR.

1. shouldRotate guard (room-worker/handler.go:319): kept the condition; the
   premise that "JetStream won't redeliver after rotate succeeds" misses
   three NAK sites in processRemoveIndividual (GetUser, sys-msg publish,
   cross-site outbox) and the symmetric ones in processRemoveOrg. Without
   the guard, redelivery re-runs rotateAndFanOut which calls
   GenerateKeyPair() — fresh random bytes — and survivors briefly offline
   during the second fan-out are stranded on the prior key. Tightened the
   inline comment to record the rationale; no behavior change.

2. buildAndFanOutRoomKey redundant Get (room-worker/handler.go:1789):
   accepted. Refactored to take *VersionedKeyPair as a parameter and
   threaded the gate-Get pair from processCreateRoom through
   processCreateRoomChannel + finishCreateRoom. Saves one Valkey round
   trip per channel/DM create. processAddMembers fetches the pair locally
   before fan-out so the contract stays uniform (caller owns the fetch).
   Kept the nil-check inside as a defensive guard.

3. KeyGenerated / KeyRotated success counters: dropped at every emit site
   (room-worker x3, room-service x1) and removed the declarations from
   pkg/roomkeymetrics. Error counters (FanoutErrors, ValkeyErrors,
   KeyAbsentErrors) stay.

Spec: docs/superpowers/specs/2026-05-19-org-to-individual-membership-upgrade-design.md Part 7
Plan: docs/superpowers/plans/2026-05-19-member-add-improvements-plan.md Tasks 16-19

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…phantom-validation tests

- pkg/pipelines: convert GetCapacityCheckPipeline / GetAddMemberCandidatesPipeline
  from panic-on-empty-roomID to (bson.A, error); library code should not crash
  the worker on bad caller input. Extract bot/pseudo regex into package-level
  const so GetNewMembersPipeline and matchCandidates can't drift.

- room-worker/processAddMembers: propagate backfill query errors instead of
  log-and-continue. Log-and-continue would silently corrupt room_members
  (existing subs would never get IRM rows). Retry is safe — subs are
  already written so needSub is empty, hadOrgsBefore stays false until
  BulkCreateRoomMembers commits, and the backfill re-runs cleanly.

- room-worker: requestID propagation on the two member-add publish-error logs
  now inside the gated block; wrap bare returns after reconcileRoomOnDuplicateKey
  and rotateAndFanOut; refine error-wrap messages on the dup-key fetch path.
  Trim BulkCreateSubscriptions doc to one why-line. Project u.account on
  GetSubscriptionAccounts.

- room-service: rename _orgRaw BSON tag to orgRaw, add omitempty across
  orgRawDisplay siblings. Clarify validateOrgIDs / validateAccountsExist
  godoc to record the fmt.Errorf wrap-with-input shape that callers see
  via errors.Is.

- room-service tests: consolidate 5 sibling AddMembers phantom tests into a
  single table-driven case; add parallel CreateRoomChannel phantom table so
  the same gate is exercised on both paths; add store-error branch cases to
  cover the FindExistingOrgIDs / FindExistingAccounts error propagation
  path (was untested per CLAUDE.md §4).

- room-worker tests: add backfill-error fail-hard tests for both
  GetSubscriptionAccounts and FindUsersByAccounts failure paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Removes the inline `COMPOSE_BAKE=false COMPOSE_PARALLEL_LIMIT=1` env-vars
from both branches of the `up` target. The Docker Compose CLI applies its
own defaults; pinning them here predates the local toolchain and is no
longer needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- pkg/displayfmt/combine_test.go: switch to in-package tests (`package
  displayfmt`) per CLAUDE.md §4 rule; drop the now-unused local import.
- room-worker/handler.go: wrap bare `return nil, reconcileErr` in
  processSyncCreateDM's dup-key reconcile branch with sync-DM context
  (CLAUDE.md §3 "Never return bare err").
- room-service/integration_test.go: make two dept-only test cases actually
  dept-only by setting SectID to a non-matching value (was `dept-x`, so
  the tests would have passed via sectId match alone — defeating the
  intent of "dept-only branch is doing its job").
- room-worker/store.go: refresh stale doc on ListNewMembersForNewRoom —
  refer to ListAddMemberCandidates (the current contract) instead of the
  removed ListNewMembers.
- docs/superpowers: rename stale `_orgRaw` → `orgRaw` in both the plan
  snippet and the spec narrative to match the renamed BSON tag; add
  language identifiers (```sh) to the four shell-command fenced blocks
  the reviewer called out in the plan.

Skipped intentionally:
- Makefile COMPOSE_BAKE finding: the user explicitly asked to REMOVE
  COMPOSE_BAKE/COMPOSE_PARALLEL_LIMIT (commit 3df2b19); bot's suggestion
  to set COMPOSE_BAKE=true contradicts that directive.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Close the consistency gap CodeRabbit flagged on PR #212: the Create
Room request schema was missing users/orgs/channels even though the
handler accepts them and the same section's error envelope mentions
"invalid org" / "user not found" rejections for those exact fields.
Add them to the request-body table (marked channel-only and optional,
matching the gate the worker enforces) and update the example JSON so
the schema, example, and error envelope agree.
… methods

All production write paths now use Bulk* variants (BulkCreateSubscriptions,
BulkCreateRoomMembers); the single-row CreateSubscription, CreateRoom (room-
service only), and CreateRoomMember linger on the store interfaces with zero
live callers and are only kept alive by integration-test seeding. Remove them
from the interface + Mongo impl, point the test seed sites at the existing
mustInsertRoom / mustInsertSub helpers (direct collection inserts), and drop
the now-misleading "existing methods (invite flow)" section comment in
room-worker/store.go. Mocks regenerated; lint, unit tests, and integration
tests all green locally.
PR #171 follow-up Finding 1: room-service stamped the current Valkey key
version into RemoveMemberRequest.BaseKeyVersion and room-worker skipped
rotation when Valkey was already past that baseline. The reviewer pointed
out that JetStream only redelivers on NAK and a single delivery only ever
reaches one pod, so the post-rotate redelivery scenario the guard was
shielding against can't actually arise — and the extra Valkey Get on the
hot remove path is unnecessary work.

Drop the whole machinery: remove BaseKeyVersion from RemoveMemberRequest,
delete the keyStore.Get-and-stamp block in room-service/handleRemoveMember,
drop the shouldRotate computation and parameter threading through
processRemoveOrg / processRemoveIndividual in room-worker, and unconditionally
rotate-and-fan-out (the existing `len(accounts) > 0` guard on the org path
stays since rotation is genuinely a no-op when nothing was deleted).

Spec + plan updated: Finding 1 is removed from Part 7 in both the design
doc and the plan, the remaining two findings are renumbered, and the
"Skip-rotation guard" test in room-worker plus the "StampsBaseKeyVersion"
test in room-service are deleted. The two ProcessRemoveIndividual content
tests pick up a ListByRoom expectation because the rotate path now always
runs.
Neither field exists on the wire. Room.CreatedBy was removed from the
model earlier in this PR (Task 13), and createdByAccount was never a
field on CreateRoomRequest — the creator's account is taken from the
{account} segment of the create-room subject by ParseRoomCreateSubject
and never read from the request body.

- docs/client-api.md: remove the createdByAccount row from the Create
  Room request-body table and from the example JSON; add a one-liner
  pointing readers at the subject-derived creator.
- docs/superpowers/spec.md: remove the stale CreatedBy row from the
  Room schema table in section 4.1.
- docs/superpowers/plans/2026-05-19-member-add-improvements-plan.md:
  rewrite Step 11/12 — the original plan instructed the grep to spare
  createdByAccount on the assumption it was a real field; both names
  should be stripped.

Historical design/plan docs (2026-03..2026-05-07) that record the
field as it existed at those points in time are left intact.
room-worker/handler.go (Major):
  Backfill in processAddMembers fed FindUsersByAccounts the existing
  subscriber accounts and assumed the result covered every requested
  one. When a stale subscription points at a deleted user document the
  call returns a subset; the buggy code committed room_members for the
  rows it got, BulkCreateRoomMembers flipped hadOrgsBefore=true, and
  no future redelivery could ever repair the missing IRM rows (backfill
  only fires on the first-org transition). Add a fail-hard guard that
  compares returned accounts against the requested set and returns
  newPermanent("backfill user %s not found ...") before any write. New
  regression test in handler_test.go locks in the contract.

docs/client-api.md:
  Replace the stale "creating a room only enrolls the owner" line — it
  contradicts the request schema introduced earlier this PR, which now
  documents users/orgs/channels enrollment on create.

docs/superpowers/specs/2026-05-19-...-upgrade-design.md:
  Reconcile contradictory stage-count claims for the _orgMatch $lookup
  inner pipeline. The later "grows to 6 stages" sentence was wrong;
  verified against room-service/store_mongo.go:520-552 — the pipeline
  is 3 stages ($match → $addFields → $group), same as before. The
  $group merely widens its accumulator set.

pkg/testutil/testimages/testimages.go + docker-local/compose.deps.yaml:
  Replace the floating "mongo:8" / "valkey/valkey:8-alpine" tags with
  patch-pinned mongo:8.2.9 / valkey/valkey:8.1.7-alpine so testcontainers
  can't drift across patch releases between CI runs.
The Room schema table still claimed userCount is "1 immediately after
creation (the owner)", but the same section now documents users / orgs /
channels enrollment at create time, so the count can land >1. Rewrite
the note to match.
Per @mliu33's review on room-service/store_mongo.go:770. The previous
implementation iterated sectIDs, deptIDs, then a separate map → slice
loop (three passes). Build the result slice directly inside the
sectIDs / deptIDs loops, using the map only as a dedupe set, so we
make two passes instead of three and skip the map → slice copy.
Behavior unchanged; existing integration test covers it.
…trip

Per @mliu33's review: the previous implementation issued two Distinct
calls (one on sectId, one on deptId) and then merged + deduped in Go.
That's two network round-trips to Mongo per room.member.add and
channel-create request, which run on every gate check.

Rewrite as a single aggregate using $unionWith. The main pipeline does
the sectId distinct, $unionWith folds in the deptId distinct, and a
terminal $group dedupes the union server-side. Each $match sits at the
top of its own pipeline so the (sectId, account) and (deptId, account)
compound indexes both still cover their respective branches —
explain() against the integration container confirms DISTINCT_SCAN on
both indexes (Mongo even applies $groupByDistinctScan), no COLLSCAN.

Net: 1 round-trip instead of 2, same index coverage, less Go-side
allocation (no more dedup map + intermediate slices). Added a regression
test for the overlap case (same orgID matched via sectId on one user +
deptId on another) that the existing suite didn't pin down.
Restore the two parallel Distinct calls from before 95f7bd7. The
$unionWith version was benchmarked at ~8.5% wall-clock improvement
(885µs → 810µs per call against a local Mongo) for one fewer round-trip,
but at the cost of ~55% more Go-side allocations per call and a deeper
dependency on Mongo's aggregation optimizer. With the gain that small
and the call rate at this gate not on the critical hot path, the
simpler two-Distinct form is the better trade: less code, less BSON
marshaling, version-agnostic from Mongo 4.4 onward, and explicit
debuggability (each Distinct's error names which field failed).

Verified TestMongoStore_FindExistingOrgIDs_Integration passes on both
mongo:8.2.9 (current default) and mongo:4.4.30, including the new
overlap-dedup case added in the previous commit. Comment on the
function records why we landed back here so a future reader doesn't
re-do the round-trip migration without fresh perf numbers.
Copy link
Copy Markdown
Collaborator

@ngangwar962 ngangwar962 left a comment

Choose a reason for hiding this comment

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

LGTM

@vjauhari-work vjauhari-work force-pushed the claude/fix-member-subscription-bug-QZhjc branch from 95f7bd7 to aa90024 Compare May 21, 2026 09:46
Comment thread pkg/pipelines/member.go Outdated
Comment thread pkg/pipelines/member.go
Per @mliu33's review on pkg/pipelines/member.go (lines 112 and 135).
All four $lookup sub-pipelines in this file end with $match + $limit:1,
then the outer pipeline only reads $size of the resulting array to
check emptiness — it never touches any field of the joined doc. Mongo
was still fetching and shipping the full subscription / room_members
document into _sub / _irm / existingSub on every match.

Append $project {_id: 1} after each $limit so each entry in the join
array drops to ~30 bytes instead of the full doc. Same emptiness check
($eq: []) and same $size semantics work unchanged. Affects:

- GetNewMembersPipeline (existingSub lookup)
- GetCapacityCheckPipeline (_sub lookup)
- GetAddMemberCandidatesPipeline (_sub and _irm lookups)

No behavior change; existing unit + integration tests cover all three
pipelines (TestMongoStore_FindExistingOrgIDs, ListAddMemberCandidates,
CountNewMembers) and remain green.
Comment thread room-service/store_mongo.go Outdated
…NewMembersPipeline

Per @mliu33's review on room-service/store_mongo.go:300. The two
functions produced byte-identical pipelines for roomID != "" (modulo
cosmetic variable names: existingSub/userAccount vs _sub/acct).
GetNewMembersPipeline already handles both cases natively — it returns
just the $match stage when roomID is empty, and adds the $lookup +
not-already-subscribed $match when roomID is non-empty.

- pkg/pipelines/member.go: delete GetCapacityCheckPipeline. Refactor
  GetNewMembersPipeline to use the shared matchCandidates() helper for
  the $match stage (DRY with GetAddMemberCandidatesPipeline).
- pkg/pipelines/member_test.go: delete TestGetCapacityCheckPipeline.
  Behavior is fully covered by the existing TestGetNewMembersPipeline*
  cases (both branches).
- room-service/store_mongo.go: CountNewMembers no longer needs the
  roomID-empty bifurcation — it now calls GetNewMembersPipeline
  unconditionally with the supplied roomID.
- docs/superpowers/specs/2026-05-19-...-upgrade-design.md: spec rewritten
  to reflect the consolidation. The plan doc (a historical record of
  Task 1 as originally executed) is left intact.
Comment thread room-worker/handler.go Outdated
claude added 2 commits May 21, 2026 10:43
…veOrg

Per @mliu33's review on room-worker/handler.go:562. The previous two-pass
logic scanned `members` once for a dept row and then (only if none was
found) scanned it again for the first sect row. OrgMemberStatus already
carries the `IsDept` flag, so we can do this in a single pass: stash the
first sect candidate as we scan, then let any encountered dept row
override it (and short-circuit further iteration). After the loop, fall
back to the stashed sect candidate only if no dept row appeared.

No behavior change — every TestHandler_ProcessRemoveOrg_DeptFirstTiebreak
subcase (all sect, all dept, mixed dept-wins, all-names-empty → orgID
fallback) still passes. Saves one full pass over the members slice on
the org-remove hot path.
…g the same user

Multi-org overlap bug introduced by Part 2 of this PR (dept-aware org
matching). Before: orgs matched only on sectId, so a user could be
reachable in a room via at most one org. After Part 2: orgs match
sectId OR deptId, so a user with both fields set can be covered by two
org rows simultaneously (one matching their sectId, one matching their
deptId).

processRemoveOrg's old toRemove rule only checked HasIndividualMembership:
any org-only user was scheduled for sub deletion. With multi-org overlap,
this silently orphans the user's sub even though the sibling org row
still claims them as a member of the room. They lose notifications,
can't see new messages, and effectively get booted — visible only on
the next list-members call where the sibling org still expands to them.

Fix is two-layer:

- room-worker/store.go: add HasOtherOrgMembership bool to OrgMemberStatus.
- room-worker/store_mongo.go: GetOrgMembersWithIndividualStatus gains a
  second $lookup against room_members. For each candidate, search for
  another org row whose member.id matches their sectId or deptId
  (excluding the orgID being removed). Same $project: {_id: 1} trick
  as the other lookups to keep the join entry small.
- room-worker/handler.go: processRemoveOrg skips members where
  HasOtherOrgMembership=true from toRemove. Org row deletion + count
  reconcile + sys-msg still fire (the org WAS removed); no rotate, no
  per-account events when toRemove is empty.

Tests:
- Handler unit test (TestHandler_ProcessRemoveOrg_OtherOrgCovers_PreservesSub):
  mock returns alice with HasOtherOrgMembership=true; asserts
  DeleteSubscriptionsByAccounts and ListByRoom are never called.
- Pipeline integration test
  (TestMongoStore_GetOrgMembersWithIndividualStatus_OtherOrgCovers_Integration):
  alice has sectId=X + deptId=Y, both X and Y as org rows; queries org
  X and asserts alice.HasOtherOrgMembership=true (Y still covers her)
  while carol — sectId=X only — has HasOtherOrgMembership=false.

Both tests were red before the corresponding fix and green after; existing
TestHandler_ProcessRemoveOrg* and TestMongoStore_GetOrgMembersWithIndividualStatus*
cases still pass.
Copy link
Copy Markdown
Collaborator

@mliu33 mliu33 left a comment

Choose a reason for hiding this comment

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

Outstanding work, thanks! Just some minor comments

// delete their subscription.
{{Key: "$lookup", Value: bson.M{
"from": "room_members",
"let": bson.M{"sectId": "$sectId", "deptId": "$deptId"},
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

super~

Copy link
Copy Markdown
Collaborator

@mliu33 mliu33 left a comment

Choose a reason for hiding this comment

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

Super~

@mliu33 mliu33 merged commit c912aed into main May 21, 2026
9 checks passed
@vjauhari-work vjauhari-work deleted the claude/fix-member-subscription-bug-QZhjc branch May 21, 2026 11:22
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.

4 participants