Skip to content

docs(spec): federated room origin-site MV fix design#145

Merged
mliu33 merged 1 commit into
mainfrom
claude/check-federated-room-sync-OaEJ2
May 5, 2026
Merged

docs(spec): federated room origin-site MV fix design#145
mliu33 merged 1 commit into
mainfrom
claude/check-federated-room-sync-OaEJ2

Conversation

@Joey0538
Copy link
Copy Markdown
Collaborator

@Joey0538 Joey0538 commented May 3, 2026

Implementation status (as of 2026-05-03)


Summary

  • Documents a bug where room-worker never publishes member events to the origin site's local INBOX lane (chat.inbox.{siteID}.member_added/member_removed), so search-sync-worker's user-room-mv and spotlight indexes are never updated on the site that owns the federated room — i.e., the exact site that holds the messages CCS queries are trying to filter.
  • Specifies the fix: three additive publishes in room-worker/handler.go (add-members at line 657, remove-individual at 272, remove-org at 401) plus a one-line FilterSubjects scoping on inbox-worker's consumer to keep it on the federated aggregate.> lane only.
  • Forward-only — no backfill for pre-fix federated rooms (documented under Known Limitations).
  • Defers role_updated and room_sync (same pattern, no consumer needs them today) to future work.

Spec only — no code changes in this PR.

Test plan

  • Reviewer confirms problem statement matches the actual code paths (room-worker publishes, inbox-worker consumer config, search-sync-worker FilterSubjects).
  • Reviewer confirms wire format (OutboxEvent wrapping MemberAddEvent/MemberRemoveEvent with SiteID == DestSiteID == originSite) is parseable by the existing parseMemberEvent helper.
  • Reviewer confirms inbox-worker scoping to chat.inbox.{siteID}.aggregate.> is safe (no event type currently published to the local lane that inbox-worker needs).
  • Reviewer confirms forward-only rollout is acceptable for current environments.

https://claude.ai/code/session_012L65kTQmvQt2X15tPRKY5H


Generated by Claude Code

Summary by CodeRabbit

  • Documentation
    • Design specification published for fixing federated-room search returning empty results on origin sites. Outlines implementation approach to ensure member updates are properly indexed and searchable, defines required testing additions, and establishes rollout strategy with observability expectations. Includes known limitation regarding backfill for existing rooms.

Documents the bug where room-worker never publishes member events to the
origin site's local INBOX lane (chat.inbox.{siteID}.member_added/removed),
leaving search-sync-worker's user-room-mv and spotlight indexes missing
docs on the site that owns the room — which is exactly the site that
holds the messages CCS queries are trying to filter.

Spec covers room-worker publish additions at three call sites,
inbox-worker consumer FilterSubjects scoping to the aggregate lane,
testing strategy, rollout, and known limitations (forward-only, no
backfill).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 3, 2026

📝 Walkthrough

Walkthrough

A design specification detailing a fix for federated room search returning empty results on origin sites. The spec defines concrete code changes to room-worker (adding local INBOX member event publishes) and inbox-worker (filtering JetStream consumers), plus required testing and rollout steps.

Changes

Federated Room Origin-Site MV Fix

Layer / File(s) Summary
Problem Statement & Scope
docs/superpowers/specs/2026-05-01-federated-room-origin-site-mv-fix-design.md (lines 1–186)
Frames root cause: room-worker publishes member add/remove events only to cross-site OUTBOX, bypassing origin-site local INBOX lanes required for MV index updates on user-room-{siteID} and spotlight-{siteID}. Defines scope boundaries and confirms no subject, stream, or model changes.
Implementation Details
docs/superpowers/specs/2026-05-01-federated-room-origin-site-mv-fix-design.md (lines 186–284)
Specifies two targeted changes: (1) room-worker/handler.go publishes local INBOX member events with model.OutboxEvent wrapper, self-loop SiteID/DestSiteID, dedup-ID construction, and conditional skip logic; (2) inbox-worker/main.go restricts JetStream consumer filter to chat.inbox.{siteID}.aggregate.> to prevent misrouted local-lane processing.
Error Handling & Constraints
docs/superpowers/specs/2026-05-01-federated-room-origin-site-mv-fix-design.md (lines 285–410)
Defines log-and-continue error handling, confirms unchanged subjects/streams/builders, and prescribes rollout safety and per-site verification steps.
Testing & Observability
docs/superpowers/specs/2026-05-01-federated-room-origin-site-mv-fix-design.md (lines 414–463)
Outlines unit and integration test requirements, identifies known limitation (no backfill for pre-existing federated rooms), enumerates operational expectations, and documents risks (log spam if deployment order reverses) with mitigations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested reviewers

  • mliu33

Poem

🐰 A federated room once lost its way,
Member updates failed to display,
Now inbox lanes shall hear the call,
Worker filters sort it all,
Origin sites search—hoorah, hooray! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: a design specification for fixing federated room search on the origin site's materialized views.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/check-federated-room-sync-OaEJ2

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get your free trial and get 200 agent minutes per Slack user (a $50 value).


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 and usage tips.

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.

🧹 Nitpick comments (2)
docs/superpowers/specs/2026-05-01-federated-room-origin-site-mv-fix-design.md (2)

454-460: ⚡ Quick win

Risk section: “deploy both services same release” mitigation should be precise.

The “Log spam from inbox-worker if room-worker deploys first” mitigation assumes a specific behavior window: that inbox-worker is still consuming local-lane events broadly (no filter) until it’s redeployed. That’s likely true, but the spec doesn’t specify whether the deploy order is:

  • (a) room-worker deploy introduces local-lane publishes immediately, while
  • (b) inbox-worker still has the old consumer (no filter) for some time.

Consider tightening the mitigation wording to reflect the exact operational assumption (which service version introduces the local INBOX events; which service version removes/filters them).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@docs/superpowers/specs/2026-05-01-federated-room-origin-site-mv-fix-design.md`
around lines 454 - 460, Update the mitigation wording to precisely state the
deployment-order assumption: explain that a new room-worker version begins
publishing to the local lane (chat.inbox.{siteID}.*) immediately upon rollout
while an older inbox-worker version continues consuming without the new filter,
causing log spam until inbox-worker is redeployed with the filter scoping;
recommend deploying inbox-worker (the version that includes filter scoping)
after or in the same release as room-worker or perform a coordinated rollout
where inbox-worker is updated first to include the filter to avoid the transient
“user not found for account” warnings.

11-35: ⚡ Quick win

Line-number references in the design doc may go stale.

There are many hard-coded references like room-worker/handler.go:657, :272, :401, and specific line ranges in other files. Since this is documentation intended to survive beyond this PR, it may be safer to reference by:

  • function name (processAddMembers, processRemoveIndividual, processRemoveOrg),
  • and key call sites (publish immediately after subject.RoomMemberEvent / subject.MemberEvent / before/inside the OUTBOX loop),
    rather than relying on line numbers.

This reduces doc drift when the handlers get refactored.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@docs/superpowers/specs/2026-05-01-federated-room-origin-site-mv-fix-design.md`
around lines 11 - 35, The doc currently uses fragile line-number references
(e.g., room-worker/handler.go:657) which will drift; update the spec to
reference stable identifiers instead — name the handler functions
(processAddMembers, processRemoveIndividual, processRemoveOrg) and key call
sites (e.g., "publish immediately after subject.RoomMemberEvent /
subject.MemberEvent" and "publish before/inside the OUTBOX loop where
outbox.{origin}.to.{dest}.member_added is emitted") rather than file:line
ranges, and replace any other hard-coded line ranges with equivalent function
names or descriptive call-site notes so the design remains correct after
refactors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@docs/superpowers/specs/2026-05-01-federated-room-origin-site-mv-fix-design.md`:
- Around line 454-460: Update the mitigation wording to precisely state the
deployment-order assumption: explain that a new room-worker version begins
publishing to the local lane (chat.inbox.{siteID}.*) immediately upon rollout
while an older inbox-worker version continues consuming without the new filter,
causing log spam until inbox-worker is redeployed with the filter scoping;
recommend deploying inbox-worker (the version that includes filter scoping)
after or in the same release as room-worker or perform a coordinated rollout
where inbox-worker is updated first to include the filter to avoid the transient
“user not found for account” warnings.
- Around line 11-35: The doc currently uses fragile line-number references
(e.g., room-worker/handler.go:657) which will drift; update the spec to
reference stable identifiers instead — name the handler functions
(processAddMembers, processRemoveIndividual, processRemoveOrg) and key call
sites (e.g., "publish immediately after subject.RoomMemberEvent /
subject.MemberEvent" and "publish before/inside the OUTBOX loop where
outbox.{origin}.to.{dest}.member_added is emitted") rather than file:line
ranges, and replace any other hard-coded line ranges with equivalent function
names or descriptive call-site notes so the design remains correct after
refactors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d16a46fb-f683-4c0d-8d99-81330d944d90

📥 Commits

Reviewing files that changed from the base of the PR and between 5d373ab and c077b00.

📒 Files selected for processing (1)
  • docs/superpowers/specs/2026-05-01-federated-room-origin-site-mv-fix-design.md

Joey0538 added a commit that referenced this pull request May 3, 2026
handleCreateRoom auto-enrolls the creator as the owner inline (Mongo
Subscription insert) and bypasses room-worker's add-member path entirely.
That path is the only place INBOX member_added is published today, so
search-sync-worker's spotlight + user-room consumers never see the owner
for a freshly-created room — the creator can find every room they were
added to but never their own. The empty user-room-site-local /
spotlight-site-local-v1-chat indices on a fresh stack are a downstream
symptom: with no member_added ever flowing, ES has no document to
materialize the index from, and search-service queries 404.

Publish a same-site OutboxEvent{Type: member_added} wrapping
InboxMemberEvent{Accounts: [creator], HSS: nil} on
chat.inbox.{siteID}.member_added after the subscription write. Wire
format mirrors PR #145's spec so parseMemberEvent in search-sync-worker
accepts it without changes; when the room-worker side of #145 lands,
both publish sites converge cleanly. HSS stays nil because owner is
unrestricted — a positive HSS would mark the bulk restricted and
spotlight skips restricted events for MVP.

Best-effort: marshal/publish failures are logged and the create still
succeeds. publishToStream is nil-tolerated so unit tests that pass nil
for it remain valid.

Does NOT cover the room-worker add-member / remove-member gaps in #145 —
those keep their separate fix per the spec.
Joey0538 added a commit that referenced this pull request May 3, 2026
Implements the add-members slice of PR #145's spec. When alice adds bob
to a same-site channel, room-worker creates bob's Subscription in Mongo
correctly but never publishes the member_added event to the local INBOX
lane (chat.inbox.{siteID}.member_added) — only to OUTBOX for cross-site
fan-out. search-sync-worker's user-room-sync and spotlight-sync
consumers listen on both lanes; with no local publish, bob is absent
from both indices for any same-site add. End-user symptom: bob can't
search the room, alice's search misses bob's access in CCS filters,
even though bob is a real member in Mongo.

Add a same-site filter pass over actualAccounts (inverse of the
existing remoteSiteMembers loop, using the same userMap lookup), wrap
an InboxMemberEvent in an OutboxEvent with SiteID == DestSiteID ==
room.SiteID, publish to subject.InboxMemberAdded(room.SiteID) with a
deterministic dedupID so JetStream redelivery is idempotent. Same wire
format as room-service's room-create owner publish (139c17a) so
parseMemberEvent in search-sync-worker accepts both publish sites
without changes — when #145's full spec lands the merge converges
cleanly.

Scope-limited to add-members for now. The remove-individual /
remove-org slices and the inbox-worker FilterSubjects scoping from #145
keep their separate fix per the spec; this commit only closes the
search-visibility-on-add gap that's blocking dev today.

Best-effort publish: marshal error or NATS publish error is logged but
not returned — the subscription writes already landed and the outbox
path still runs.
Joey0538 added a commit that referenced this pull request May 3, 2026
handleCreateRoom auto-enrolls the creator as the owner inline (Mongo
Subscription insert) and bypasses room-worker's add-member path entirely.
That path is the only place INBOX member_added is published today, so
search-sync-worker's spotlight + user-room consumers never see the owner
for a freshly-created room — the creator can find every room they were
added to but never their own. The empty user-room-site-local /
spotlight-site-local-v1-chat indices on a fresh stack are a downstream
symptom: with no member_added ever flowing, ES has no document to
materialize the index from, and search-service queries 404.

Publish a same-site OutboxEvent{Type: member_added} wrapping
InboxMemberEvent{Accounts: [creator], HSS: nil} on
chat.inbox.{siteID}.member_added after the subscription write. Wire
format mirrors PR #145's spec so parseMemberEvent in search-sync-worker
accepts it without changes; when the room-worker side of #145 lands,
both publish sites converge cleanly. HSS stays nil because owner is
unrestricted — a positive HSS would mark the bulk restricted and
spotlight skips restricted events for MVP.

Best-effort: marshal/publish failures are logged and the create still
succeeds. publishToStream is nil-tolerated so unit tests that pass nil
for it remain valid.

Does NOT cover the room-worker add-member / remove-member gaps in #145 —
those keep their separate fix per the spec.
Joey0538 added a commit that referenced this pull request May 3, 2026
Implements the add-members slice of PR #145's spec. When alice adds bob
to a same-site channel, room-worker creates bob's Subscription in Mongo
correctly but never publishes the member_added event to the local INBOX
lane (chat.inbox.{siteID}.member_added) — only to OUTBOX for cross-site
fan-out. search-sync-worker's user-room-sync and spotlight-sync
consumers listen on both lanes; with no local publish, bob is absent
from both indices for any same-site add. End-user symptom: bob can't
search the room, alice's search misses bob's access in CCS filters,
even though bob is a real member in Mongo.

Add a same-site filter pass over actualAccounts (inverse of the
existing remoteSiteMembers loop, using the same userMap lookup), wrap
an InboxMemberEvent in an OutboxEvent with SiteID == DestSiteID ==
room.SiteID, publish to subject.InboxMemberAdded(room.SiteID) with a
deterministic dedupID so JetStream redelivery is idempotent. Same wire
format as room-service's room-create owner publish (139c17a) so
parseMemberEvent in search-sync-worker accepts both publish sites
without changes — when #145's full spec lands the merge converges
cleanly.

Scope-limited to add-members for now. The remove-individual /
remove-org slices and the inbox-worker FilterSubjects scoping from #145
keep their separate fix per the spec; this commit only closes the
search-visibility-on-add gap that's blocking dev today.

Best-effort publish: marshal error or NATS publish error is logged but
not returned — the subscription writes already landed and the outbox
path still runs.
Joey0538 added a commit that referenced this pull request May 3, 2026
…ber_added

Four changes to handleCreateRoom — none of which existed before — so
that newly-created rooms are immediately functional end-to-end:

- Mint a P-256 keypair in Valkey via h.keyStore.Set after CreateRoom.
  Without this, broadcast-worker fails the encrypt step ("no current
  key") and JetStream redelivers forever. Extends the narrow
  RoomKeyStore interface with Set; nil-tolerated for tests.
- DMs now persist a second Subscription for req.Members[0] and bump
  Room.UserCount to 2. Without this, the recipient logs in and every
  read path hits "not subscribed to room". Dev convention is account
  == user.ID, so req.Members[0] doubles for both fields; prod will
  need a real account → user.ID lookup.
- Best-effort core-NATS publish of SubscriptionUpdateEvent{Action:
  "added"} via a new WithEventPublisher hook so the creator's
  frontend sees the room appear without a refresh. Mirrors how
  room-worker emits the event for member-add / role-update.
- Best-effort INBOX same-site OutboxEvent{member_added} for the new
  subscription(s) so search-sync-worker's spotlight + user-room
  collections index the auto-enrolled accounts. Wire format matches
  PR #145's spec; HSS=nil keeps the bulk unrestricted.
Joey0538 added a commit that referenced this pull request May 3, 2026
Two member-event fixes:

- processAddMembers now publishes a same-site
  OutboxEvent{member_added} on chat.inbox.{siteID}.member_added for
  the local subset of accounts (cross-site keep going through OUTBOX
  unchanged). Implements the add-members slice of PR #145's spec;
  same wire format as room-service's room-create owner publish so
  search-sync-worker's parseMemberEvent accepts both.

- processRemoveIndividual + processRemoveOrg system messages now
  populate UserID/UserAccount from req.Requester. Prior code left
  these blank, so message-worker logged "user not found for system
  message" on every member-remove and the chat history rendered
  the entry as "Unknown". Dev convention: account == _id. Prod
  needs a real account → user.ID lookup upstream.

Remove-individual / remove-org INBOX publishes from #145's spec are
still TODO; only the add-member slice is closed here.
Joey0538 added a commit that referenced this pull request May 3, 2026
Two member-event fixes:

- processAddMembers now publishes a same-site
  OutboxEvent{member_added} on chat.inbox.{siteID}.member_added for
  the local subset of accounts (cross-site keep going through OUTBOX
  unchanged). Implements the add-members slice of PR #145's spec;
  same wire format as room-service's room-create owner publish so
  search-sync-worker's parseMemberEvent accepts both.

- processRemoveIndividual + processRemoveOrg system messages now
  populate UserID/UserAccount from req.Requester. Prior code left
  these blank, so message-worker logged "user not found for system
  message" on every member-remove and the chat history rendered
  the entry as "Unknown". Dev convention: account == _id. Prod
  needs a real account → user.ID lookup upstream.

Remove-individual / remove-org INBOX publishes from #145's spec are
still TODO; only the add-member slice is closed here.
Joey0538 added a commit that referenced this pull request May 4, 2026
…ber_added

Four changes to handleCreateRoom — none of which existed before — so
that newly-created rooms are immediately functional end-to-end:

- Mint a P-256 keypair in Valkey via h.keyStore.Set after CreateRoom.
  Without this, broadcast-worker fails the encrypt step ("no current
  key") and JetStream redelivers forever. Extends the narrow
  RoomKeyStore interface with Set; nil-tolerated for tests.
- DMs now persist a second Subscription for req.Members[0] and bump
  Room.UserCount to 2. Without this, the recipient logs in and every
  read path hits "not subscribed to room". Dev convention is account
  == user.ID, so req.Members[0] doubles for both fields; prod will
  need a real account → user.ID lookup.
- Best-effort core-NATS publish of SubscriptionUpdateEvent{Action:
  "added"} via a new WithEventPublisher hook so the creator's
  frontend sees the room appear without a refresh. Mirrors how
  room-worker emits the event for member-add / role-update.
- Best-effort INBOX same-site OutboxEvent{member_added} for the new
  subscription(s) so search-sync-worker's spotlight + user-room
  collections index the auto-enrolled accounts. Wire format matches
  PR #145's spec; HSS=nil keeps the bulk unrestricted.
Joey0538 added a commit that referenced this pull request May 4, 2026
Two member-event fixes:

- processAddMembers now publishes a same-site
  OutboxEvent{member_added} on chat.inbox.{siteID}.member_added for
  the local subset of accounts (cross-site keep going through OUTBOX
  unchanged). Implements the add-members slice of PR #145's spec;
  same wire format as room-service's room-create owner publish so
  search-sync-worker's parseMemberEvent accepts both.

- processRemoveIndividual + processRemoveOrg system messages now
  populate UserID/UserAccount from req.Requester. Prior code left
  these blank, so message-worker logged "user not found for system
  message" on every member-remove and the chat history rendered
  the entry as "Unknown". Dev convention: account == _id. Prod
  needs a real account → user.ID lookup upstream.

Remove-individual / remove-org INBOX publishes from #145's spec are
still TODO; only the add-member slice is closed here.
Joey0538 added a commit that referenced this pull request May 4, 2026
…ber_added

Four changes to handleCreateRoom — none of which existed before — so
that newly-created rooms are immediately functional end-to-end:

- Mint a P-256 keypair in Valkey via h.keyStore.Set after CreateRoom.
  Without this, broadcast-worker fails the encrypt step ("no current
  key") and JetStream redelivers forever. Extends the narrow
  RoomKeyStore interface with Set; nil-tolerated for tests.
- DMs now persist a second Subscription for req.Members[0] and bump
  Room.UserCount to 2. Without this, the recipient logs in and every
  read path hits "not subscribed to room". Dev convention is account
  == user.ID, so req.Members[0] doubles for both fields; prod will
  need a real account → user.ID lookup.
- Best-effort core-NATS publish of SubscriptionUpdateEvent{Action:
  "added"} via a new WithEventPublisher hook so the creator's
  frontend sees the room appear without a refresh. Mirrors how
  room-worker emits the event for member-add / role-update.
- Best-effort INBOX same-site OutboxEvent{member_added} for the new
  subscription(s) so search-sync-worker's spotlight + user-room
  collections index the auto-enrolled accounts. Wire format matches
  PR #145's spec; HSS=nil keeps the bulk unrestricted.
Joey0538 added a commit that referenced this pull request May 4, 2026
Two member-event fixes:

- processAddMembers now publishes a same-site
  OutboxEvent{member_added} on chat.inbox.{siteID}.member_added for
  the local subset of accounts (cross-site keep going through OUTBOX
  unchanged). Implements the add-members slice of PR #145's spec;
  same wire format as room-service's room-create owner publish so
  search-sync-worker's parseMemberEvent accepts both.

- processRemoveIndividual + processRemoveOrg system messages now
  populate UserID/UserAccount from req.Requester. Prior code left
  these blank, so message-worker logged "user not found for system
  message" on every member-remove and the chat history rendered
  the entry as "Unknown". Dev convention: account == _id. Prod
  needs a real account → user.ID lookup upstream.

Remove-individual / remove-org INBOX publishes from #145's spec are
still TODO; only the add-member slice is closed here.
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.

Excellent,thanks!

@mliu33 mliu33 merged commit 32d341e into main May 5, 2026
1 check passed
Joey0538 pushed a commit that referenced this pull request May 7, 2026
Implements docs/superpowers/specs/2026-05-01-federated-room-origin-site-mv-fix-design.md
in full. Federated rooms whose owning site adds or removes members now
update that site's user-room and spotlight indexes for every affected
member (same-site + cross-site), so CCS terms-lookup queries from any
site resolve against the origin-site MV.

room-worker (Changes 1-3): three additive log-and-continue publishes to
chat.inbox.{originSiteID}.member_{added,removed}, wrapping the existing
MemberAddEvent / MemberRemoveEvent in OutboxEvent so the wire format
matches the federated `aggregate.>` lane that search-sync-worker's
parseMemberEvent already decodes. Self-leave collapses to
OutboxEvent.Type=member_removed at the wrapper while preserving the
inner MemberRemoveEvent.Type=member_left, matching the cross-site
OUTBOX convention. add-members skips the publish when actualAccounts
is empty; remove-org reuses the existing len(accounts)>0 gate.

inbox-worker (Change 4): scope the durable consumer's FilterSubjects to
chat.inbox.{siteID}.aggregate.> so the new local-lane events reach
search-sync-worker only. Without this, inbox-worker would re-process
every cross-site member_added and emit duplicate-key churn against
subscriptions room-worker already wrote locally.

Tests:
- room-worker/handler_test.go: six new unit tests covering the happy
  path for each handler method, the empty-accounts no-publish guards,
  and the self-leave wrapper-vs-inner type collapse. Existing
  publishedMsg gains an msgID field so dedup-ID assertions can compare
  against outboxDedupID directly. Existing length expectations bumped
  by one publish where applicable.
- room-worker/integration_test.go: two real-Mongo tests assert the
  local-INBOX OutboxEvent + inner-payload structure end-to-end through
  processAddMembers and processRemoveMember.
- inbox-worker/integration_test.go: NATS-via-testcontainers test
  publishes one local-lane and one aggregate-lane event, then asserts
  the consumer's NumPending = 1 — locking in the FilterSubjects
  scoping so a future regression that drops it surfaces immediately.

Forward-only rollout per the spec; no backfill for pre-fix federated
rooms.

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
mliu33 pushed a commit that referenced this pull request May 8, 2026
…158)

* feat(room-worker,inbox-worker): origin-site MV fix per PR #145 spec

Implements docs/superpowers/specs/2026-05-01-federated-room-origin-site-mv-fix-design.md
in full. Federated rooms whose owning site adds or removes members now
update that site's user-room and spotlight indexes for every affected
member (same-site + cross-site), so CCS terms-lookup queries from any
site resolve against the origin-site MV.

room-worker (Changes 1-3): three additive log-and-continue publishes to
chat.inbox.{originSiteID}.member_{added,removed}, wrapping the existing
MemberAddEvent / MemberRemoveEvent in OutboxEvent so the wire format
matches the federated `aggregate.>` lane that search-sync-worker's
parseMemberEvent already decodes. Self-leave collapses to
OutboxEvent.Type=member_removed at the wrapper while preserving the
inner MemberRemoveEvent.Type=member_left, matching the cross-site
OUTBOX convention. add-members skips the publish when actualAccounts
is empty; remove-org reuses the existing len(accounts)>0 gate.

inbox-worker (Change 4): scope the durable consumer's FilterSubjects to
chat.inbox.{siteID}.aggregate.> so the new local-lane events reach
search-sync-worker only. Without this, inbox-worker would re-process
every cross-site member_added and emit duplicate-key churn against
subscriptions room-worker already wrote locally.

Tests:
- room-worker/handler_test.go: six new unit tests covering the happy
  path for each handler method, the empty-accounts no-publish guards,
  and the self-leave wrapper-vs-inner type collapse. Existing
  publishedMsg gains an msgID field so dedup-ID assertions can compare
  against outboxDedupID directly. Existing length expectations bumped
  by one publish where applicable.
- room-worker/integration_test.go: two real-Mongo tests assert the
  local-INBOX OutboxEvent + inner-payload structure end-to-end through
  processAddMembers and processRemoveMember.
- inbox-worker/integration_test.go: NATS-via-testcontainers test
  publishes one local-lane and one aggregate-lane event, then asserts
  the consumer's NumPending = 1 — locking in the FilterSubjects
  scoping so a future regression that drops it surfaces immediately.

Forward-only rollout per the spec; no backfill for pre-fix federated
rooms.

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp

* test(inbox-worker): extract setupNATS helper per CodeRabbit nitpick

Aligns the new NATS-testcontainer test with the CLAUDE.md convention
"Write setup<Dep>(t *testing.T) helpers that start a container, register
t.Cleanup, and return a connected client". No behavior change.

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp

* feat(searchengine): env-gated TLS skip verify for ES connections

Adds an opt-in tlsSkipVerify bool to searchengine.New, plumbed from
each service's config:

- search-service:     SEARCH_TLS_SKIP_VERIFY (default false)
- search-sync-worker: SEARCH_TLS_SKIP_VERIFY (default false)

Default-off keeps prod safe; ops opts in per environment for
self-signed/internal ES clusters. When false, the factory uses the
standard ES client transport — same behavior as before this PR.
When true, clones http.DefaultTransport (preserving ProxyFromEnvironment,
dial/TLS-handshake timeouts, HTTP/2, idle-conn tuning) and overrides
only TLSClientConfig with InsecureSkipVerify=true and MinVersion=TLS 1.2,
guarding the type assertion on http.DefaultTransport so we error out
cleanly if a middleware (e.g. OTel) has replaced it.

Also enables gosec G402 narrowly in .golangci.yml so the //nolint:gosec
annotation in pkg/oidc and pkg/searchengine actually suppresses a real
rule, and any future unannotated InsecureSkipVerify is rejected at
lint time.

Includes a goimports-only struct alignment tweak in
room-worker/integration_test.go picked up while running make fmt — no
behavior change.

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp

* feat(auth): support multiple allowed OIDC audiences

Replaces the single-audience verifier with a multi-audience allow-list
so a shared Keycloak realm that issues tokens for several client
audiences (the common org pattern) can be served by one auth-service.

pkg/oidc:
- Config.Audience (string) → Config.Audiences ([]string).
- Disable go-oidc's built-in single-audience check via
  oidc.Config{SkipClientIDCheck: true} and enforce the allow-list in
  Validate after cryptographic verification: a token is accepted when
  any of its `aud` entries matches any configured audience.
- New ErrNoAudiences and ErrAudienceNotAllowed sentinels for clear
  error propagation; NewValidator fails fast on an empty Audiences list.
- Single Verify call per request — no retry-on-mismatch loop.

auth-service:
- OIDC_AUDIENCE → OIDC_AUDIENCES (comma-separated; envSeparator:",").
- Required-config check updated; deploy/.env.example and
  deploy/docker-compose.yml renamed to match.

Tests:
- pkg/oidc/oidc_test.go: table-driven tests for containsAudience
  (single/multi/empty cases) plus NewValidator empty-audiences guard.
  pkg/oidc had no tests before this commit.

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp

---------

Co-authored-by: Claude <noreply@anthropic.com>
Joey0538 pushed a commit that referenced this pull request May 11, 2026
Sibling fix to PR #145 (federated-room MV update for add/remove): apply
the same local-INBOX publish pattern to the room-creation path so
freshly-created rooms appear in user-room and spotlight indexes
immediately, not on the next add/remove operation.

room-worker.finishCreateRoom:
- After the existing subscription.update fan-out and channel sys-message
  publish, emit one chat.inbox.{site}.member_added event carrying every
  account in subs[] (creator + DM recipient + every initial channel
  member, with org-expansion already done upstream).
- HistorySharedSince is nil — no prior history exists at room creation.
- Dedup ID derived from outboxDedupID(ctx, room.SiteID, "{rid}:{requester}:{ts}"),
  reusing PR #145's helper.
- Log-and-continue on publish failure.

inbox-worker.handleRoomCreated:
- Same publish, symmetric placement: after BulkCreateSubscriptions
  succeeds, emit chat.inbox.{thisSite}.member_added for the locally-
  resolved Accounts.
- New Handler fields publish + siteID, wired from main.go to
  js.PublishMsg with Nats-Msg-Id for JetStream dedup.
- New unexported outboxDedupID helper mirroring room-worker's.

The two publishes are byte-for-byte compatible with PR #145's
add/remove publishes, so search-sync-worker decodes all three
identically. inbox-worker's consumer FilterSubjects is already scoped
to chat.inbox.{site}.aggregate.> (PR #145 / commit c779ede), so the
new local-lane publish stays off its own consumer.

Tests: TDD pattern. Two new tests in room-worker (DM cross-site +
channel mixed-site) and three in inbox-worker (DM, channel,
empty-accounts negative). Existing inbox-worker tests updated for the
NewHandler signature.

Forward-only rollout per spec; no backfill tool.

Spec: docs/superpowers/specs/2026-05-11-create-room-origin-site-mv-fix-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 11, 2026
Sibling fix to PR #145 (federated-room MV update for add/remove). PR
#145 closed the origin-site MV gap for member.add / member.remove;
this PR applies the same local-INBOX + cross-site OUTBOX pattern to
the room-creation path so freshly-created rooms appear in
user-room-{site} and spotlight-{site} ES indexes immediately, not on
the next add/remove operation.

room-worker.finishCreateRoom now emits two new publishes:

1. Local origin-site INBOX: chat.inbox.{origin}.member_added carrying
   every account in subs[] (creator + every auto-enrolled member).
   Drives the origin site's search-sync-worker MV update.

2. Cross-site OUTBOX per remote site: outbox.{origin}.to.{remote}.
   member_added carrying only the remote-site accounts (per-dest
   split). Reuses the existing federation lane PR #145 established for
   add-members: SubjectTransform rewrites it to chat.inbox.{remote}.
   aggregate.member_added, which the remote site's search-sync-worker
   already consumes. No new inbox-worker code, no new event types, no
   new stream config — just one more publish on a path that already
   exists.

Wire format byte-for-byte identical to PR #145 so parseMemberEvent
decodes all member_added events the same way regardless of which path
they take.

Also: lift outboxDedupID from room-worker's private helper to
natsutil.OutboxDedupID — used in 9 call sites on this branch, removes
the copy I would have introduced if inbox-worker had needed to
publish too.

Tests: 3 new unit tests in room-worker (DM local INBOX, channel local
INBOX, channel cross-site OUTBOX member_added).

Forward-only rollout per spec; no backfill tool.

Spec: docs/superpowers/specs/2026-05-11-create-room-origin-site-mv-fix-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 11, 2026
Sibling fix to PR #145 (federated-room MV update for add/remove). PR
#145 closed the origin-site MV gap for member.add / member.remove;
this PR applies the same local-INBOX + cross-site OUTBOX pattern to
the room-creation path so freshly-created rooms appear in
user-room-{site} and spotlight-{site} ES indexes immediately, not on
the next add/remove operation.

room-worker.finishCreateRoom now emits two new publishes:

1. Local origin-site INBOX: chat.inbox.{origin}.member_added carrying
   every account in subs[] (creator + every auto-enrolled member).
   Drives the origin site's search-sync-worker MV update.

2. Cross-site OUTBOX per remote site: outbox.{origin}.to.{remote}.
   member_added carrying only the remote-site accounts (per-dest
   split). Reuses the existing federation lane PR #145 established for
   add-members: SubjectTransform rewrites it to chat.inbox.{remote}.
   aggregate.member_added, which the remote site's search-sync-worker
   already consumes. No new inbox-worker code, no new event types, no
   new stream config — just one more publish on a path that already
   exists.

Wire format byte-for-byte identical to PR #145 so parseMemberEvent
decodes all member_added events the same way regardless of which path
they take.

Also: lift outboxDedupID from room-worker's private helper to
natsutil.OutboxDedupID — used in 9 call sites on this branch, removes
the copy I would have introduced if inbox-worker had needed to
publish too.

Tests: 3 new unit tests in room-worker (DM local INBOX, channel local
INBOX, channel cross-site OUTBOX member_added).

Forward-only rollout per spec; no backfill tool.

Spec: docs/superpowers/specs/2026-05-11-create-room-origin-site-mv-fix-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 11, 2026
Sibling fix to PR #145 (federated-room MV update for add/remove). PR
#145 closed the origin-site MV gap for member.add / member.remove;
this PR applies the same local-INBOX + cross-site OUTBOX pattern to
the room-creation path so freshly-created rooms appear in
user-room-{site} and spotlight-{site} ES indexes immediately, not on
the next add/remove operation.

room-worker.finishCreateRoom now emits two new publishes:

1. Local origin-site INBOX: chat.inbox.{origin}.member_added carrying
   every account in subs[] (creator + every auto-enrolled member).
   Drives the origin site's search-sync-worker MV update.

2. Cross-site OUTBOX per remote site: outbox.{origin}.to.{remote}.
   member_added carrying only the remote-site accounts (per-dest
   split). Reuses the existing federation lane PR #145 established for
   add-members: SubjectTransform rewrites it to chat.inbox.{remote}.
   aggregate.member_added, which the remote site's search-sync-worker
   already consumes. No new inbox-worker code, no new event types, no
   new stream config — just one more publish on a path that already
   exists.

Wire format byte-for-byte identical to PR #145 so parseMemberEvent
decodes all member_added events the same way regardless of which path
they take.

Also: lift outboxDedupID from room-worker's private helper to
natsutil.OutboxDedupID — used in 9 call sites on this branch, removes
the copy I would have introduced if inbox-worker had needed to
publish too.

Tests: 3 new unit tests in room-worker (DM local INBOX, channel local
INBOX, channel cross-site OUTBOX member_added).

Forward-only rollout per spec; no backfill tool.

Spec: docs/superpowers/specs/2026-05-11-create-room-origin-site-mv-fix-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 11, 2026
Sibling fix to PR #145 (federated-room MV update for add/remove): apply
the same local-INBOX publish pattern to the room-creation path so
freshly-created rooms appear in user-room and spotlight indexes
immediately, not on the next add/remove.

Two narrow additions: one publish at the end of room-worker's
finishCreateRoom (origin site) and a symmetric one at the end of
inbox-worker's handleRoomCreated (federated remote sites). Wire format
byte-for-byte matches PR #145 so search-sync-worker decodes both
identically.

Forward-only rollout per agreement; no backfill tool.

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 11, 2026
Sibling fix to PR #145 (federated-room MV update for add/remove). PR
#145 closed the origin-site MV gap for member.add / member.remove;
this PR applies the same local-INBOX + cross-site OUTBOX pattern to
the room-creation path so freshly-created rooms appear in
user-room-{site} and spotlight-{site} ES indexes immediately, not on
the next add/remove operation.

room-worker.finishCreateRoom now emits two new publishes:

1. Local origin-site INBOX: chat.inbox.{origin}.member_added carrying
   every account in subs[] (creator + every auto-enrolled member).
   Drives the origin site's search-sync-worker MV update.

2. Cross-site OUTBOX per remote site: outbox.{origin}.to.{remote}.
   member_added carrying only the remote-site accounts (per-dest
   split). Reuses the existing federation lane PR #145 established for
   add-members: SubjectTransform rewrites it to chat.inbox.{remote}.
   aggregate.member_added, which the remote site's search-sync-worker
   already consumes. No new inbox-worker code, no new event types, no
   new stream config — just one more publish on a path that already
   exists.

Wire format byte-for-byte identical to PR #145 so parseMemberEvent
decodes all member_added events the same way regardless of which path
they take.

Also: lift outboxDedupID from room-worker's private helper to
natsutil.OutboxDedupID — used in 9 call sites on this branch, removes
the copy I would have introduced if inbox-worker had needed to
publish too.

Tests: 3 new unit tests in room-worker (DM local INBOX, channel local
INBOX, channel cross-site OUTBOX member_added).

Forward-only rollout per spec; no backfill tool.

Spec: docs/superpowers/specs/2026-05-11-create-room-origin-site-mv-fix-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 12, 2026
Single cross-site event for room creation: outbox.{origin}.to.{remote}.
member_added does double duty — drives sub creation in inbox-worker
(with correct DM/botDM/channel shapes) AND MV update in
search-sync-worker. Drops the redundant room_created event entirely.

Schema (pkg/model/event.go):
- MemberAddEvent gains RoomType + RequesterAccount (both omitempty).
- Delete RoomCreatedOutbox struct.
- Delete OutboxTypeRoomCreated constant.
- MessageTypeRoomCreated stays — distinct system-message-type constant
  used by room-worker's publishChannelSysMessages, unrelated to
  federation.

Consumer (inbox-worker/handler.go):
- handleMemberAdded dispatches on event.RoomType. Empty RoomType
  defaults to RoomTypeChannel for backward-compat with pre-deploy
  publishers that didn't set the field.
- subscriptionName / subscriptionIsSubscribed helpers refactored to
  take primitives (roomType, roomName, requesterAccount, *user)
  instead of *RoomCreatedOutbox, so handleMemberAdded can call them.
- Duplicate-key BulkCreateSubscriptions errors swallowed (replay
  after a crashed prior delivery is idempotent — matches PR #169 fix).
- handleRoomCreated function deleted.
- case model.MessageTypeRoomCreated arm in HandleEvent switch deleted.

Publisher (room-worker/handler.go):
- finishCreateRoom: delete the per-remote-site room_created OUTBOX
  publish. Cross-site member_added publish now carries RoomType +
  RequesterAccount.
- finishCreateRoom local INBOX publish: same fields populated for
  consistency (search-sync-worker reads them).
- processAddMembers: populate RoomType + RequesterAccount on all
  three member_added publishes (UI fan-out, local INBOX, cross-site
  OUTBOX). Channels-only path, but consistent shape avoids surprises.
- publishSyncDMOutbox: switch from room_created to member_added with
  the full new schema.

Tests:
- inbox-worker/handler_test.go: replace 5 TestHandleRoomCreated* tests
  with TestHandleMemberAdded_DM/BotDM/Channel/EmptyRoomType/
  DuplicateKey cases. Helpers refactored to match new signatures.
- inbox-worker/integration_test.go: replace 2 room_created integration
  tests with member_added equivalents going through HandleEvent.
- room-worker/handler_test.go + integration_test.go: assertions on
  cross-site outboxes now look for OutboxMemberAdded subjects with
  full RoomType + RequesterAccount payload.

Incidental fix: search-sync-worker.spotlight.go has been writing an
empty `roomType` field to the spotlight ES doc since PR #145 because
MemberAddEvent's wire format didn't carry RoomType. Once room-worker
starts populating RoomType, the spotlight doc gets correct roomType
for the first time. No code change in search-sync-worker; existing
TestSpotlightCollection_BuildAction_MemberAdded asserts the correct
value.

Spec: docs/superpowers/specs/2026-05-12-consolidate-room-create-federation-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 12, 2026
Single cross-site event for room creation: outbox.{origin}.to.{remote}.
member_added does double duty — drives sub creation in inbox-worker
(with correct DM/botDM/channel shapes) AND MV update in
search-sync-worker. Drops the redundant room_created event entirely.

Schema (pkg/model/event.go):
- MemberAddEvent gains RoomType + RequesterAccount (both omitempty).
- Delete RoomCreatedOutbox struct.
- Delete OutboxTypeRoomCreated constant.
- MessageTypeRoomCreated stays — distinct system-message-type constant
  used by room-worker's publishChannelSysMessages, unrelated to
  federation.

Consumer (inbox-worker/handler.go):
- handleMemberAdded dispatches on event.RoomType. Empty RoomType
  defaults to RoomTypeChannel for backward-compat with pre-deploy
  publishers that didn't set the field.
- subscriptionName / subscriptionIsSubscribed helpers refactored to
  take primitives (roomType, roomName, requesterAccount, *user)
  instead of *RoomCreatedOutbox, so handleMemberAdded can call them.
- Duplicate-key BulkCreateSubscriptions errors swallowed (replay
  after a crashed prior delivery is idempotent — matches PR #169 fix).
- handleRoomCreated function deleted.
- case model.MessageTypeRoomCreated arm in HandleEvent switch deleted.

Publisher (room-worker/handler.go):
- finishCreateRoom: delete the per-remote-site room_created OUTBOX
  publish. Cross-site member_added publish now carries RoomType +
  RequesterAccount.
- finishCreateRoom local INBOX publish: same fields populated for
  consistency (search-sync-worker reads them).
- processAddMembers: populate RoomType + RequesterAccount on all
  three member_added publishes (UI fan-out, local INBOX, cross-site
  OUTBOX). Channels-only path, but consistent shape avoids surprises.
- publishSyncDMOutbox: switch from room_created to member_added with
  the full new schema.

Tests:
- inbox-worker/handler_test.go: replace 5 TestHandleRoomCreated* tests
  with TestHandleMemberAdded_DM/BotDM/Channel/EmptyRoomType/
  DuplicateKey cases. Helpers refactored to match new signatures.
- inbox-worker/integration_test.go: replace 2 room_created integration
  tests with member_added equivalents going through HandleEvent.
- room-worker/handler_test.go + integration_test.go: assertions on
  cross-site outboxes now look for OutboxMemberAdded subjects with
  full RoomType + RequesterAccount payload.

Incidental fix: search-sync-worker.spotlight.go has been writing an
empty `roomType` field to the spotlight ES doc since PR #145 because
MemberAddEvent's wire format didn't carry RoomType. Once room-worker
starts populating RoomType, the spotlight doc gets correct roomType
for the first time. No code change in search-sync-worker; existing
TestSpotlightCollection_BuildAction_MemberAdded asserts the correct
value.

Spec: docs/superpowers/specs/2026-05-12-consolidate-room-create-federation-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 12, 2026
Single cross-site event for room creation: outbox.{origin}.to.{remote}.
member_added does double duty — drives sub creation in inbox-worker
(with correct DM/botDM/channel shapes) AND MV update in
search-sync-worker. Drops the redundant room_created event entirely.

Schema (pkg/model/event.go):
- MemberAddEvent gains RoomType + RequesterAccount (both omitempty).
- Delete RoomCreatedOutbox struct.
- Delete OutboxTypeRoomCreated constant.
- MessageTypeRoomCreated stays — distinct system-message-type constant
  used by room-worker's publishChannelSysMessages, unrelated to
  federation.

Consumer (inbox-worker/handler.go):
- handleMemberAdded dispatches on event.RoomType. Empty RoomType
  defaults to RoomTypeChannel for backward-compat with pre-deploy
  publishers that didn't set the field.
- subscriptionName / subscriptionIsSubscribed helpers refactored to
  take primitives (roomType, roomName, requesterAccount, *user)
  instead of *RoomCreatedOutbox, so handleMemberAdded can call them.
- Duplicate-key BulkCreateSubscriptions errors swallowed (replay
  after a crashed prior delivery is idempotent — matches PR #169 fix).
- handleRoomCreated function deleted.
- case model.MessageTypeRoomCreated arm in HandleEvent switch deleted.

Publisher (room-worker/handler.go):
- finishCreateRoom: delete the per-remote-site room_created OUTBOX
  publish. Cross-site member_added publish now carries RoomType +
  RequesterAccount.
- finishCreateRoom local INBOX publish: same fields populated for
  consistency (search-sync-worker reads them).
- processAddMembers: populate RoomType + RequesterAccount on all
  three member_added publishes (UI fan-out, local INBOX, cross-site
  OUTBOX). Channels-only path, but consistent shape avoids surprises.
- publishSyncDMOutbox: switch from room_created to member_added with
  the full new schema.

Tests:
- inbox-worker/handler_test.go: replace 5 TestHandleRoomCreated* tests
  with TestHandleMemberAdded_DM/BotDM/Channel/EmptyRoomType/
  DuplicateKey cases. Helpers refactored to match new signatures.
- inbox-worker/integration_test.go: replace 2 room_created integration
  tests with member_added equivalents going through HandleEvent.
- room-worker/handler_test.go + integration_test.go: assertions on
  cross-site outboxes now look for OutboxMemberAdded subjects with
  full RoomType + RequesterAccount payload.

Incidental fix: search-sync-worker.spotlight.go has been writing an
empty `roomType` field to the spotlight ES doc since PR #145 because
MemberAddEvent's wire format didn't carry RoomType. Once room-worker
starts populating RoomType, the spotlight doc gets correct roomType
for the first time. No code change in search-sync-worker; existing
TestSpotlightCollection_BuildAction_MemberAdded asserts the correct
value.

Spec: docs/superpowers/specs/2026-05-12-consolidate-room-create-federation-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 12, 2026
Single cross-site event for room creation: outbox.{origin}.to.{remote}.
member_added does double duty — drives sub creation in inbox-worker
(with correct DM/botDM/channel shapes) AND MV update in
search-sync-worker. Drops the redundant room_created event entirely.

Schema (pkg/model/event.go):
- MemberAddEvent gains RoomType + RequesterAccount (both omitempty).
- Delete RoomCreatedOutbox struct.
- Delete OutboxTypeRoomCreated constant.
- MessageTypeRoomCreated stays — distinct system-message-type constant
  used by room-worker's publishChannelSysMessages, unrelated to
  federation.

Consumer (inbox-worker/handler.go):
- handleMemberAdded dispatches on event.RoomType. Empty RoomType
  defaults to RoomTypeChannel for backward-compat with pre-deploy
  publishers that didn't set the field.
- subscriptionName / subscriptionIsSubscribed helpers refactored to
  take primitives (roomType, roomName, requesterAccount, *user)
  instead of *RoomCreatedOutbox, so handleMemberAdded can call them.
- Duplicate-key BulkCreateSubscriptions errors swallowed (replay
  after a crashed prior delivery is idempotent — matches PR #169 fix).
- handleRoomCreated function deleted.
- case model.MessageTypeRoomCreated arm in HandleEvent switch deleted.

Publisher (room-worker/handler.go):
- finishCreateRoom: delete the per-remote-site room_created OUTBOX
  publish. Cross-site member_added publish now carries RoomType +
  RequesterAccount.
- finishCreateRoom local INBOX publish: same fields populated for
  consistency (search-sync-worker reads them).
- processAddMembers: populate RoomType + RequesterAccount on all
  three member_added publishes (UI fan-out, local INBOX, cross-site
  OUTBOX). Channels-only path, but consistent shape avoids surprises.
- publishSyncDMOutbox: switch from room_created to member_added with
  the full new schema.

Tests:
- inbox-worker/handler_test.go: replace 5 TestHandleRoomCreated* tests
  with TestHandleMemberAdded_DM/BotDM/Channel/EmptyRoomType/
  DuplicateKey cases. Helpers refactored to match new signatures.
- inbox-worker/integration_test.go: replace 2 room_created integration
  tests with member_added equivalents going through HandleEvent.
- room-worker/handler_test.go + integration_test.go: assertions on
  cross-site outboxes now look for OutboxMemberAdded subjects with
  full RoomType + RequesterAccount payload.

Incidental fix: search-sync-worker.spotlight.go has been writing an
empty `roomType` field to the spotlight ES doc since PR #145 because
MemberAddEvent's wire format didn't carry RoomType. Once room-worker
starts populating RoomType, the spotlight doc gets correct roomType
for the first time. No code change in search-sync-worker; existing
TestSpotlightCollection_BuildAction_MemberAdded asserts the correct
value.

Spec: docs/superpowers/specs/2026-05-12-consolidate-room-create-federation-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 12, 2026
Single cross-site event for room creation: outbox.{origin}.to.{remote}.
member_added does double duty — drives sub creation in inbox-worker
(with correct DM/botDM/channel shapes) AND MV update in
search-sync-worker. Drops the redundant room_created event entirely.

Schema (pkg/model/event.go):
- MemberAddEvent gains RoomType + RequesterAccount (both omitempty).
- Delete RoomCreatedOutbox struct.
- Delete OutboxTypeRoomCreated constant.
- MessageTypeRoomCreated stays — distinct system-message-type constant
  used by room-worker's publishChannelSysMessages, unrelated to
  federation.

Consumer (inbox-worker/handler.go):
- handleMemberAdded dispatches on event.RoomType. Empty RoomType
  defaults to RoomTypeChannel for backward-compat with pre-deploy
  publishers that didn't set the field.
- subscriptionName / subscriptionIsSubscribed helpers refactored to
  take primitives (roomType, roomName, requesterAccount, *user)
  instead of *RoomCreatedOutbox, so handleMemberAdded can call them.
- Duplicate-key BulkCreateSubscriptions errors swallowed (replay
  after a crashed prior delivery is idempotent — matches PR #169 fix).
- handleRoomCreated function deleted.
- case model.MessageTypeRoomCreated arm in HandleEvent switch deleted.

Publisher (room-worker/handler.go):
- finishCreateRoom: delete the per-remote-site room_created OUTBOX
  publish. Cross-site member_added publish now carries RoomType +
  RequesterAccount.
- finishCreateRoom local INBOX publish: same fields populated for
  consistency (search-sync-worker reads them).
- processAddMembers: populate RoomType + RequesterAccount on all
  three member_added publishes (UI fan-out, local INBOX, cross-site
  OUTBOX). Channels-only path, but consistent shape avoids surprises.
- publishSyncDMOutbox: switch from room_created to member_added with
  the full new schema.

Tests:
- inbox-worker/handler_test.go: replace 5 TestHandleRoomCreated* tests
  with TestHandleMemberAdded_DM/BotDM/Channel/EmptyRoomType/
  DuplicateKey cases. Helpers refactored to match new signatures.
- inbox-worker/integration_test.go: replace 2 room_created integration
  tests with member_added equivalents going through HandleEvent.
- room-worker/handler_test.go + integration_test.go: assertions on
  cross-site outboxes now look for OutboxMemberAdded subjects with
  full RoomType + RequesterAccount payload.

Incidental fix: search-sync-worker.spotlight.go has been writing an
empty `roomType` field to the spotlight ES doc since PR #145 because
MemberAddEvent's wire format didn't carry RoomType. Once room-worker
starts populating RoomType, the spotlight doc gets correct roomType
for the first time. No code change in search-sync-worker; existing
TestSpotlightCollection_BuildAction_MemberAdded asserts the correct
value.

Spec: docs/superpowers/specs/2026-05-12-consolidate-room-create-federation-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 12, 2026
Single cross-site event for room creation: outbox.{origin}.to.{remote}.
member_added does double duty — drives sub creation in inbox-worker
(with correct DM/botDM/channel shapes) AND MV update in
search-sync-worker. Drops the redundant room_created event entirely.

Schema (pkg/model/event.go):
- MemberAddEvent gains RoomType + RequesterAccount (both omitempty).
- Delete RoomCreatedOutbox struct.
- Delete OutboxTypeRoomCreated constant.
- MessageTypeRoomCreated stays — distinct system-message-type constant
  used by room-worker's publishChannelSysMessages, unrelated to
  federation.

Consumer (inbox-worker/handler.go):
- handleMemberAdded dispatches on event.RoomType. Empty RoomType
  defaults to RoomTypeChannel for backward-compat with pre-deploy
  publishers that didn't set the field.
- subscriptionName / subscriptionIsSubscribed helpers refactored to
  take primitives (roomType, roomName, requesterAccount, *user)
  instead of *RoomCreatedOutbox, so handleMemberAdded can call them.
- Duplicate-key BulkCreateSubscriptions errors swallowed (replay
  after a crashed prior delivery is idempotent — matches PR #169 fix).
- handleRoomCreated function deleted.
- case model.MessageTypeRoomCreated arm in HandleEvent switch deleted.

Publisher (room-worker/handler.go):
- finishCreateRoom: delete the per-remote-site room_created OUTBOX
  publish. Cross-site member_added publish now carries RoomType +
  RequesterAccount.
- finishCreateRoom local INBOX publish: same fields populated for
  consistency (search-sync-worker reads them).
- processAddMembers: populate RoomType + RequesterAccount on all
  three member_added publishes (UI fan-out, local INBOX, cross-site
  OUTBOX). Channels-only path, but consistent shape avoids surprises.
- publishSyncDMOutbox: switch from room_created to member_added with
  the full new schema.

Tests:
- inbox-worker/handler_test.go: replace 5 TestHandleRoomCreated* tests
  with TestHandleMemberAdded_DM/BotDM/Channel/EmptyRoomType/
  DuplicateKey cases. Helpers refactored to match new signatures.
- inbox-worker/integration_test.go: replace 2 room_created integration
  tests with member_added equivalents going through HandleEvent.
- room-worker/handler_test.go + integration_test.go: assertions on
  cross-site outboxes now look for OutboxMemberAdded subjects with
  full RoomType + RequesterAccount payload.

Incidental fix: search-sync-worker.spotlight.go has been writing an
empty `roomType` field to the spotlight ES doc since PR #145 because
MemberAddEvent's wire format didn't carry RoomType. Once room-worker
starts populating RoomType, the spotlight doc gets correct roomType
for the first time. No code change in search-sync-worker; existing
TestSpotlightCollection_BuildAction_MemberAdded asserts the correct
value.

Spec: docs/superpowers/specs/2026-05-12-consolidate-room-create-federation-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 12, 2026
Single cross-site event for room creation: outbox.{origin}.to.{remote}.
member_added does double duty — drives sub creation in inbox-worker
(with correct DM/botDM/channel shapes) AND MV update in
search-sync-worker. Drops the redundant room_created event entirely.

Schema (pkg/model/event.go):
- MemberAddEvent gains RoomType + RequesterAccount (both omitempty).
- Delete RoomCreatedOutbox struct.
- Delete OutboxTypeRoomCreated constant.
- MessageTypeRoomCreated stays — distinct system-message-type constant
  used by room-worker's publishChannelSysMessages, unrelated to
  federation.

Consumer (inbox-worker/handler.go):
- handleMemberAdded dispatches on event.RoomType. Empty RoomType
  defaults to RoomTypeChannel for backward-compat with pre-deploy
  publishers that didn't set the field.
- subscriptionName / subscriptionIsSubscribed helpers refactored to
  take primitives (roomType, roomName, requesterAccount, *user)
  instead of *RoomCreatedOutbox, so handleMemberAdded can call them.
- Duplicate-key BulkCreateSubscriptions errors swallowed (replay
  after a crashed prior delivery is idempotent — matches PR #169 fix).
- handleRoomCreated function deleted.
- case model.MessageTypeRoomCreated arm in HandleEvent switch deleted.

Publisher (room-worker/handler.go):
- finishCreateRoom: delete the per-remote-site room_created OUTBOX
  publish. Cross-site member_added publish now carries RoomType +
  RequesterAccount.
- finishCreateRoom local INBOX publish: same fields populated for
  consistency (search-sync-worker reads them).
- processAddMembers: populate RoomType + RequesterAccount on all
  three member_added publishes (UI fan-out, local INBOX, cross-site
  OUTBOX). Channels-only path, but consistent shape avoids surprises.
- publishSyncDMOutbox: switch from room_created to member_added with
  the full new schema.

Tests:
- inbox-worker/handler_test.go: replace 5 TestHandleRoomCreated* tests
  with TestHandleMemberAdded_DM/BotDM/Channel/EmptyRoomType/
  DuplicateKey cases. Helpers refactored to match new signatures.
- inbox-worker/integration_test.go: replace 2 room_created integration
  tests with member_added equivalents going through HandleEvent.
- room-worker/handler_test.go + integration_test.go: assertions on
  cross-site outboxes now look for OutboxMemberAdded subjects with
  full RoomType + RequesterAccount payload.

Incidental fix: search-sync-worker.spotlight.go has been writing an
empty `roomType` field to the spotlight ES doc since PR #145 because
MemberAddEvent's wire format didn't carry RoomType. Once room-worker
starts populating RoomType, the spotlight doc gets correct roomType
for the first time. No code change in search-sync-worker; existing
TestSpotlightCollection_BuildAction_MemberAdded asserts the correct
value.

Spec: docs/superpowers/specs/2026-05-12-consolidate-room-create-federation-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 18, 2026
Drop the redundant outbox.{origin}.to.{remote}.room_created event in
favor of the existing outbox.{origin}.to.{remote}.member_added event
(added in PR #169) doing double duty: drive sub creation in inbox-worker
AND MV update in search-sync-worker, mirroring the add-members path
which already works that way since PR #145.

Extend MemberAddEvent with RoomType + RequesterAccount so
inbox-worker.handleMemberAdded can build correctly-shaped DM/botDM subs
via the existing helpers (subscriptionName / rolesForType /
subscriptionIsSubscribed) instead of needing a separate handleRoomCreated
path.

Full removal of room_created event, model, handler, and tests. Incidental
benefit: heals a latent search-sync-worker bug where the spotlight ES
doc's roomType field has been empty since PR #145 because today's
MemberAddEvent wire format doesn't carry RoomType.

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 pushed a commit that referenced this pull request May 18, 2026
Single cross-site event for room creation: outbox.{origin}.to.{remote}.
member_added does double duty — drives sub creation in inbox-worker
(with correct DM/botDM/channel shapes) AND MV update in
search-sync-worker. Drops the redundant room_created event entirely.

Schema (pkg/model/event.go):
- MemberAddEvent gains RoomType + RequesterAccount (both omitempty).
- Delete RoomCreatedOutbox struct.
- Delete OutboxTypeRoomCreated constant.
- MessageTypeRoomCreated stays — distinct system-message-type constant
  used by room-worker's publishChannelSysMessages, unrelated to
  federation.

Consumer (inbox-worker/handler.go):
- handleMemberAdded dispatches on event.RoomType. Empty RoomType
  defaults to RoomTypeChannel for backward-compat with pre-deploy
  publishers that didn't set the field.
- subscriptionName / subscriptionIsSubscribed helpers refactored to
  take primitives (roomType, roomName, requesterAccount, *user)
  instead of *RoomCreatedOutbox, so handleMemberAdded can call them.
- Duplicate-key BulkCreateSubscriptions errors swallowed (replay
  after a crashed prior delivery is idempotent — matches PR #169 fix).
- handleRoomCreated function deleted.
- case model.MessageTypeRoomCreated arm in HandleEvent switch deleted.

Publisher (room-worker/handler.go):
- finishCreateRoom: delete the per-remote-site room_created OUTBOX
  publish. Cross-site member_added publish now carries RoomType +
  RequesterAccount.
- finishCreateRoom local INBOX publish: same fields populated for
  consistency (search-sync-worker reads them).
- processAddMembers: populate RoomType + RequesterAccount on all
  three member_added publishes (UI fan-out, local INBOX, cross-site
  OUTBOX). Channels-only path, but consistent shape avoids surprises.
- publishSyncDMOutbox: switch from room_created to member_added with
  the full new schema.

Tests:
- inbox-worker/handler_test.go: replace 5 TestHandleRoomCreated* tests
  with TestHandleMemberAdded_DM/BotDM/Channel/EmptyRoomType/
  DuplicateKey cases. Helpers refactored to match new signatures.
- inbox-worker/integration_test.go: replace 2 room_created integration
  tests with member_added equivalents going through HandleEvent.
- room-worker/handler_test.go + integration_test.go: assertions on
  cross-site outboxes now look for OutboxMemberAdded subjects with
  full RoomType + RequesterAccount payload.

Incidental fix: search-sync-worker.spotlight.go has been writing an
empty `roomType` field to the spotlight ES doc since PR #145 because
MemberAddEvent's wire format didn't carry RoomType. Once room-worker
starts populating RoomType, the spotlight doc gets correct roomType
for the first time. No code change in search-sync-worker; existing
TestSpotlightCollection_BuildAction_MemberAdded asserts the correct
value.

Spec: docs/superpowers/specs/2026-05-12-consolidate-room-create-federation-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp
Joey0538 added a commit that referenced this pull request May 18, 2026
* docs(spec): consolidate cross-site room-creation federation event

Drop the redundant outbox.{origin}.to.{remote}.room_created event in
favor of the existing outbox.{origin}.to.{remote}.member_added event
(added in PR #169) doing double duty: drive sub creation in inbox-worker
AND MV update in search-sync-worker, mirroring the add-members path
which already works that way since PR #145.

Extend MemberAddEvent with RoomType + RequesterAccount so
inbox-worker.handleMemberAdded can build correctly-shaped DM/botDM subs
via the existing helpers (subscriptionName / rolesForType /
subscriptionIsSubscribed) instead of needing a separate handleRoomCreated
path.

Full removal of room_created event, model, handler, and tests. Incidental
benefit: heals a latent search-sync-worker bug where the spotlight ES
doc's roomType field has been empty since PR #145 because today's
MemberAddEvent wire format doesn't carry RoomType.

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp

* docs(spec): address CodeRabbit review on federation consolidation

- Line 160: "consts" -> "constants" (style)
- Line 208: "sub shape" -> "sub-shape" (hyphenation)
- Rollout section: rewrite to honestly document the
  no-fully-safe-single-PR-deploy-order issue CodeRabbit flagged.
  Walks both deploy orders showing the malformed-DM window each
  produces. Presents three options (A: ship as-is, B: 2-PR split,
  C: single PR + follow-up cleanup) with a recommendation for
  option C. Marks this as an open question requiring user input
  before implementation begins.

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp

* docs(spec): simplify rollout to single-PR per pre-prod context

User confirmed cross-site federation is not yet integrated end-to-end,
so the theoretical mixed-version DM-sub-malformation window has no
real-world incidence today. Reverting the spec to option (A):
single PR ships both publisher and consumer changes; deploy order
(room-worker first) is defensive rather than strictly required.

Trims the option-discussion text and rolls the deploy-window risk into
the Risks section with explicit acknowledgment that it's theoretical
under current operational reality.

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp

* feat(model,room-worker,inbox-worker): consolidate room-create federation

Single cross-site event for room creation: outbox.{origin}.to.{remote}.
member_added does double duty — drives sub creation in inbox-worker
(with correct DM/botDM/channel shapes) AND MV update in
search-sync-worker. Drops the redundant room_created event entirely.

Schema (pkg/model/event.go):
- MemberAddEvent gains RoomType + RequesterAccount (both omitempty).
- Delete RoomCreatedOutbox struct.
- Delete OutboxTypeRoomCreated constant.
- MessageTypeRoomCreated stays — distinct system-message-type constant
  used by room-worker's publishChannelSysMessages, unrelated to
  federation.

Consumer (inbox-worker/handler.go):
- handleMemberAdded dispatches on event.RoomType. Empty RoomType
  defaults to RoomTypeChannel for backward-compat with pre-deploy
  publishers that didn't set the field.
- subscriptionName / subscriptionIsSubscribed helpers refactored to
  take primitives (roomType, roomName, requesterAccount, *user)
  instead of *RoomCreatedOutbox, so handleMemberAdded can call them.
- Duplicate-key BulkCreateSubscriptions errors swallowed (replay
  after a crashed prior delivery is idempotent — matches PR #169 fix).
- handleRoomCreated function deleted.
- case model.MessageTypeRoomCreated arm in HandleEvent switch deleted.

Publisher (room-worker/handler.go):
- finishCreateRoom: delete the per-remote-site room_created OUTBOX
  publish. Cross-site member_added publish now carries RoomType +
  RequesterAccount.
- finishCreateRoom local INBOX publish: same fields populated for
  consistency (search-sync-worker reads them).
- processAddMembers: populate RoomType + RequesterAccount on all
  three member_added publishes (UI fan-out, local INBOX, cross-site
  OUTBOX). Channels-only path, but consistent shape avoids surprises.
- publishSyncDMOutbox: switch from room_created to member_added with
  the full new schema.

Tests:
- inbox-worker/handler_test.go: replace 5 TestHandleRoomCreated* tests
  with TestHandleMemberAdded_DM/BotDM/Channel/EmptyRoomType/
  DuplicateKey cases. Helpers refactored to match new signatures.
- inbox-worker/integration_test.go: replace 2 room_created integration
  tests with member_added equivalents going through HandleEvent.
- room-worker/handler_test.go + integration_test.go: assertions on
  cross-site outboxes now look for OutboxMemberAdded subjects with
  full RoomType + RequesterAccount payload.

Incidental fix: search-sync-worker.spotlight.go has been writing an
empty `roomType` field to the spotlight ES doc since PR #145 because
MemberAddEvent's wire format didn't carry RoomType. Once room-worker
starts populating RoomType, the spotlight doc gets correct roomType
for the first time. No code change in search-sync-worker; existing
TestSpotlightCollection_BuildAction_MemberAdded asserts the correct
value.

Spec: docs/superpowers/specs/2026-05-12-consolidate-room-create-federation-design.md

https://claude.ai/code/session_01UkLD7hpaypxjeh5zbEWTjp

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

3 participants