Files
Duc Nguyenandviettranx ebc82d3265 fix: clear cron session in DB when not cached (cold-cache after restart) (#424)
* fix: clear cron session in DB when not cached (cold-cache after restart)

Fixes #365

PGSessionStore.Reset() only cleared the in-memory cache. After a server
restart the cache is empty, so Reset was a no-op — the next GetOrCreate
loaded the full accumulated history from DB, causing LLM tool loops from
contradictory context.

Add a DB fallback: when the session isn't in cache, issue a direct
UPDATE to clear messages and summary in PostgreSQL. This ensures cron
sessions always start clean regardless of server restarts.

The #294 fix (Reset+Save before each cron run) already had the right
intent but only worked within the same server lifetime.

* fix: move DB call outside mutex and log errors in Reset cold-cache path

ExecContext was running under s.mu.Lock(), blocking all session cache
operations during DB round-trip. Release the lock before the DB call.
Also log ExecContext errors instead of silently discarding them —
silent failure defeats the purpose of the cold-cache fix.

---------

Co-authored-by: viettranx <viettranx@gmail.com>
2026-03-24 20:47:44 +07:00

70 lines
1.8 KiB
Go

package pg
import (
"context"
"log/slog"
"time"
"github.com/nextlevelbuilder/goclaw/internal/providers"
)
func (s *PGSessionStore) TruncateHistory(ctx context.Context, key string, keepLast int) {
s.mu.Lock()
defer s.mu.Unlock()
if data, ok := s.cache[sessionCacheKey(ctx, key)]; ok {
if keepLast <= 0 {
data.Messages = []providers.Message{}
} else if len(data.Messages) > keepLast {
data.Messages = data.Messages[len(data.Messages)-keepLast:]
}
data.Updated = time.Now()
}
}
func (s *PGSessionStore) SetHistory(ctx context.Context, key string, msgs []providers.Message) {
s.mu.Lock()
defer s.mu.Unlock()
if data, ok := s.cache[sessionCacheKey(ctx, key)]; ok {
data.Messages = msgs
data.Updated = time.Now()
}
}
func (s *PGSessionStore) Reset(ctx context.Context, key string) {
s.mu.Lock()
if data, ok := s.cache[sessionCacheKey(ctx, key)]; ok {
data.Messages = []providers.Message{}
data.Summary = ""
data.Updated = time.Now()
s.mu.Unlock()
return
}
s.mu.Unlock()
// Session not in cache (e.g. after server restart). Clear directly in DB
// so the next GetOrCreate loads a clean session instead of stale history.
tid := tenantIDForInsert(ctx)
if _, err := s.db.ExecContext(ctx,
`UPDATE sessions SET messages = '[]', summary = '', updated_at = $1
WHERE session_key = $2 AND tenant_id = $3`,
time.Now(), key, tid,
); err != nil {
slog.Warn("sessions.reset_db_fallback_failed", "key", key, "error", err)
}
}
func (s *PGSessionStore) Delete(ctx context.Context, key string) error {
s.mu.Lock()
delete(s.cache, sessionCacheKey(ctx, key))
s.mu.Unlock()
// Clean up associated media files before deleting from DB.
if s.OnDelete != nil {
s.OnDelete(key)
}
tid := tenantIDForInsert(ctx)
_, err := s.db.ExecContext(ctx, "DELETE FROM sessions WHERE session_key = $1 AND tenant_id = $2", key, tid)
return err
}