agent: debounce session persistence

saveSession() now sets a dirty flag and starts a 2-second timer
instead of immediately serializing and writing the full message
history. For a 50-step turn, this reduces 50 JSON marshals + disk
writes down to ~25 (one every 2s) plus a guaranteed flush at
turn end and on close.

flushSave() is called:
- At end of turn (after setState idle)
- On interrupt
- On Close()
- When the 2s timer fires (if still dirty)
This commit is contained in:
ollie 2026-07-21 12:00:30 +02:00
parent 69b90de68f
commit bd3b46bbed
1 changed files with 28 additions and 0 deletions

View File

@ -289,6 +289,11 @@ type agent struct {
submitMu sync.Mutex // serializes Submit calls (commands + turns)
warnedContext bool // true after a context-usage warning; cleared on compaction
auditLog *olog.Logger
// Debounced session persistence
saveMu sync.Mutex
saveDirty bool
saveTimer *time.Timer
}
// ToolCallCount returns the total number of tool calls executed in this
@ -460,6 +465,7 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
// Close releases resources for this session, including its tmpdir.
func (s *agent) Close() {
s.log.Debug("Close() session=%q", s.sessionID)
s.flushSave()
if s.runtime != nil && s.runtime.Dispatcher != nil {
if srv, ok := s.runtime.Dispatcher.GetServer("execute"); ok {
if c, ok := srv.(interface{ Close() }); ok {
@ -805,6 +811,27 @@ func (s *agent) runCompact(ctx context.Context, trigger string) (int, error) {
}
func (s *agent) saveSession() {
s.saveMu.Lock()
s.saveDirty = true
if s.saveTimer == nil {
s.saveTimer = time.AfterFunc(2*time.Second, s.flushSave)
}
s.saveMu.Unlock()
}
// flushSave immediately persists the session if dirty.
func (s *agent) flushSave() {
s.saveMu.Lock()
dirty := s.saveDirty
s.saveDirty = false
if s.saveTimer != nil {
s.saveTimer.Stop()
s.saveTimer = nil
}
s.saveMu.Unlock()
if !dirty {
return
}
if s.session == nil || s.sessionID == "" || s.sessionsDir == "" {
return
}
@ -1292,6 +1319,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
s.mu.Unlock()
replyBuf.Reset()
s.setState("idle")
s.flushSave()
if err != nil {
// Keep completed work — only remove cancelled tool results.