From 8cebf08cbd15387ca2a18733c417e2e547b944fd Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Mon, 3 Aug 2026 18:08:09 +0200 Subject: [PATCH] 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. --- fs/sessionfiles.go | 12 +++++++++++ fs/types.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/fs/sessionfiles.go b/fs/sessionfiles.go index ee93d4b..2307be3 100644 --- a/fs/sessionfiles.go +++ b/fs/sessionfiles.go @@ -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]) } diff --git a/fs/types.go b/fs/types.go index 75cfc1b..ac72c1f 100644 --- a/fs/types.go +++ b/fs/types.go @@ -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.