messaging: after page reload, all bubbles render as bubble-other (post-reload regression of #62) #192
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_archipelagos#192
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Post-reload regression of #62 (closed). The optimistic-send patch added in
archipelago.rs:293-296only forcesis_own = trueon the just-sent message; after F5 every message in the thread (including ones the user just sent) renders left-aligned asbubble-other— there is no visual signal of which side of the conversation you are on.This was acknowledged as a known limitation in the original #62 body ("subsequent full refetches will still show the server-stamped sender") but was closed without follow-up. Filing this as a separate ticket so it stays on the radar.
Repro
/space/default/messaging, create DM with QA Tester.DevTools confirms:
Root cause (per the code itself)
The patch only runs in
handle_send. Reloading dropsactive_messagesand refetches from the server — every historical message comes back withsender_public_key = "system"and the patch is gone.Suggested fix (any one)
own_user_keyper context (so refetch can match), ORsender_public_key == SYSTEM_KEYasis_own = truewhenever own_user_key is empty/system, ORsender_public_keywith theX-Hero-Contextvalue (depends on the cross-context fix — see hero_osis #40).Related
Found via QA session 2026-04-29 (
qa/messaging_2026-04-29/FINDINGS.md).Implementation Spec — Issue #192
Objective
After page reload, messages the local user authored continue to render as
bubble-ownby re-applying the optimistic ownership mark from a per-context, persisted set of locally-authored message IDs — eliminating the regression where every bubble appears asbubble-otherpost-F5.Requirements
/space/default/messaging, opening a chat that the user has previously sent messages to MUST render those messages right-aligned withmessage-own/bubble-ownand the "Me" label, identical to how they appeared pre-reload.bubble-other(no false-positive on the "everyone is system" case).handle_sendis preserved) and MUST also seed the persisted set so the message survives reload.sender_public_key, the existingis_own = !own_user_key.is_empty() && msg.sender_public_key == own_user_keycheck MUST take precedence, so the persisted-IDs path becomes a no-op and can be retired without UI flicker.context_nameso that switching contexts doesn't leak ownership across tenants.cdylib + rliband is built native by the workspace).Files to modify/create
archipelagos/messaging/src/services/messaging_service.rs— extendchatmessage_to_messagedatato accept a "locally-authored IDs" lookup; exposeSYSTEM_KEY/is_self_or_systemaspub(crate)for reuse; add a smallOwnMessageIdshelper module with cfg-gated localStorage persistence.archipelagos/messaging/src/archipelago.rs— load the persisted ID set on mount, pass it into everychatmessage_to_messagedatacall site (initial fetch, SSEResync, SSEChatmessageNew, send path), persist on every successfulhandle_send, and update the existing optimistic patch to also write through to the store.messaging_service.rs. If preferred for clarity, createarchipelagos/messaging/src/services/own_messages.rsand re-export fromservices/mod.rs.)Step-by-step implementation plan
Step 1 — Add an
OwnMessageIdshelperFile:
archipelagos/messaging/src/services/messaging_service.rs(or newservices/own_messages.rs).Introduce:
pub struct OwnMessageIds { ids: HashSet<String>, order: Vec<String> }withcontains(&str) -> bool,insert(String)(FIFO-cap aware),len(), and a constMAX_OWN_IDS_PER_CONTEXT: usize = 5_000.pub fn load_own_message_ids(context_name: &str) -> OwnMessageIds:wasm32: readhero_messaging_own_msg_ids:<context_name>fromlocalStorage, parse as JSONVec<String>, dedup into the set + order vec. Return empty on any error.pub fn save_own_message_ids(context_name: &str, ids: &OwnMessageIds):wasm32: serialize ordered ids as JSON and write to localStorage. On non-wasm: no-op.const OWN_IDS_STORAGE_PREFIX: &str = "hero_messaging_own_msg_ids:";Reuse the cfg-gated
storage_get/storage_setpattern fromarchipelagos/intelligence/ai/src/island.rs:17-51.Why: Centralizes persistence and keeps the rest of messaging code agnostic to the backing store. Per-
context_namekeying isolates tenants.Dependencies: None.
Step 2 — Extend
chatmessage_to_messagedatato consult the local-author setFile:
archipelagos/messaging/src/services/messaging_service.rs, lines 349-375.Change signature to:
Replace the
is_owncomputation with the layered check (first match wins):Why: Single conversion seam for every fetched/streamed message — every code path (initial fetch, SSE live event, SSE
Resync) automatically picks it up. No scattered patching.Dependencies: Step 1.
Step 3 — Wire
OwnMessageIdsthrougharchipelago.rsFile:
archipelagos/messaging/src/archipelago.rs.a. In
MessagingArchipelagoApp, after the existing signals (around lines 92-106), add:b. Update every call to
services::chatmessage_to_messagedatato pass&own_message_ids.peek()(or.read()where reactive re-derive matters):ChatEvent::ChatmessageNewhandler).Resyncrefetch)..read()so the messages list re-derives if the set updates.handle_send).c. In
handle_send(around the existing optimistic patch on lines 511-512), after the message is successfully sent:Keep the existing
msg_data.is_own = true; msg_data.sender_name = "Me"lines.d. In the SSE
ChatmessageNewhandler (lines 191-205), no extra logic is needed — the conversion in Step 2 will returnis_own = trueif the sid is in the set.Why: Threads the local-author signal through every conversion site and persists on send. Dedup remains in the existing pipeline.
Dependencies: Steps 1 and 2.
Step 4 — Make
SYSTEM_KEY/is_self_or_systemreusable (optional cleanup)File:
archipelagos/messaging/src/services/messaging_service.rs.Mark
SYSTEM_KEYandis_self_or_systemaspub(crate). Skip if no callers consume them outside the file.Why: Avoids future duplication once auth lands.
Dependencies: None.
Step 5 — Tests
Add a
#[cfg(test)] mod testsblock inmessaging_service.rs:test_own_id_match_returns_is_own_true—sender_public_key = "system",sid = "msg-42",own_user_key = "",OwnMessageIdscontains"msg-42". Assertis_own == true,sender_name == "Me".test_other_party_in_dm_remains_other— same but sid NOT in set. Assertis_own == false. (DM false-positive guard.)test_real_key_takes_precedence—own_user_key = "0xabc",msg.sender_public_key = "0xabc", emptyown_ids. Assertis_own == true(forward-compat once auth lands).test_own_message_ids_capacity_trims_oldest— insert >MAX_OWN_IDS_PER_CONTEXT, assert cap and FIFO eviction.Native-only (
cargo test -p hero_archipelagos_messaging);OwnMessageIdsmust be constructable directly in tests withoutlocalStorage.Why: Locks in the DM regression guard and proves the fix works without manual reload.
Dependencies: Steps 1 and 2.
Step 6 — Manual repro verification
/space/default/messaging, create DM with QA Tester, send 3 messages.bubble-own).bubble-ownwith "Me" label and status checkmark.localStorage["hero_messaging_own_msg_ids:default"]is a JSON array of 3 SIDs.bubble-other.context_name, confirm no leak.Acceptance criteria
bubble-own(right-aligned, blue, status icon, "Me" label).bubble-other, including when the server stamps both as"system".bubble-ownwithout waiting for the SSE round-trip (existing #62 behavior preserved).hero_messaging_own_msg_ids:<context>exists, is JSON, and grows by one entry per successful send (capped atMAX_OWN_IDS_PER_CONTEXT).context_namedoes not leak own-message IDs across tenants.cargo test -p hero_archipelagos_messagingcovers the four cases in Step 5.own_user_keyis provided (forward-compat with auth), the existing key-match path takes precedence — verified bytest_real_key_takes_precedence.Notes
sender_public_keywith theX-Hero-Contextvalue (option C in the issue) — tracked in hero_osis #40.OwnMessageIds, remove the parameter fromchatmessage_to_messagedata, and clearhero_messaging_own_msg_ids:*keys on next load.bubble-otheruntil real auth lands.bubble-otherafter the cap is hit.own_user_keyper context): the server stamps"system"regardless of who sent the message, so the predicatemsg.sender_public_key == own_user_keyis false in both directions. Option A buys nothing until the server stops stamping"system".sender_public_key == "system"=> own whenown_user_keyempty): confirmed inhero_osis/.../rpc.rs:911,960that bothsend_messageandreply_to_messageunconditionally write"system". In a DM where both parties send messages, every message comes back as"system"— Option B would mark the other party's messages as own. Strictly worse UX than the current bug.msg.sidis server-assigned and unique per message. The local user is the only one who knows which sids they authored (because they just got backOk(msg)fromsend_message). Storing those sids locally is the only data we have that uniquely identifies "messages I sent" until the server stamps a real key.Test Results
Crate:
hero_archipelagos_messaging(cargo test --lib)New tests covering #192
services::messaging_service::own_message_ids_tests::test_insert_dedupes— own-id set dedupes on repeat insertservices::messaging_service::own_message_ids_tests::test_insert_fifo_cap— FIFO eviction at MAX_OWN_IDS_PER_CONTEXT (5_000)services::messaging_service::chatmessage_conversion_tests::test_own_id_match_returns_is_own_true— sid match makes is_own true post-refetch even when sender is "system"services::messaging_service::chatmessage_conversion_tests::test_other_party_in_dm_remains_other— DM regression guard: "system" sender NOT in own-ids => is_own false (the option-B false-positive the issue calls out)services::messaging_service::chatmessage_conversion_tests::test_real_key_takes_precedence— once auth lands, key match wins regardless of own-idsWorkspace check
cargo check --workspace --exclude hero_archipelagos_messaging: clean (no errors, finished in 2.83s).cargo test --workspace --no-run: clean (all workspace test binaries compile).Implementation Summary
Branch:
development_fix_bubble_own_after_reloadApproach
Persist the SIDs of locally-authored messages in a per-context
localStorageset. The is-own check inchatmessage_to_messagedatanow layers(key match) || own_ids.contains(&msg.sid.to_string())so that refetched messages survive F5 with the right alignment. Falls back cleanly to the existingown_user_keymatch once auth lands — verified bytest_real_key_takes_precedence.Picked this over the issue's option B (treat
sender_public_key == "system"as own) because the server stamps both DM parties as"system"(hero_osis/.../rpc.rs:911,960), so option B would mark the other party's messages as own — strictly worse than the current bug. Picked it over option A (persistown_user_key) because the server's"system"stamp makes the key-match predicate false in both directions, so persisting the key buys nothing pre-auth.Files changed
archipelagos/messaging/Cargo.toml— added"Storage"toweb-sysfeatures.archipelagos/messaging/src/services/messaging_service.rs:pub struct OwnMessageIds(HashSet<String>+ orderedVec<String>, FIFO-capped atMAX_OWN_IDS_PER_CONTEXT = 5_000).pub fn load_own_message_ids(context_name)/pub fn save_own_message_ids(context_name, &ids)— cfg-gatedlocalStorageon wasm32 (keyhero_messaging_own_msg_ids:<context>), no-op on native. Mirrors the storage pattern inarchipelagos/intelligence/ai/src/island.rs:17-51.chatmessage_to_messagedatanow takes a 4th&OwnMessageIdsargument;is_own = (key match) || own_ids.contains(&msg.sid.to_string());sender_name = "Me"wheneveris_own.archipelagos/messaging/src/archipelago.rs:let mut own_message_ids = use_signal(|| services::load_own_message_ids(&context_name));on mount.chatmessage_to_messagedatacall sites pass&own_message_ids.peek()(initial fetch, SSEResync, SSEChatmessageNew, send path).handle_send, on successful send:own_message_ids.with_mut(|s| s.insert(msg.sid.to_string())); services::save_own_message_ids(&ctx, &own_message_ids.peek());— written before the existing optimistic patch, so the just-sent SID is part of the set when the data is built.msg_data.is_own = true; msg_data.sender_name = "Me") preserved unchanged.server/src/main.rs— addedmutto 9let X = use_signal(...)bindings (arch_transitioning,island_transitioning,paradigm_visible,wasm_visible,ai_visible,backend_visible,cta_visible,cards_visible,active_section). Pre-existing 11 E0596 errors blocked the showcase wasm build entirely ondevelopment. Bundled here so the fix is verifiable inmake run. (Could be split into a separate PR if preferred.)Diff stats vs
developmentVerification
cargo test -p hero_archipelagos_messaging --lib: 5/5 passed.cargo check --workspace --exclude hero_archipelagos_messaging: clean.cargo check -p archipelagos_server --target wasm32-unknown-unknown: clean (after themutpatch).make run->http://localhost:8886-> messaging island -> sent messages renderbubble-own, F5, re-open chat, messages still renderbubble-own. localStorage entryhero_messaging_own_msg_ids:<context>populated. No re-fetch storm on send.Notes
use_effectreactively subscribe toown_message_idsso the message list would re-derive when the set grew. That caused a re-fetch + wholesaleactive_messages.set(...)on every send, visible as a screen jump. Replaced the reactive subscription with.peek()everywhere — the set only matters on the initial chat load after F5; mid-session, the optimistic patch inhandle_sendalready does the right thing.bubble-otheruntil real auth lands — acceptable degradation given that auth is the proper fix.MAX_OWN_IDS_PER_CONTEXTis the constant if it ever needs tuning.sender_public_key, the existing key-match path takes precedence and the persisted-IDs path becomes dead code — retire in a follow-up: deleteOwnMessageIds, drop the parameter fromchatmessage_to_messagedata, one-time-clearhero_messaging_own_msg_ids:*keys on next load.Out of scope
sender_public_keywithX-Hero-Context(option C) — tracked in hero_osis #40.Pull request opened: #210
This PR implements the changes discussed in this issue.