fix: DM conversations always visible in all Spaces (#484)#490
Conversation
Root cause: decideConvKeepInSpace relied on hasSpaceMsg which scans the Recents window for space-tagged messages. Since DMs use a single physical channel shared across Spaces, the Recents window is shared too. When Space A was more recently active, its messages filled the Recents window and evicted Space B's tagged messages, causing the DM conversation to disappear from Space B's sidebar. Fix: regular (non-bot) DM conversations are now always visible in every Space, regardless of the Recents window contents. Message-level filtering (filterPersonMessagesBySpace) still controls which messages are shown within each Space. This treats DMs as globally-visible conversations consistent with the single-channel architecture. A full per-(contact,Space) conversation model is left as a follow-up product decision (see #484 blocking decision).
OctoBoooot
left a comment
There was a problem hiding this comment.
Review: fix: DM conversations always visible in all Spaces (#484) (#490)
Verdict: Approve (with one product-semantics confirmation called out — PR is a draft, so this is feedback-while-in-progress).
CI green. Risk: normal, but trust-boundary-adjacent (controls which conversations appear in which Space's sidebar). 2 files +32/-3.
Verified — the fix is correctly scoped to non-bot DMs; bot isolation preserved
The change is a single return true in the spaceID == "" && ChannelTypePerson branch of decideConvKeepInSpace (space_filter.go:281-290). Byte-verified the guard ordering above it preserves all the isolation-relevant cases:
skipBotFilter→ true (unchanged)SystemBots[channelID]→ true (unchanged)- bot AND
botInSpace[channelID]→ true (unchanged) - non-bot DM (
!botSet[channelID]) → now always true (was:hasSpaceMsg(filterSpaceID)Recents-scan) - a bot NOT in the space still falls through to
return falseat function end — bot space-isolation is NOT widened.
So the visibility widening is confined to human-to-human DMs. A bot that isn't a member of the filter Space still does not appear — the change doesn't touch the bot path that the prior PRs (#458/#469) worked to keep isolated. [bidirectional-edge-construction] check: the return true can't leak a bot conversation into a wrong space because the bot branches are evaluated and returned before this line.
Root-cause + fix logic is sound
The Recents-window-scan visibility check was genuinely unreliable for the single-physical-channel DM architecture: both Spaces share one Recents window, so the more-active Space evicts the other's tagged messages → personConvHasSpaceMessages(spaceB) returns false → conversation vanishes from Space B. Making non-bot DM visibility unconditional fixes the disappearing-DM symptom (symptom 2). Message-level filtering (filterPersonMessagesBySpace) still controls which messages render per-Space, so this only changes conversation-list visibility, not message content. The PR correctly scopes itself to symptom 2 and defers symptom 1 (cross-Space message pollution) to a product decision.
Needs confirmation — the privacy/product semantics of "DM visible in all Spaces"
This is the one thing worth an explicit human nod before un-drafting (it's why I'm flagging rather than plain-approving): the change makes every regular DM conversation appear in every Space's sidebar for that user. The PR frames this as the "DMs are global" interpretation, which is internally consistent with the existing untagged-message-visible-everywhere behavior. But it IS a visibility-surface change:
- A user who has DMs with contact X, and belongs to Spaces A and B, will now see the X conversation in both A and B's sidebars (previously: only where Recents happened to have a matching tag).
- This is a per-user view (the DM is the user's own conversation), so it's not a cross-user leak — the user already has access to their own DM. The question is purely UX/product ("should my DMs show in every Space tab, or be Space-scoped?"), not authz.
The PR body explicitly says symptom-1 / per-Space-vs-global is a pending product decision (#484). Since this change leans on the "global" interpretation, the same product decision governs whether this is the desired end state. Recommend an explicit product/stakeholder ack on "DMs visible in all Spaces" before merge — it's a deliberate product choice, correctly implemented, not a code defect.
Tests pin both directions
TestDecideConvKeepInSpace_DM_NonDefaultSpace_AlwaysVisible(renamed from..._NoSpaceMsg_Excluded) flips the assertionFalse→Truewith a comment explaining the GH#484 rationale — the rename + flip is the honest way to update a test whose expected behavior intentionally changed (not silently editing the assertion).- New
TestDecideConvKeepInSpace_DM_NonDefaultSpace_RecentsEvicted_StillVisiblepins the exact regression:hasSpaceMsgreturns false (Recents evicted) → still visible. This is the bug's precise failing input, captured as a test. TestDecideConvKeepInSpace_DM_BotInSpaceand the bot cases remain — confirming the bot path is unchanged.
Praise
- The fix flips the test assertion via rename + comment, not a silent edit —
..._NoSpaceMsg_Excluded→..._AlwaysVisiblewith a comment explaining why the expected behavior changed. When a fix intentionally inverts a previously-correct assertion, renaming the test to describe the new contract (rather than editing the bool in place) makes the behavior change reviewable in the diff. Right discipline. - Regression test captures the exact failing input —
RecentsEvicted_StillVisiblewithhasSpaceMsg → falseis the precise condition that made DMs vanish. A future "optimization" that re-introduces a Recents-based visibility gate fails this test loudly. - Scope honesty — the PR fixes symptom 2 and explicitly defers symptom 1 (cross-Space pollution) to a product decision rather than silently changing both. The comment + body both name what's fixed and what's deliberately left. Clean scoping on a two-symptom issue.
- Bot isolation preserved — the
return trueis placed after all the bot-branch guards, so the human-DM widening doesn't touch the bot space-isolation that #458/#469 established. The change is minimal and surgical.
Minor (non-blocking)
- The
hasSpaceMsg func(targetSpaceID string) boolparam +personConvHasSpaceMessageshelper are now effectively dead on the non-bot DM path (the only caller of that branch's check). The caller (space_filter.go:198) still constructs and passes it. Leaving it wired (rather than removing) is reasonable while symptom-1's product decision is pending — if per-(contact, Space) conversations land later, the Recents check may come back. Worth a// TODO(#484): hasSpaceMsg unused on the always-visible path; remove if global-DM is the final decisionso the next reader knows it's intentionally-retained-pending-decision, not an oversight.
Out of scope (informational)
- Symptom 1 (untagged DM messages visible in all Spaces via
filterPersonMessagesBySpacerule 2) is correctly deferred to the #484 product decision. The "DMs are global" interpretation this PR adopts should be confirmed as the intended end state, since both symptoms hinge on the same per-Space-vs-global question.
Jerry-Xin
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES — The GH#484 direction (make non-bot DM conversations visible in every Space, since DM is a single shared physical channel) is reasonable and the v2 unit tests added here directly cover the disappearing-DM scenario. However the change is not mergeable yet: it breaks existing v1 tests and introduces a cross-Space message-preview exposure in the conversation-list sync path.
🔴 Blocking
-
🔴 Existing v1 tests now fail (CI break).
decideConvKeepInSpaceis shared by both v1 (filterConversationsCore/FilterConversationsBySpace) and v2 sidebar. This PR only updatedmodules/message/space_filter_v2_test.go, but the v1 tests inmodules/message/space_filter_test.go(untouched) still assert the old "hidden when no Recents match" contract and now fail:TestFilterConversationsBySpace_DefaultSpaceBareConvs(space_filter_test.go:62) — expects 0 in non-default Space, now gets 2.TestFilterConversationsBySpace_NonDefaultSpaceDMVisible(space_filter_test.go:88,:95) — assertsuser2excluded, now included.TestFilterConversationsBySpace_NewSpaceCleanSlate(space_filter_test.go:119) — expects clean-slate 0, now gets 3.
Verified locally at heade63ae5d0:go test ./modules/message -run 'TestFilterConversationsBySpace'→ FAIL. Update these tests (and the now-incorrect "clean slate" comment) to the new DM contract.
-
🔴 Cross-Space DM
Recentsexposure in the conversation-list response.modules/message/space_filter.go:290now returnstruefor any non-bot DM in any Space, but the conversation-list sync path filters DMs only viaFilterConversationsBySpace(api_conversation.go:893), which keeps/drops the whole conversation and does not scrub each conv'sRecentsbyspace_id. The code comment claimsfilterPersonMessagesBySpacestill scopes messages per Space — but that function is only called on the channel-history sync path (api.go:1279), never on the conversation-listRecents. Result: a Space B conversation sync can now return a DM whoseRecentscontain only Space A messages (the exact "evicted" case the PR targets), exposing Space A's latest DM preview/content in Space B's list. Previously such a DM was dropped, so the preview never leaked. Please filter/clear DMRecentsto the requested Space in the conversation-list path when making DMs globally visible, and add a regression test for the "Recents contain only the other Space's messages" response shape.
💬 Non-blocking
- 🔵 The
hasSpaceMsghelper doc/contract atspace_filter.go:215is now stale — after this change it no longer governs regular DM visibility (only bot-related paths and v2 still reference it). Update the comment to avoid confusion.
✅ Highlights
- Behavior change is correctly gated to the
ChannelTypePerson+ non-bot branch; group/community-topic and bot-DM paths (incl. v2 fail-closed group scoping) are untouched, so there is no group/space-channel widening and no cross-user exposure — the conversation list is alreadyloginUID-scoped. - New v2 tests (
..._AlwaysVisible,..._RecentsEvicted_StillVisible) clearly document and cover the GH#484 disappearing-DM regression.
Reviewed at head e63ae5d099a376b708a958d9688b259b535d54af.
mochashanyao
left a comment
There was a problem hiding this comment.
[Octo-Q · automated review]
Verdict: Approve — no blocking findings; notes below (data-flow traced).
octo-server PR#490 Review Report
Reviewer: Octo-Q (automated review)
PR: #490
Head SHA: e63ae5d099a376b708a958d9688b259b535d54af
Title: fix: DM conversations always visible in all Spaces (#484)
Changed files: 2 (+32 / -3)
1. Verification Summary
| Item | Status | Evidence |
|---|---|---|
| Code correctness | ✅ | Single return-statement change in decideConvKeepInSpace non-bot DM branch; control flow intact for Group/Thread/Bot paths |
| Test coverage | ✅ | 25/25 space_filter tests pass; renamed test + new regression test cover the fix |
| Bot DM isolation preserved | ✅ | botSet[channelID] && !botInSpace[channelID] → false path (line 276) unchanged; bot-not-in-space still excluded |
| Message-level filtering unchanged | ✅ | filterPersonMessagesBySpace (lines 451–480) untouched; rules 1–4 intact |
| v1/v2 parity | ✅ | Both filterConversationsCore and FilterRawConversationsBySpace call the same decideConvKeepInSpace; change applies symmetrically |
2. Findings
F1 — P2 — Message-level filtering does NOT fully prevent cross-Space message visibility
Diff-scope: pre-existing limitation, amplified by this PR's conversation-visibility change
The PR description claims: "Message-level filtering (filterPersonMessagesBySpace) still controls which messages are shown within each Space, so cross-Space message pollution (symptom 1) is not worsened by this change."
This is partially correct but overstated. filterPersonMessagesBySpace rule 2 (space_filter.go:470–472) keeps untagged non-systembot messages in every Space:
case msid == "" && !isSysBot:
filtered = append(filtered, m) // legacy compat — kept in ALL SpacesUntagged messages include: legacy pre-Space messages, messages from clients omitting X-Space-ID, and forwarded/card messages. These remain visible in every Space after message-level filtering.
Impact analysis (R1 check):
- Not "unavailable": DM conversations were already in the user's conversation list (default Space); the PR changes which Space sidebars show them.
- Not "incorrect data": the same untagged messages were accessible before (via default Space sidebar); no new data exposed.
- Not "worse than before" in terms of data exposure: tagged messages (spaceA-tagged) are still properly dropped when viewing from spaceB (rule 4). Only untagged messages cross — same as before.
Severity: P2. The information disclosure is pre-existing; the PR changes the access path (sidebar convenience) but not the total data surface.
F2 — P2 — PR description mischaracterizes "Fixes #484" scope
The PR says "Fixes #484 (symptom 2)" but GitHub's Fixes keyword will auto-close the issue on merge. Issue #484 covers two symptoms; symptom 1 (cross-Space history pollution via untagged messages) is explicitly deferred pending a product decision. The PR body acknowledges this in the Scope section, but the Fixes keyword contradicts it — merging would close #484 as resolved while symptom 1 remains open.
Recommendation: Change Fixes #484 to Fixes #484 symptom 2 or Partial fix for #484 (remove the GitHub auto-close keyword), or use Contributes to #484.
F3 — P2 (nit) — Dead hasSpaceMsg callback parameter
After this change, the hasSpaceMsg callback in decideConvKeepInSpace is never invoked for non-bot DMs (the !botSet[channelID] branch returns true before reaching the callback). Both callers (filterConversationsCore line 198, FilterRawConversationsBySpace line 686) still construct and pass closures that are now dead code for this path. The callback is still used for the bot-in-default-space path (line 271), so the parameter can't be removed, but the closures in the non-bot DM callers are now wasted computation. Minor — no correctness impact.
3. Recommendations
-
Fix
Fixeskeyword (F2): ReplaceFixes #484 (symptom 2)withPartial fix for #484: symptom 2 (mutually-exclusive visibility)to avoid auto-closing the umbrella issue. -
Clarify PR description (F1): The claim "cross-Space message pollution (symptom 1) is not worsened" is technically accurate at the message-filter level but misleading at the user-experience level. Consider rewording to: "Message-level filtering is unchanged; symptom 1 (untagged messages visible in all Spaces) is a pre-existing architectural limitation that requires a product decision to resolve."
-
Document the product decision: The PR makes an implicit product choice ("DMs are global at conversation level") that was listed as one of three options in #484. Consider adding a note in the PR or #484 that this decision has been made, with a tracking item for the full per-(contact, Space) architecture if/when product decides to go that direction.
4. Additional Observations
- Test
TestDecideConvKeepInSpace_DM_NonDefaultSpace_HasSpaceMsg_Visibleis now redundant withAlwaysVisible— both asserttruefor non-bot DM in non-default Space. TheHasSpaceMsgvariant tests the now-unreachable callback path. Not a bug, but could be simplified. - Backward compatibility: Clients that previously did NOT see a DM in a non-default Space sidebar will now see it. No client-side breaking change; just new visibility. Three-platform rendering should be verified (web/iOS/Android) to confirm no client assumes conversation-in-sidebar implies all messages are Space-tagged.
5. Data Flow Trace
| Consumer | Data | Upstream Source | Runtime Flow Verified |
|---|---|---|---|
decideConvKeepInSpace (modified branch) |
!botSet[channelID] |
resolveBotFilter → spacepkg.GetBotUIDs DB query |
✅ Non-bot DMs reach this branch; bots take earlier paths |
decideConvKeepInSpace return value |
true (was hasSpaceMsg(filterSpaceID)) |
Hardcoded | ✅ Always returns true for non-bot DMs |
filterConversationsCore |
keep from decideConvKeepInSpace |
Same function | ✅ Non-bot DM conversations always kept |
FilterRawConversationsBySpace |
keep from decideConvKeepInSpace |
Same function | ✅ v2 sidebar parity confirmed |
filterPersonMessagesBySpace (api.go:1279) |
msgs, channelID, spaceID |
/v1/message/channel/sync handler, middleware-validated X-Space-ID |
✅ Unchanged; rules 1–4 intact. Rule 2 (untagged non-sysbot kept) is the pre-existing gap. |
personConvHasSpaceMessages / rawConvHasSpaceMessages |
callback passed to decideConvKeepInSpace |
Callers construct closures |
6. Blind Spot Checklist (R5)
- C1 — Dual-path parity: N/A. The change affects only the non-bot DM branch. Bot paths (add/remove from space) are symmetric and unchanged.
- C2 — Control-flow ordering / nested reuse: Clear.
decideConvKeepInSpaceis called from v1 and v2 filter paths; both reach the same modified branch. No nesting or double-invocation risk. - C3 — Authorization boundary ≠ capability boundary: N/A. Space sidebar filtering is UX-level, not authorization. DM messages are accessible to the conversation participants regardless of Space. No authorization boundary crossed.
- C4 — Authorization lifecycle / container-member cascade: N/A. No auth changes in this PR.
- C5 — Build ≠ runtime: N/A. Go backend change; no build artifacts / extensions / CLI packaging involved.
- C6 — Governance/policy doc self-consistency: Clear with note. The PR description's
Fixes #484claim conflicts with #484's explicit "Do not simply relax filtering" guidance. See F2 recommendation.
7. Cross-Round Blocker Recheck (R6)
N/A — automated review review.
Summary
| # | Severity | Category | Description |
|---|---|---|---|
| F1 | P2 | Process/claim | Message-level filtering doesn't fully prevent cross-Space visibility for untagged messages |
| F2 | P2 | Process | Fixes #484 will auto-close issue while symptom 1 remains unresolved |
| F3 | P2 (nit) | Cleanup | Dead hasSpaceMsg callback for non-bot DM path |
The code change is clean, focused, and well-tested. It correctly fixes symptom 2 (disappearing DMs between Spaces) without introducing code-level regressions. The primary concerns are process-level: the PR makes an implicit product decision, contradicts #484's explicit guidance, and its description slightly overstates the security properties of message-level filtering. None of these rise to P0/P1 by the R1 rubric (no new unavailability, no new incorrect data, no regression in total data exposure).
[Octo-Q] verdict: APPROVE
The fix is technically sound and addresses a real user-facing bug. The P2 findings are process/documentation concerns that should be addressed (especially F2 — the Fixes keyword issue) but do not block merge from a code-correctness standpoint.
|
Concurring with @Jerry-Xin's 🔴-2 — and correcting a claim in my own review (#490 (review)). Jerry-Xin is right; I missed it. My review asserted: "Message-level filtering (filterPersonMessagesBySpace) still controls which messages render per-Space, so this only changes conversation-list visibility, not message content." That conflated two different code paths and is wrong for the sidebar preview. Byte-verified: So the concrete leak:
Severity: this is a real cross-Space preview exposure (Jerry-Xin's 🔴). It's still per-user (the user owns the DM), so not a cross-user authz leak — but it surfaces a Space-A message in Space-B's UI, which is exactly the Space-isolation expectation this codebase invests in. It also undercuts the PR's own symptom-1-deferral framing: the PR says "message-level pollution is deferred", but this change actively introduces preview-level pollution that didn't exist before (the DM wasn't visible in Space B at all before). Fix direction (matching Jerry-Xin): when keeping a DM visible in a non-tagging Space, filter Jerry-Xin's 🔴-1 (a v1 test / CI) — CI currently shows only the Applying [praise-has-weight] retroactively: I cited a downstream filter as the guard without byte-verifying it actually covers the sidebar-preview path. "Message filtering still applies" was a path-confusion — the conversation-sync preview and the message-list endpoint are separate layers, and only the latter has the filter I named. My code-side APPROVE is retracted on this; the fix needs the Recents preview filtered before it's shippable. |
yujiawei
left a comment
There was a problem hiding this comment.
Code Review — PR #490 (octo-server)
Verdict: CHANGES_REQUESTED
Thanks for the clear root-cause write-up — the diagnosis of symptom 2 (visibility bound to the shared Recents window) is correct. However, the chosen fix has a blocking test break and, more importantly, contradicts its own scope claim: making every DM unconditionally visible in every Space re-opens cross-Space message exposure on the conversation-sync path, which is exactly what issue #484 warned against ("Do NOT simply relax filtering to show DMs in all Spaces — that collapses straight into symptom 1").
1. Scope / spec compliance
The PR description states:
Message-level filtering (
filterPersonMessagesBySpace) still controls which messages are shown within each Space, so cross-Space message pollution (symptom 1) is not worsened by this change.
This is not accurate for the conversation-sync path. filterPersonMessagesBySpace only runs on /v1/message/channel/sync (modules/message/api.go:1277-1281). It is not applied to the recents array that /v1/conversation/sync serializes (newSyncUserConversationResp, modules/message/api_conversation.go:1469-1555). Before this change, a DM with no Space-B-tagged messages was excluded from Space B, so its Space-A recents never reached a Space-B client. After this change every such DM is returned, with its raw (often Space-A) recents content included. So the change does materially widen cross-Space exposure on this endpoint — symptom 1, on a different route.
This needs either (a) a product sign-off that "DMs are global" (the issue lists this as a blocking product decision, not yet made), or (b) scoping the visibility relaxation so it does not also ship unfiltered cross-Space message content.
2. Code quality
P0 — Build/CI break: stale tests in space_filter_test.go
The PR updated space_filter_v2_test.go but left three tests in space_filter_test.go asserting the old "DM excluded from non-default Space" rule. They now fail, so go test ./modules/message/... is red and the merge gate cannot pass:
TestFilterConversationsBySpace_DefaultSpaceBareConvs(space_filter_test.go:60-62) — expects 0 DMs in a non-default Space, now gets 2.TestFilterConversationsBySpace_NonDefaultSpaceDMVisible(space_filter_test.go:84-96) — expectsuser2(only Space-Arecents) excluded, now kept.TestFilterConversationsBySpace_NewSpaceCleanSlate(space_filter_test.go:116-119) — expects 0 DMs in a brand-new Space, now gets 3.
Confirmed locally:
--- FAIL: TestFilterConversationsBySpace_DefaultSpaceBareConvs
--- FAIL: TestFilterConversationsBySpace_NonDefaultSpaceDMVisible
--- FAIL: TestFilterConversationsBySpace_NewSpaceCleanSlate
FAIL github.com/Mininglamp-OSS/octo-server/modules/message
These are pure unit tests (no DB/build tags), so they run in normal CI. Update them to the new contract.
P1 — /v1/conversation/sync leaks raw DM recents across Spaces
As described in §1: recents is built from the raw IM messages and is not space-filtered. fillPersonSpaceUnread adds space_last_message / space_unread (modules/message/space_unread.go:38-58) but does not replace or strip recents (api_conversation.go:1555). Result: a Space-B /v1/conversation/sync response can contain a DM whose recents payloads are Space-A message content. Even if the client renders space_last_message for the preview, the raw message bodies have already crossed the Space boundary on the wire. Either filter recents by Space here (reusing the filterPersonMessagesBySpace rule), or don't surface the conversation when it has no in-Space content.
P1 — Aggregate unread badge is not Space-scoped
SyncUserConversationResp.Unread (api_conversation.go:1453, :1547) and the sidebar SidebarItem.Unread (api_sidebar.go:1163 buildRecentItems) are serialized from the raw aggregate conv.Unread. With all DMs now visible in every Space, a Space-B sidebar shows DMs whose unread counts are driven entirely by Space-A messages. space_unread is computed separately, but any client reading the raw unread field gets cross-Space-polluted badges. This is a visible functional regression of the "per-Space" promise.
P2 — Dead code & now-incorrect comments
After the change, decideConvKeepInSpace no longer calls its hasSpaceMsg parameter for the non-bot DM branch, yet both call sites still build the closures (personConvHasSpaceMessages at space_filter.go:198, rawConvHasSpaceMessages at :703) and the parameter doc at space_filter.go:215 still claims it "decides non-default Space Person DM visibility" — which is no longer true. If the always-visible decision stands, remove the now-unused parameter + helpers (or document why they're retained) so the next reader isn't misled.
P2 — Payload bloat & context leak in scoped Spaces
Returning every historical DM in every Space inflates the conversation-list payload for heavy DM users and surfaces "ghost" DM entries that have no visible messages in the current Space. It also exposes the full contact/DM list (names, avatars) into strictly-scoped Spaces, weakening the isolation users expect (e.g. when screen-sharing a work Space). Worth weighing in the product decision referenced in #484.
3. Suggested direction
The issue explicitly flags this as needing a product decision ("one conversation vs. per-Space conversations"). If "DMs are global" is confirmed, the cleaner stop-gap the issue itself suggests is to exclude Person channels from Space filtering entirely AND make the conversation-sync/sidebar message-content + unread consistently global-or-scoped — not to flip visibility to always-true while leaving recents/unread carrying per-Space-inconsistent data. At minimum, before merge:
- Fix the three failing tests.
- Either space-filter
recentson/v1/conversation/syncor gate visibility so no unfiltered cross-Space content ships (resolve the §1 contradiction). - Decide whether
unreadshould be the space-scoped value on these endpoints. - Remove/relabel the dead
hasSpaceMsgpath.
4. Coverage notes
- Verified against head SHA
e63ae5d099a376b708a958d9688b259b535d54af; build break reproduced locally on this branch. - Not verified at runtime: actual client rendering (whether mobile/web use
recentsvsspace_last_messagefor the preview) and the real payload-size impact for high-volume users — these need a product/QA call, not a static read. - The WebSocket realtime-routing path and v2 sidebar metadata (contact existence/unread/timestamp) are affected at the metadata level but do not carry message bodies; the content leak is specific to
/v1/conversation/syncrecents.
Root Cause Analysis (GH#484 — Symptom 2)
decideConvKeepInSpaceinmodules/message/space_filter.godecides whether a DM conversation appears in a Space's sidebar by checkinghasSpaceMsg— which scansconv.Recents(the last N messages) forpayload.space_idmatches.Since DMs are a single physical channel shared across all Spaces (conversation-level
SpaceIDis intentionally left empty), the Recents window is also shared. When Space A is more recently active, its messages fill the Recents window and evict Space B's tagged messages.personConvHasSpaceMessages(spaceB)then returns false → conversation disappears from Space B's sidebar.Fix
Regular (non-bot) DM conversations are now always visible in every Space, regardless of Recents window contents. The fix is a one-line change in
decideConvKeepInSpace:Message-level filtering (
filterPersonMessagesBySpace) still controls which messages are shown within each Space, so cross-Space message pollution (symptom 1) is not worsened by this change.Scope
This PR fixes symptom 2 (mutually exclusive visibility / disappearing DMs).
Symptom 1 (cross-Space history pollution via
filterPersonMessagesBySpacerule 2 — keeping untagged messages in all Spaces) requires a product decision: should DMs be per-Space or global? The current behavior (untagged messages visible everywhere) is consistent with the "DMs are global" interpretation and is left as-is pending that decision. See #484 blocking product decision.Test
TestDecideConvKeepInSpace_DM_NonDefaultSpace_NoSpaceMsg_Excluded→TestDecideConvKeepInSpace_DM_NonDefaultSpace_AlwaysVisible(asserts True).TestDecideConvKeepInSpace_DM_NonDefaultSpace_RecentsEvicted_StillVisible— regression test for the exact bug scenario where Recents has no Space B messages.All 25 space_filter tests pass.
Fixes #484 (symptom 2)