From f85612a4da858806d2f345e6db275ceca487e6a3 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 1 Oct 2026 13:34:33 +0200 Subject: [PATCH] session: pause all sessions on shutdown, fix agent config loading Shutdown changes: - Shutdown() now calls PauseAll() before closing sessions - PauseAll() interrupts agents then pauses each session - Sessions persist their state, allowing resume on next startup Agent loading fix: - LoadConfig returns (*AgentConfig, error) instead of *AgentConfig - Parse errors are no longer silently discarded - Callers handle nil config gracefully (no nil pointer dereferences) --- cmd/olliesrv/internal/agent/config_paths.go | 12 ++++--- cmd/olliesrv/internal/session/registry.go | 35 ++++++++++++++++++--- cmd/olliesrv/internal/session/session.go | 14 +++++++-- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/cmd/olliesrv/internal/agent/config_paths.go b/cmd/olliesrv/internal/agent/config_paths.go index b421b2a..0ddd32f 100644 --- a/cmd/olliesrv/internal/agent/config_paths.go +++ b/cmd/olliesrv/internal/agent/config_paths.go @@ -17,16 +17,20 @@ func AgentConfigPath(agentsDir, name string) string { } // LoadConfig loads an agent configuration file by profile name. -func LoadConfig(agentsDir, name string, open func(string) (*os.File, error)) *AgentConfig { +// Returns (nil, nil) if the file does not exist. +// Returns (nil, err) for parse errors. +func LoadConfig(agentsDir, name string, open func(string) (*os.File, error)) (*AgentConfig, error) { if open == nil { open = os.Open } path := AgentConfigPath(agentsDir, name) f, err := open(path) if err != nil { - return nil + if os.IsNotExist(err) { + return nil, nil + } + return nil, err } defer f.Close() - cfg, _ := Load(f) - return cfg + return Load(f) } diff --git a/cmd/olliesrv/internal/session/registry.go b/cmd/olliesrv/internal/session/registry.go index b9c51fd..3359611 100644 --- a/cmd/olliesrv/internal/session/registry.go +++ b/cmd/olliesrv/internal/session/registry.go @@ -170,9 +170,10 @@ func Rename(oldName, newName string) error { return nil } -// Shutdown interrupts all sessions, waits for idle, and closes them. +// Shutdown pauses all sessions (to allow later resume), waits for idle, then closes them. func Shutdown() { - InterruptAll() + // First, pause all sessions to persist their state. + PauseAll() waitIdle(100*time.Millisecond, 5*time.Second) mu.Lock() @@ -188,6 +189,8 @@ func Shutdown() { delete(sessions, name) mu.Unlock() if sess != nil { + // Session is already paused, so Cancel/Keeper/Proc should be nil. + // But close anything still open defensively. if sess.Cancel != nil { sess.Cancel() } @@ -202,6 +205,25 @@ func Shutdown() { } } +// PauseAll pauses every active session. Agents are interrupted first. +func PauseAll() { + InterruptAll() + mu.RLock() + var toProcess []*Session + for _, sess := range sessions { + if !sess.IsPaused() { + toProcess = append(toProcess, sess) + } + } + mu.RUnlock() + + for _, sess := range toProcess { + if err := sess.Pause(); err != nil { + pkgLog.Warn("pause session %s: %v", sess.ID, err) + } + } +} + // InterruptAll interrupts every active session. func InterruptAll() { mu.RLock() @@ -380,7 +402,10 @@ func buildAgent(sess *Session, p AgentParams) (*agent.Agent, error) { return nil, fmt.Errorf("session cwd is required") } - cfg := agent.LoadConfig(pkgAgentsDir, p.Profile, nil) + cfg, err := agent.LoadConfig(pkgAgentsDir, p.Profile, nil) + if err != nil { + return nil, fmt.Errorf("load agent config %q: %w", p.Profile, err) + } sessID := sess.ID // Set up tool server — reuse existing session infrastructure @@ -431,7 +456,7 @@ func buildAgent(sess *Session, p AgentParams) (*agent.Agent, error) { // Create backend beName := p.Backend - if beName == "" { + if beName == "" && cfg != nil { beName = cfg.Backend } be, err := backend.NewWithName(beName) @@ -442,7 +467,7 @@ func buildAgent(sess *Session, p AgentParams) (*agent.Agent, error) { if p.Model != "" { rt.Backend.SetModel(p.Model) - } else if cfg.Model != "" { + } else if cfg != nil && cfg.Model != "" { rt.Backend.SetModel(cfg.Model) } diff --git a/cmd/olliesrv/internal/session/session.go b/cmd/olliesrv/internal/session/session.go index 5e1721e..9bc59ee 100644 --- a/cmd/olliesrv/internal/session/session.go +++ b/cmd/olliesrv/internal/session/session.go @@ -496,15 +496,23 @@ func (s *Session) Resume() error { // rebuildAgentRuntime rebuilds the full runtime for an agent (used on resume). func rebuildAgentRuntime(ag *agent.Agent, sessID string) error { - cfg := agent.LoadConfig(pkgAgentsDir, ag.Profile(), os.Open) + cfg, err := agent.LoadConfig(pkgAgentsDir, ag.Profile(), os.Open) + if err != nil { + return fmt.Errorf("load agent config %q: %w", ag.Profile(), err) + } layers := BuildPromptLayers(cfg, ag.Cwd(), sessID, ag.ID(), "linux", false, "") env := []string{"OLLIE_SESSION_ID=" + sessID, "OLLIE_UNAME=" + ag.ID()} rt := agent.BuildRuntime(cfg, ag.ToolServer(), ag.Cwd(), env, layers.SystemPrompt, layers.EnvBlock) - be, err := backend.NewWithName(cfg.Backend) + + var beName string + if cfg != nil { + beName = cfg.Backend + } + be, err := backend.NewWithName(beName) if err != nil { return fmt.Errorf("backend: %w", err) } - if cfg.Model != "" { + if cfg != nil && cfg.Model != "" { be.SetModel(cfg.Model) } rt.Backend = be