docs(spec): federated room origin-site MV fix design#145
Conversation
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).
📝 WalkthroughWalkthroughA design specification detailing a fix for federated room search returning empty results on origin sites. The spec defines concrete code changes to ChangesFederated Room Origin-Site MV Fix
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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.
Built for teams:
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
docs/superpowers/specs/2026-05-01-federated-room-origin-site-mv-fix-design.md (2)
454-460: ⚡ Quick winRisk 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 winLine-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
📒 Files selected for processing (1)
docs/superpowers/specs/2026-05-01-federated-room-origin-site-mv-fix-design.md
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.
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.
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.
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.
…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.
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.
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.
…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.
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.
…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.
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.
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
…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>
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
* 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>
Implementation status (as of 2026-05-03)
room-worker.processAddMembersnow publishesOutboxEvent{member_added}tochat.inbox.{siteID}.member_addedfor same-site accounts).room-worker— still TODO.inbox-workerFilterSubjectsscoping toaggregate.>— still TODO.member_addedfromroom-service.handleCreateRoomfor the auto-enrolled owner (room creation path, out of docs(spec): federated room origin-site MV fix design #145's scope but using the same wire format soparseMemberEventaccepts both publish sites).Summary
room-workernever publishes member events to the origin site's local INBOX lane (chat.inbox.{siteID}.member_added/member_removed), sosearch-sync-worker'suser-room-mvandspotlightindexes 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.room-worker/handler.go(add-members at line 657, remove-individual at 272, remove-org at 401) plus a one-lineFilterSubjectsscoping oninbox-worker's consumer to keep it on the federatedaggregate.>lane only.role_updatedandroom_sync(same pattern, no consumer needs them today) to future work.Spec only — no code changes in this PR.
Test plan
MemberAddEvent/MemberRemoveEventwithSiteID == DestSiteID == originSite) is parseable by the existingparseMemberEventhelper.chat.inbox.{siteID}.aggregate.>is safe (no event type currently published to the local lane that inbox-worker needs).https://claude.ai/code/session_012L65kTQmvQt2X15tPRKY5H
Generated by Claude Code
Summary by CodeRabbit