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)
This commit is contained in:
Levi Neely 2026-10-01 13:34:33 +02:00
parent c1888d31fe
commit f85612a4da
3 changed files with 49 additions and 12 deletions

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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