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.