Location
crates/librefang-kernel/src/kernel/mod.rs:5115-5499 (send_message_streaming_* family)
crates/librefang-kernel/src/kernel/mod.rs:5985 (run_agent_loop_streaming)
crates/librefang-kernel/src/kernel/mod.rs:5431 (only try_read on config_reload_lock, no agent/session mutex)
Problem
Non-streaming send_message_full acquires agent_msg_locks[agent_id] for the duration of the LLM loop (mod.rs:4733). The streaming path only does try_read on config_reload_lock and spawns the loop without taking either agent_msg_locks or session_msg_locks. Concurrent streaming-from-dashboard + non-streaming-from-channel-bridge both load the same session row, mutate session.messages independently, then race to save_session — last write wins.
Impact
A user typing into dashboard while a channel adapter relays a message: one branch's user message + assistant reply silently disappears from persisted history. Worse: each branch's LLM also sees a different (stale) context, so replies are computed from an inconsistent view.
Fix
Take agent_msg_locks[agent_id].lock_owned().await (or .try_lock_owned() if streaming wants to fail-fast on contention) before spawning the streamed loop, move the OwnedMutexGuard into the spawned task. Drop the misleading "non-blocking try_read on config_reload_lock" comment — the lock that was needed is agent_msg_locks, not config-reload.
Filed via automated multi-perspective audit (security / concurrency / architecture / error-handling / frontend / performance subagents). Curated against #3359–#3409 to avoid duplicates.
Location
crates/librefang-kernel/src/kernel/mod.rs:5115-5499(send_message_streaming_*family)crates/librefang-kernel/src/kernel/mod.rs:5985(run_agent_loop_streaming)crates/librefang-kernel/src/kernel/mod.rs:5431(onlytry_readonconfig_reload_lock, no agent/session mutex)Problem
Non-streaming
send_message_fullacquiresagent_msg_locks[agent_id]for the duration of the LLM loop (mod.rs:4733). The streaming path only doestry_readonconfig_reload_lockand spawns the loop without taking eitheragent_msg_locksorsession_msg_locks. Concurrent streaming-from-dashboard + non-streaming-from-channel-bridge both load the same session row, mutatesession.messagesindependently, then race tosave_session— last write wins.Impact
A user typing into dashboard while a channel adapter relays a message: one branch's user message + assistant reply silently disappears from persisted history. Worse: each branch's LLM also sees a different (stale) context, so replies are computed from an inconsistent view.
Fix
Take
agent_msg_locks[agent_id].lock_owned().await(or.try_lock_owned()if streaming wants to fail-fast on contention) before spawning the streamed loop, move theOwnedMutexGuardinto the spawned task. Drop the misleading "non-blocking try_read on config_reload_lock" comment — the lock that was needed isagent_msg_locks, not config-reload.Filed via automated multi-perspective audit (security / concurrency / architecture / error-handling / frontend / performance subagents). Curated against #3359–#3409 to avoid duplicates.