Files
cc-connect/core/session.go
T
chenhg5andClaude e22a6028f4 feat(codex): add OpenAI Codex agent support with docs and config updates
Add new Codex agent implementation with permission modes, session management, and history retrieval. Update installation docs, README files, and config templates to reflect Codex support. Enhance session listing and history commands to work with both Claude and Codex agents.

generated by llmgit

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-01 09:20:38 +08:00

240 lines
5.6 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(),
})
}
func (s *Session) ClearHistory() {
s.mu.Lock()
defer s.mu.Unlock()
s.History = nil
}
// 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))
}