Files
cc-connect/core/session.go
T
chenhg5andClaude 92164b38fb feat(claudecode): add session management and persistence support
Add session listing capability via Agent interface
Implement session persistence in SessionManager
Improve Feishu markdown formatting support
Add verbose logging and error handling
Clean up session switching logic
Support session resume via --resume flag
Add agent session metadata scanning

generated by llmgit

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-28 12:53:43 +08:00

234 lines
5.5 KiB
Go

package core
import (
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"sync"
"time"
)
// Session tracks one conversation between a user and the agent.
type Session struct {
ID string `json:"id"`
Name string `json:"name"`
AgentSessionID string `json:"agent_session_id"`
History []HistoryEntry `json:"history"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
mu sync.Mutex `json:"-"`
busy bool `json:"-"`
}
func (s *Session) TryLock() bool {
s.mu.Lock()
defer s.mu.Unlock()
if s.busy {
return false
}
s.busy = true
return true
}
func (s *Session) Unlock() {
s.mu.Lock()
defer s.mu.Unlock()
s.busy = false
s.UpdatedAt = time.Now()
}
func (s *Session) AddHistory(role, content string) {
s.mu.Lock()
defer s.mu.Unlock()
s.History = append(s.History, HistoryEntry{
Role: role,
Content: content,
Timestamp: time.Now(),
})
}
// GetHistory returns the last n entries. If n <= 0, returns all.
func (s *Session) GetHistory(n int) []HistoryEntry {
s.mu.Lock()
defer s.mu.Unlock()
total := len(s.History)
if n <= 0 || n > total {
n = total
}
out := make([]HistoryEntry, n)
copy(out, s.History[total-n:])
return out
}
// sessionSnapshot is the JSON-serializable state of the SessionManager.
type sessionSnapshot struct {
Sessions map[string]*Session `json:"sessions"`
ActiveSession map[string]string `json:"active_session"`
UserSessions map[string][]string `json:"user_sessions"`
Counter int64 `json:"counter"`
}
// SessionManager supports multiple named sessions per user with active-session tracking.
// It can persist state to a JSON file and reload on startup.
type SessionManager struct {
mu sync.RWMutex
sessions map[string]*Session
activeSession map[string]string
userSessions map[string][]string
counter int64
storePath string // empty = no persistence
}
func NewSessionManager(storePath string) *SessionManager {
sm := &SessionManager{
sessions: make(map[string]*Session),
activeSession: make(map[string]string),
userSessions: make(map[string][]string),
storePath: storePath,
}
if storePath != "" {
sm.load()
}
return sm
}
func (sm *SessionManager) nextID() string {
sm.counter++
return fmt.Sprintf("s%d", sm.counter)
}
func (sm *SessionManager) GetOrCreateActive(userKey string) *Session {
sm.mu.Lock()
defer sm.mu.Unlock()
if sid, ok := sm.activeSession[userKey]; ok {
if s, ok := sm.sessions[sid]; ok {
return s
}
}
return sm.createLocked(userKey, "default")
}
func (sm *SessionManager) NewSession(userKey, name string) *Session {
sm.mu.Lock()
defer sm.mu.Unlock()
s := sm.createLocked(userKey, name)
sm.saveLocked()
return s
}
func (sm *SessionManager) createLocked(userKey, name string) *Session {
id := sm.nextID()
now := time.Now()
s := &Session{
ID: id,
Name: name,
CreatedAt: now,
UpdatedAt: now,
}
sm.sessions[id] = s
sm.activeSession[userKey] = id
sm.userSessions[userKey] = append(sm.userSessions[userKey], id)
return s
}
func (sm *SessionManager) SwitchSession(userKey, target string) (*Session, error) {
sm.mu.Lock()
defer sm.mu.Unlock()
for _, sid := range sm.userSessions[userKey] {
s := sm.sessions[sid]
if s != nil && (s.ID == target || s.Name == target) {
sm.activeSession[userKey] = s.ID
sm.saveLocked()
return s, nil
}
}
return nil, fmt.Errorf("session %q not found", target)
}
func (sm *SessionManager) ListSessions(userKey string) []*Session {
sm.mu.RLock()
defer sm.mu.RUnlock()
ids := sm.userSessions[userKey]
out := make([]*Session, 0, len(ids))
for _, sid := range ids {
if s, ok := sm.sessions[sid]; ok {
out = append(out, s)
}
}
return out
}
func (sm *SessionManager) ActiveSessionID(userKey string) string {
sm.mu.RLock()
defer sm.mu.RUnlock()
return sm.activeSession[userKey]
}
// Save persists current state to disk. Safe to call from outside (e.g. after message processing).
func (sm *SessionManager) Save() {
sm.mu.RLock()
defer sm.mu.RUnlock()
sm.saveLocked()
}
func (sm *SessionManager) saveLocked() {
if sm.storePath == "" {
return
}
snap := sessionSnapshot{
Sessions: sm.sessions,
ActiveSession: sm.activeSession,
UserSessions: sm.userSessions,
Counter: sm.counter,
}
data, err := json.MarshalIndent(snap, "", " ")
if err != nil {
slog.Error("session: failed to marshal", "error", err)
return
}
if err := os.MkdirAll(filepath.Dir(sm.storePath), 0o755); err != nil {
slog.Error("session: failed to create dir", "error", err)
return
}
if err := os.WriteFile(sm.storePath, data, 0o644); err != nil {
slog.Error("session: failed to write", "path", sm.storePath, "error", err)
}
}
func (sm *SessionManager) load() {
data, err := os.ReadFile(sm.storePath)
if err != nil {
if !os.IsNotExist(err) {
slog.Error("session: failed to read", "path", sm.storePath, "error", err)
}
return
}
var snap sessionSnapshot
if err := json.Unmarshal(data, &snap); err != nil {
slog.Error("session: failed to unmarshal", "path", sm.storePath, "error", err)
return
}
sm.sessions = snap.Sessions
sm.activeSession = snap.ActiveSession
sm.userSessions = snap.UserSessions
sm.counter = snap.Counter
if sm.sessions == nil {
sm.sessions = make(map[string]*Session)
}
if sm.activeSession == nil {
sm.activeSession = make(map[string]string)
}
if sm.userSessions == nil {
sm.userSessions = make(map[string][]string)
}
slog.Info("session: loaded from disk", "path", sm.storePath, "sessions", len(sm.sessions))
}