Problem
StreamingProxyActorStore uses a singleton ConcurrentDictionary to track room participants in-memory. This violates the architecture constraint prohibiting middleware-level in-memory entity state mappings (CLAUDE.md: 中间层状态约束).
Current issues:
- Drift on restart: participant data is lost when the process restarts, even though the actor's event-sourced state (
StreamingProxyGAgentState) has the authoritative participant list
- No leave tracking from external sources: only the HTTP join endpoint updates the store;
GroupChatParticipantLeftEvent from other producers is not reflected
- Not replay-driven: the store is not rebuilt from committed events, so it can diverge from the actor's authority with no repair path
- Concurrency risk:
ConcurrentDictionary + lock pattern in a singleton, acting as a second source of truth alongside the actor state
Current State
The in-memory store is used by:
HandleListParticipantsAsync — returns participant snapshot for REST clients
HandleJoinAsync — tracks new participants
StreamingProxyNyxParticipantCoordinator.EnsureParticipantsJoinedAsync — checks existing participants before joining Nyx providers
Real-time participant changes are visible via the SSE stream, but snapshot queries (reconnect/refresh flows) depend on the in-memory store.
Proposed Solution
Materialize an actor-scoped current-state readmodel from committed GroupChatParticipantJoined/Left events through the projection pipeline:
- Create a
StreamingProxyParticipantProjector that consumes committed participant events
- Back it with an in-memory document store (dev) or persistent store (prod)
- Replace
StreamingProxyActorStore.ListParticipants/AddParticipant with readmodel queries
- Remove the
ConcurrentDictionary-based participant tracking from StreamingProxyActorStore
The room tracking in StreamingProxyActorStore (used only by NyxParticipantCoordinator) can remain as-is or be migrated to IGAgentActorStore in a follow-up.
References
Problem
StreamingProxyActorStoreuses a singletonConcurrentDictionaryto track room participants in-memory. This violates the architecture constraint prohibiting middleware-level in-memory entity state mappings (CLAUDE.md: 中间层状态约束).Current issues:
StreamingProxyGAgentState) has the authoritative participant listGroupChatParticipantLeftEventfrom other producers is not reflectedConcurrentDictionary+lockpattern in a singleton, acting as a second source of truth alongside the actor stateCurrent State
The in-memory store is used by:
HandleListParticipantsAsync— returns participant snapshot for REST clientsHandleJoinAsync— tracks new participantsStreamingProxyNyxParticipantCoordinator.EnsureParticipantsJoinedAsync— checks existing participants before joining Nyx providersReal-time participant changes are visible via the SSE stream, but snapshot queries (reconnect/refresh flows) depend on the in-memory store.
Proposed Solution
Materialize an actor-scoped current-state readmodel from committed
GroupChatParticipantJoined/Leftevents through the projection pipeline:StreamingProxyParticipantProjectorthat consumes committed participant eventsStreamingProxyActorStore.ListParticipants/AddParticipantwith readmodel queriesConcurrentDictionary-based participant tracking fromStreamingProxyActorStoreThe room tracking in
StreamingProxyActorStore(used only byNyxParticipantCoordinator) can remain as-is or be migrated toIGAgentActorStorein a follow-up.References