Files
goclaw/internal/store/pg/sessions.go
T
viettranx 4e9f155a4c feat(agent): adaptive tool timing with slow tool notification
Track per-tool execution time statistics in session metadata. When a tool
call exceeds its adaptive threshold (2x historical max, min 120s default),
send a direct outbound notification to the user.

- ToolTimingMap: parse/serialize/record/threshold from session metadata
- StartSlowTimer: fires once per tool call, auto-cancels on completion
- Team config: slow_tool toggle (default on, always direct, never leader)
- UI: toggle in team settings with i18n (en/vi/zh)
- Store: add GetSessionMetadata to session store interface
2026-03-19 13:35:57 +07:00

194 lines
4.2 KiB
Go

package pg
import (
"database/sql"
"encoding/json"
"maps"
"strings"
"sync"
"time"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/providers"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// PGSessionStore implements store.SessionStore backed by Postgres.
type PGSessionStore struct {
db *sql.DB
mu sync.RWMutex
// In-memory cache for hot sessions (reduces DB reads during tool loops)
cache map[string]*store.SessionData
// OnDelete is called with the session key when a session is deleted.
// Used for media file cleanup.
OnDelete func(sessionKey string)
}
func NewPGSessionStore(db *sql.DB) *PGSessionStore {
return &PGSessionStore{
db: db,
cache: make(map[string]*store.SessionData),
}
}
func (s *PGSessionStore) GetOrCreate(key string) *store.SessionData {
s.mu.Lock()
defer s.mu.Unlock()
if cached, ok := s.cache[key]; ok {
return cached
}
data := s.loadFromDB(key)
if data != nil {
s.cache[key] = data
return data
}
// Create new
now := time.Now()
data = &store.SessionData{
Key: key,
Messages: []providers.Message{},
Created: now,
Updated: now,
}
// Extract team_id from team session keys (agent:{agentId}:team:{teamId}:{chatId}).
var teamID *uuid.UUID
if parts := strings.SplitN(key, ":", 5); len(parts) >= 4 && parts[2] == "team" {
if tid, err := uuid.Parse(parts[3]); err == nil {
teamID = &tid
data.TeamID = teamID
}
}
s.cache[key] = data
msgsJSON, _ := json.Marshal([]providers.Message{})
s.db.Exec(
`INSERT INTO sessions (id, session_key, messages, created_at, updated_at, team_id)
VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (session_key) DO NOTHING`,
uuid.Must(uuid.NewV7()), key, msgsJSON, now, now, teamID,
)
return data
}
func (s *PGSessionStore) AddMessage(key string, msg providers.Message) {
s.mu.Lock()
defer s.mu.Unlock()
data := s.getOrInit(key)
data.Messages = append(data.Messages, msg)
data.Updated = time.Now()
}
func (s *PGSessionStore) GetHistory(key string) []providers.Message {
s.mu.RLock()
if data, ok := s.cache[key]; ok {
msgs := make([]providers.Message, len(data.Messages))
copy(msgs, data.Messages)
s.mu.RUnlock()
return msgs
}
s.mu.RUnlock()
// Not in cache — load from DB and cache it
s.mu.Lock()
defer s.mu.Unlock()
// Double-check after acquiring write lock
if data, ok := s.cache[key]; ok {
msgs := make([]providers.Message, len(data.Messages))
copy(msgs, data.Messages)
return msgs
}
data := s.loadFromDB(key)
if data == nil {
return nil
}
s.cache[key] = data
msgs := make([]providers.Message, len(data.Messages))
copy(msgs, data.Messages)
return msgs
}
func (s *PGSessionStore) GetSummary(key string) string {
s.mu.RLock()
defer s.mu.RUnlock()
if data, ok := s.cache[key]; ok {
return data.Summary
}
return ""
}
func (s *PGSessionStore) SetSummary(key, summary string) {
s.mu.Lock()
defer s.mu.Unlock()
if data, ok := s.cache[key]; ok {
data.Summary = summary
data.Updated = time.Now()
}
}
func (s *PGSessionStore) SetLabel(key, label string) {
s.mu.Lock()
defer s.mu.Unlock()
if data, ok := s.cache[key]; ok {
data.Label = label
data.Updated = time.Now()
}
}
func (s *PGSessionStore) GetSessionMetadata(key string) map[string]string {
s.mu.RLock()
defer s.mu.RUnlock()
if data, ok := s.cache[key]; ok && data.Metadata != nil {
out := make(map[string]string, len(data.Metadata))
maps.Copy(out, data.Metadata)
return out
}
return nil
}
func (s *PGSessionStore) SetSessionMetadata(key string, metadata map[string]string) {
s.mu.Lock()
defer s.mu.Unlock()
data := s.getOrInit(key)
if data.Metadata == nil {
data.Metadata = make(map[string]string)
}
maps.Copy(data.Metadata, metadata)
data.Updated = time.Now()
}
func (s *PGSessionStore) SetAgentInfo(key string, agentUUID uuid.UUID, userID string) {
s.mu.Lock()
defer s.mu.Unlock()
data := s.getOrInit(key)
if agentUUID != uuid.Nil {
data.AgentUUID = agentUUID
}
if userID != "" {
data.UserID = userID
}
}
func (s *PGSessionStore) UpdateMetadata(key, model, provider, channel string) {
s.mu.Lock()
defer s.mu.Unlock()
if data, ok := s.cache[key]; ok {
if model != "" {
data.Model = model
}
if provider != "" {
data.Provider = provider
}
if channel != "" {
data.Channel = channel
}
}
}