Refactoring Plan: Consolidate Session State Mutexes in ConversationService
Context
The ConversationService in internal/domain/service/conversation_service.go currently uses 8 separate mutexes to protect session-specific state maps:
| Mutex |
State Map |
Purpose |
conversationsMu |
conversations |
Conversation object storage |
processingMu |
processing |
Tool execution state tracking |
sessionModesMu |
sessionModes |
Plan mode state |
sessionThinkingModesMu |
sessionThinkingModes |
Thinking mode configuration |
sessionSystemPromptsMu |
sessionSystemPrompts |
Custom system prompts |
sessionTokenUsageMu |
sessionTokenUsage |
Token usage for auto-compaction |
sessionActiveSkillsMu |
sessionActiveSkills |
Active skills for tool restrictions |
sessionAllowedToolsMu |
sessionAllowedTools |
Cached allowed tools union |
The EndConversation method performs 16 lock/unlock operations (8 mutexes × 2 operations) to clean up a single session.
Problem Statement
- High Complexity: 8 separate mutexes make the code harder to reason about and maintain
- Performance Overhead: Each lock/unlock in
EndConversation adds latency for a cleanup operation
- Bug Surface Area: Easy to forget to lock one of the mutexes when adding new session state
- Existing Inconsistency: Comment on line 41-43 notes: "Pre-existing methods (GetConversation, EndConversation, etc.) access conversations without this mutex. Consistent usage is a separate refactor."
Root Cause
As features were incrementally added to ConversationService, each new session-specific map was given its own mutex. This was done with good intentions (fine-grained locking), but it didn't consider that:
- All session state maps share the same key (
sessionID)
- They are logically related (all describe metadata about a session)
- Most access patterns already require two locks anyway (check session exists → access state)
- Cleanup operations like
EndConversation must access all maps anyway
Proposed Solution
Consolidate all session-specific state into a single SessionState struct protected by one mutex.
Design
// SessionState groups all session-specific metadata under a single mutex
type SessionState struct {
mu sync.RWMutex
processing bool
planMode bool
thinkingMode port.ThinkingModeInfo
systemPrompt string
tokenUsage int64
activeSkills []entity.Skill
allowedTools map[string]bool
// Note: conversation entity remains separate for non-session-specific operations
}
// ConversationService struct changes
type ConversationService struct {
aiProvider port.AIProvider
toolExecutor port.ToolExecutor
conversations map[string]*entity.Conversation
conversationsMu sync.RWMutex // Protects conversations map
sessionStates map[string]*SessionState // New: single session state map
currentSession string
compactionThreshold int64
}
Benefits
- Single lock for session meta operations:
EndConversation now only needs 1-2 locks instead of 8
- Simpler mental model: All session metadata in one place
- Reduced bug surface: Only one mutex to remember when adding session state
- Consistent locking patterns: All session operations follow same pattern
- Better for atomic operations: Related state changes can be atomic
Trade-offs
- Increased lock contention: If many goroutines access different session metadata, they might wait on the same lock
- More complex individual operations: Reading/writing state requires an extra level of indirection
- Breaking changes: All methods accessing session state must be updated
Risk Assessment: Low to Medium
- The codebase has tests (
conversation_service_test.go) to verify functionality
- Session operations are not extremely hot paths (cleanup, configuration changes)
- Most session maps already require two locks (existence check + data access)
- RWMutex behavior can be preserved for read-heavy operations
Implementation Steps
Step 1: Create SessionState Struct
- Add
SessionState struct with all session-related fields
- Add
sessionStates map to ConversationService
Step 2: Update StartConversation
- Initialize SessionState for new sessions
- Keep conversations separate (not moved into SessionState)
Step 3: Update EndConversation
- Simplify to only lock
sessionStates[sessionID].mu
- Delete entire SessionState entry
Step 4: Update Setter Methods
- Update these methods to use
sessionStates[sessionID].mu:
SetPlanMode()
SetThinkingMode()
SetCustomSystemPrompt()
SetProcessingState()
AppendActiveSkill(), SetActiveSkills(), RemoveActiveSkill()
- Internal methods:
setTokenUsage(), etc.
Step 5: Update Getter Methods
- Update these methods to use
sessionStates[sessionID].mu (RLock):
IsPlanMode()
GetThinkingMode()
GetCustomSystemPrompt()
IsProcessing()
GetActiveSkills()
GetAllowedToolsForSession()
GetTokenUsage()
Step 6: Update Helper Methods
computeAllowedTools() - no changes needed
- Methods that set
sessionAllowedToolsMu must now get the session state first
Step 7: Remove Old Mutexes
- Remove 7 mutexes (keep conversationsMu)
- Remove their corresponding maps
- Keep
processing, sessionModes, etc. as struct fields in SessionState
Step 8: Run Tests
- Run full test suite:
go test ./internal/domain/service/...
- Verify all existing tests pass
Step 9: Code Review
- Ensure all mutex accesses are correct
- Check for potential deadlocks
- Verify RWMutex usage is appropriate
Files to Modify
- internal/domain/service/conversation_service.go
- Add
SessionState struct
- Modify
ConversationService struct
- Update all methods that access session state
Testing Plan
- Run existing unit tests to ensure no regression
- Verify thread safety through concurrent test scenarios
- Performance test to ensure no meaningful degradation
- Check for data races with
-race flag
Rollback Strategy
If issues arise:
- The refactor can be reverted by restoring the old structure
- Git history makes rollback straightforward
- Consider using a feature flag if gradual rollout is desired
Success Criteria
- ✅
EndConversation reduced from 16 to 2 lock/unlock operations
- ✅ All existing tests pass
- ✅ No race conditions detected with
-race flag
- ✅ Code is simpler and easier to understand
- ✅ Performance is not meaningfully degraded
Estimated Effort: 2-3 hours
Risk Level: Low-Medium
Priority: Medium (code quality improvement, not blocking feature work)
Refactoring Plan: Consolidate Session State Mutexes in ConversationService
Context
The
ConversationServiceininternal/domain/service/conversation_service.gocurrently uses 8 separate mutexes to protect session-specific state maps:conversationsMuconversationsprocessingMuprocessingsessionModesMusessionModessessionThinkingModesMusessionThinkingModessessionSystemPromptsMusessionSystemPromptssessionTokenUsageMusessionTokenUsagesessionActiveSkillsMusessionActiveSkillssessionAllowedToolsMusessionAllowedToolsThe
EndConversationmethod performs 16 lock/unlock operations (8 mutexes × 2 operations) to clean up a single session.Problem Statement
EndConversationadds latency for a cleanup operationRoot Cause
As features were incrementally added to
ConversationService, each new session-specific map was given its own mutex. This was done with good intentions (fine-grained locking), but it didn't consider that:sessionID)EndConversationmust access all maps anywayProposed Solution
Consolidate all session-specific state into a single
SessionStatestruct protected by one mutex.Design
Benefits
EndConversationnow only needs 1-2 locks instead of 8Trade-offs
Risk Assessment: Low to Medium
conversation_service_test.go) to verify functionalityImplementation Steps
Step 1: Create SessionState Struct
SessionStatestruct with all session-related fieldssessionStatesmap toConversationServiceStep 2: Update StartConversation
Step 3: Update EndConversation
sessionStates[sessionID].muStep 4: Update Setter Methods
sessionStates[sessionID].mu:SetPlanMode()SetThinkingMode()SetCustomSystemPrompt()SetProcessingState()AppendActiveSkill(),SetActiveSkills(),RemoveActiveSkill()setTokenUsage(), etc.Step 5: Update Getter Methods
sessionStates[sessionID].mu(RLock):IsPlanMode()GetThinkingMode()GetCustomSystemPrompt()IsProcessing()GetActiveSkills()GetAllowedToolsForSession()GetTokenUsage()Step 6: Update Helper Methods
computeAllowedTools()- no changes neededsessionAllowedToolsMumust now get the session state firstStep 7: Remove Old Mutexes
processing,sessionModes, etc. as struct fields in SessionStateStep 8: Run Tests
go test ./internal/domain/service/...Step 9: Code Review
Files to Modify
SessionStatestructConversationServicestructTesting Plan
-raceflagRollback Strategy
If issues arise:
Success Criteria
EndConversationreduced from 16 to 2 lock/unlock operations-raceflagEstimated Effort: 2-3 hours
Risk Level: Low-Medium
Priority: Medium (code quality improvement, not blocking feature work)