feat: session pause/resume to reduce resource usage

Adds pause and resume commands to session/ctl:
- pause: stops the tool server process, closes connections
- resume: respawns the process and re-establishes connection

When paused, tool calls return 'connection closed' errors which
propagate to the chat log. Useful for reducing resource usage when
many sessions exist but aren't actively being used.

Also adds session/{id}/paused file to check pause status.
This commit is contained in:
Levi Neely 2026-08-03 18:08:09 +02:00
parent ff3f8c8819
commit 8cebf08cbd
2 changed files with 66 additions and 0 deletions

View File

@ -33,6 +33,14 @@ func listSessions(ctx HandlerCtx) ([]FsNodeDecl, error) {
return []byte(sessionEnvContent(s)), nil
})),
// paused — session pause status (read-only)
Leaf("paused", 0444, Read(func(_ HandlerCtx) ([]byte, error) {
if s.IsPaused() {
return []byte("true\n"), nil
}
return []byte("false\n"), nil
})),
// ctl — session control
Leaf("ctl", 0666,
Read(nopRead),
@ -143,6 +151,10 @@ func handleSessionCtl(sess *Session, kill func(), data []byte) error {
}
case "invalidate":
sess.InvalidateModelsCache()
case "pause":
return sess.Pause()
case "resume":
return sess.Resume()
default:
return fmt.Errorf("unknown session ctl command: %s", cmd[0])
}

View File

@ -25,6 +25,7 @@ type Session struct {
cancel context.CancelFunc
proc *toolsrv.Process // toolsrv subprocess (session owns lifecycle)
keeper *toolsrv.ProcessKeeper // resilient process manager (respawns on failure)
paused bool // true when session is paused (process stopped)
// toolsConn is the multiplexed connection to the remote tool server,
// shared by agent execution and 9P tool-management handlers.
@ -227,6 +228,59 @@ func (sess *Session) Interrupt() {
}
}
// IsPaused returns true if the session is paused.
func (sess *Session) IsPaused() bool {
sess.mu.RLock()
defer sess.mu.RUnlock()
return sess.paused
}
// Pause stops the tool server process to save resources. Tool calls will
// return errors until Resume is called. Returns an error if already paused.
func (sess *Session) Pause() error {
sess.mu.Lock()
defer sess.mu.Unlock()
if sess.paused {
return fmt.Errorf("session already paused")
}
// Close the keeper which kills the underlying process
if sess.keeper != nil {
sess.keeper.Close()
} else if sess.proc != nil {
sess.proc.Close()
}
// Close the connection (it will fail on next use anyway)
if sess.toolsConn != nil {
if c, ok := sess.toolsConn.(interface{ Close() }); ok {
c.Close()
}
sess.toolsConn = nil
}
sess.paused = true
return nil
}
// Resume restarts the tool server process after a Pause. Returns an error
// if not paused or if respawn fails.
func (sess *Session) Resume() error {
sess.mu.Lock()
defer sess.mu.Unlock()
if !sess.paused {
return fmt.Errorf("session not paused")
}
if sess.keeper == nil {
return fmt.Errorf("no process keeper available")
}
// Dial triggers respawn if needed
conn, err := sess.keeper.Dial()
if err != nil {
return fmt.Errorf("resume failed: %w", err)
}
sess.toolsConn = conn
sess.paused = false
return nil
}
// --- AgentLog methods ---
// AppendLog appends data to the agent's log and bumps the version.