all: kill dead code, remove classifier system, clean up names

Dead code removed:
- agent: firstSentence, Checkpoint, WaitForChange, InitCond, cfgDir,
  execServer, SaveTo, SaveFull, saveTo, saveToFull, sanitizeMessages,
  LoadPersistedAgent, RestoreHistory, Restore, PersistedAgent type
- toolsrv: entire tier.go (MemoryTier, MemoryTierArgs, OutputFormat,
  CanParallelize on Server), same methods on Conn
- cmd/ollie-remote: can_parallelize, memory_tier, memory_tier_args RPC
  handlers
- toolsrv/rpc_integration_test: all classifier tests
- fs/lifecycle: state(root) dead helper

Renames:
- session.Session receiver: 'a' → 's' throughout
- SaveSession(path) → Save() (dead parameter removed)
- execServer() eliminated — callers use ag.runtime.ToolServer directly

The agent-package save/restore chain was entirely dead: PersistSession
in session/persist.go reads agent accessors directly and never called
SaveTo/SaveFull.
This commit is contained in:
Ollie Agent 2026-08-08 14:52:10 +02:00
parent b6694d409a
commit 63c4cd6173
11 changed files with 181 additions and 643 deletions

View File

@ -224,11 +224,6 @@ func (ag *Agent) WaitChange(ctx context.Context, field, current string) (string,
} }
} }
// InitCond initializes the changeCond. Must be called once after construction.
func (ag *Agent) InitCond() {
ag.changeCond = sync.NewCond(&ag.changeMu)
}
// emit sends an event to the agent's output handler. // emit sends an event to the agent's output handler.
func (ag *Agent) emit(ev Event) { func (ag *Agent) emit(ev Event) {
if ag.output != nil { if ag.output != nil {
@ -311,19 +306,6 @@ func (ag *Agent) HasHistory() bool {
return ag.history != nil return ag.history != nil
} }
// SaveTo saves the current history to the given path.
func (ag *Agent) SaveTo(path, name, cwd string) error {
if ag.history == nil {
return fmt.Errorf("no active session")
}
return ag.history.saveTo(path, name, ag.Name(), cwd)
}
// Restore restores agent history from a persisted session.
func (ag *Agent) Restore(ps *PersistedAgent) {
ag.history = RestoreHistory(ps)
}
// ToolCallCount returns the total number of tool calls executed. // ToolCallCount returns the total number of tool calls executed.
func (ag *Agent) ToolCallCount() int64 { func (ag *Agent) ToolCallCount() int64 {
return ag.toolCallCount.Load() return ag.toolCallCount.Load()
@ -511,19 +493,6 @@ func (ag *Agent) toolsNeedRefresh() bool {
return false return false
} }
// SaveFull persists the full session state (history + metadata) to the given path.
func (ag *Agent) SaveFull(path, sessionID, sessionName, cwd, remote string) error {
// Switching profiles intentionally clears the conversation history, but
// the profile and display name still need to be persisted. Save an empty
// history in that case rather than dropping the entire metadata snapshot.
history := ag.history
if history == nil {
history = &History{}
}
return history.saveToFull(path, sessionID, sessionName, ag.id, ag.Name(), ag.profile,
ag.runtime.Backend.Name(), ag.runtime.Backend.Model(), cwd, remote)
}
// CtxSz returns a human-readable context size string. // CtxSz returns a human-readable context size string.
func (ag *Agent) CtxSz() string { func (ag *Agent) CtxSz() string {
if ag.history == nil { if ag.history == nil {
@ -606,15 +575,10 @@ func (ag *Agent) ListModels() []string {
return ag.runtime.Backend.Models(context.Background()) return ag.runtime.Backend.Models(context.Background())
} }
// execServer returns the execute server, or nil if unavailable.
func (ag *Agent) execServer() toolsrv.Runner {
return ag.runtime.ToolServer
}
// ToolServer returns the tool execution server, or nil if unavailable. // ToolServer returns the tool execution server, or nil if unavailable.
// Exported for use by the 9P filesystem layer to sync tool registries. // Exported for use by the 9P filesystem layer to sync tool registries.
func (ag *Agent) ToolServer() toolsrv.Runner { func (ag *Agent) ToolServer() toolsrv.Runner {
return ag.execServer() return ag.runtime.ToolServer
} }
// Queue pushes a prompt onto the agent's FIFO. // Queue pushes a prompt onto the agent's FIFO.
@ -634,18 +598,9 @@ func (ag *Agent) BroadcastChange() {
ag.changeMu.Unlock() ag.changeMu.Unlock()
} }
// WaitForChange blocks until a state change is broadcast or ctx is cancelled.
func (ag *Agent) WaitForChange(ctx context.Context) {
ag.changeMu.Lock()
if ctx.Err() == nil {
ag.changeCond.Wait()
}
ag.changeMu.Unlock()
}
// Detach detaches the current running process to background. // Detach detaches the current running process to background.
func (ag *Agent) Detach() bool { func (ag *Agent) Detach() bool {
if srv := ag.execServer(); srv != nil { if srv := ag.runtime.ToolServer; srv != nil {
if d, ok := srv.(interface{ Detach() bool }); ok { if d, ok := srv.(interface{ Detach() bool }); ok {
return d.Detach() return d.Detach()
} }
@ -664,7 +619,7 @@ type DetachedInfo struct {
// ListDetached returns info about all detached processes. // ListDetached returns info about all detached processes.
func (ag *Agent) ListDetached() []DetachedInfo { func (ag *Agent) ListDetached() []DetachedInfo {
srv := ag.execServer() srv := ag.runtime.ToolServer
if srv == nil { if srv == nil {
return nil return nil
} }
@ -701,7 +656,7 @@ func (ag *Agent) ListDetached() []DetachedInfo {
// SignalDetached sends a signal to a detached process. // SignalDetached sends a signal to a detached process.
func (ag *Agent) SignalDetached(pid int, signal int) error { func (ag *Agent) SignalDetached(pid int, signal int) error {
if srv := ag.execServer(); srv != nil { if srv := ag.runtime.ToolServer; srv != nil {
type signaler interface { type signaler interface {
SignalDetached(int, syscall.Signal) error SignalDetached(int, syscall.Signal) error
} }
@ -714,7 +669,7 @@ func (ag *Agent) SignalDetached(pid int, signal int) error {
// GetDetachedOutput reads output from a detached process. // GetDetachedOutput reads output from a detached process.
func (ag *Agent) GetDetachedOutput(pid int) (string, error) { func (ag *Agent) GetDetachedOutput(pid int) (string, error) {
if srv := ag.execServer(); srv != nil { if srv := ag.runtime.ToolServer; srv != nil {
type outputGetter interface{ GetDetachedOutput(int) (string, error) } type outputGetter interface{ GetDetachedOutput(int) (string, error) }
if og, ok := srv.(outputGetter); ok { if og, ok := srv.(outputGetter); ok {
return og.GetDetachedOutput(pid) return og.GetDetachedOutput(pid)
@ -725,7 +680,7 @@ func (ag *Agent) GetDetachedOutput(pid int) (string, error) {
// DismissDetached removes a finished detached process. // DismissDetached removes a finished detached process.
func (ag *Agent) DismissDetached(pid int) bool { func (ag *Agent) DismissDetached(pid int) bool {
if srv := ag.execServer(); srv != nil { if srv := ag.runtime.ToolServer; srv != nil {
type dismisser interface{ DismissDetached(int) bool } type dismisser interface{ DismissDetached(int) bool }
if d, ok := srv.(dismisser); ok { if d, ok := srv.(dismisser); ok {
return d.DismissDetached(pid) return d.DismissDetached(pid)

View File

@ -5,7 +5,6 @@ import (
"crypto/rand" "crypto/rand"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"strings" "strings"
"ollie/backend" "ollie/backend"
@ -33,31 +32,6 @@ Be concise. Capture what another LLM needs to seamlessly continue.`
warmIndexSize = 10 warmIndexSize = 10
) )
// PersistedAgent is the on-disk format for a saved session.
type PersistedAgent struct {
ID string `json:"id"`
// SessionName is the mutable display name of the containing session.
SessionName string `json:"sessionName,omitempty"`
Uname string `json:"uname,omitempty"` // agent's immutable 9P identity
Agent string `json:"agent,omitempty"`
Profile string `json:"profile,omitempty"`
Backend string `json:"backend,omitempty"`
Model string `json:"model,omitempty"`
CWD string `json:"cwd,omitempty"`
Remote string `json:"remote,omitempty"`
Messages []backend.Message `json:"messages"`
TaskState *TaskState `json:"taskState,omitempty"`
// Usage and cost tracking, persisted across restarts.
TotalInputTokens int `json:"totalInputTokens,omitempty"`
TotalCachedInputTokens int `json:"totalCachedInputTokens,omitempty"`
TotalCacheCreationTokens int `json:"totalCacheCreationTokens,omitempty"`
TotalOutputTokens int `json:"totalOutputTokens,omitempty"`
TotalRequests int `json:"totalRequests,omitempty"`
Estimated bool `json:"estimated,omitempty"`
LastTurnCostUSD float64 `json:"lastTurnCostUSD,omitempty"`
SessionCostUSD float64 `json:"sessionCostUSD,omitempty"`
}
// TaskState is a compact structured overlay that summarizes the agent's // TaskState is a compact structured overlay that summarizes the agent's
// current position in a task. Injected at the top of every turn so the // current position in a task. Injected at the top of every turn so the
// model doesn't rely on full history recall. // model doesn't rely on full history recall.
@ -97,104 +71,6 @@ func (ts *TaskState) render() string {
return sb.String() return sb.String()
} }
// saveTo writes the full message history to path as JSON.
func (s *History) saveTo(path, id, displayName, cwd string) error {
return s.saveToFull(path, id, "", "", displayName, "", "", "", cwd, "")
}
// saveToFull writes session state including backend/model info.
func sanitizeMessages(msgs []backend.Message) []backend.Message {
out := make([]backend.Message, 0, len(msgs))
for _, m := range msgs {
for j, tc := range m.ToolCalls {
if len(tc.Arguments) > 0 {
var dummy json.RawMessage
if err := json.Unmarshal(tc.Arguments, &dummy); err != nil {
// Corrupted arguments — replace with empty object so save succeeds.
m.ToolCalls[j].Arguments = json.RawMessage("{}")
}
}
}
out = append(out, m)
}
return out
}
func (s *History) saveToFull(path, id, sessionName, uname, displayName, profile, backendName, modelName, cwd, remote string) error {
ps := PersistedAgent{
ID: id,
SessionName: sessionName,
Uname: uname,
Agent: displayName,
Profile: profile,
Backend: backendName,
Model: modelName,
CWD: cwd,
Remote: remote,
Messages: sanitizeMessages(s.messages),
TaskState: s.TaskState,
TotalInputTokens: s.TotalInputTokens,
TotalCachedInputTokens: s.TotalCachedInputTokens,
TotalCacheCreationTokens: s.TotalCacheCreationTokens,
TotalOutputTokens: s.TotalOutputTokens,
TotalRequests: s.TotalRequests,
Estimated: s.Estimated,
LastTurnCostUSD: s.LastTurnCostUSD,
SessionCostUSD: s.SessionCostUSD,
}
data, err := json.Marshal(ps)
if err != nil {
return fmt.Errorf("session save: %w", err)
}
// Atomic write: write to temp file, then rename to preserve last good snapshot.
tmpPath := path + ".tmp"
if err := os.WriteFile(tmpPath, data, 0600); err != nil {
return fmt.Errorf("session save tmp: %w", err)
}
return os.Rename(tmpPath, path)
}
// LoadPersistedAgent reads a PersistedAgent from a JSON file.
func LoadPersistedAgent(path string) (*PersistedAgent, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var ps PersistedAgent
if err := json.Unmarshal(data, &ps); err != nil {
return nil, fmt.Errorf("parse session %s: %w", path, err)
}
return &ps, nil
}
// RestoreHistory reconstructs a Session from a persisted message list.
func RestoreHistory(ps *PersistedAgent) *History {
s := &History{
messages: ps.Messages,
TaskState: ps.TaskState,
TotalInputTokens: ps.TotalInputTokens,
TotalCachedInputTokens: ps.TotalCachedInputTokens,
TotalCacheCreationTokens: ps.TotalCacheCreationTokens,
TotalOutputTokens: ps.TotalOutputTokens,
TotalRequests: ps.TotalRequests,
Estimated: ps.Estimated,
LastTurnCostUSD: ps.LastTurnCostUSD,
SessionCostUSD: ps.SessionCostUSD,
}
for i := range s.messages {
if s.messages[i].Role == "assistant" && s.messages[i].ID == "" {
s.messages[i].ID = NewResponseID()
}
}
for _, m := range ps.Messages {
if m.Role == "user" {
s.goal = m.Content
break
}
}
return s
}
// RestoreHistoryFromMessages reconstructs a History from just the message list. // RestoreHistoryFromMessages reconstructs a History from just the message list.
// Used for multi-agent session restore where each agent has separate messages. // Used for multi-agent session restore where each agent has separate messages.
func RestoreHistoryFromMessages(messages []backend.Message) *History { func RestoreHistoryFromMessages(messages []backend.Message) *History {
@ -244,21 +120,6 @@ func newHistory(goal string) *History {
} }
// Checkpoint forks the session: returns a new Session that inherits the given // Checkpoint forks the session: returns a new Session that inherits the given
// TaskState but starts with a clean message history. This enables narrow-context
// sub-agents that know what to do without inheriting all parent message noise.
func (s *History) Checkpoint(ts TaskState) *History {
child := &History{
goal: ts.Objective,
TaskState: &ts,
}
// Seed with a user message so the child has a valid initial turn.
child.messages = []backend.Message{{
Role: "user",
Content: ts.render(),
}}
return child
}
func (s *History) history() []backend.Message { func (s *History) history() []backend.Message {
return s.messages return s.messages
} }

View File

@ -2,7 +2,8 @@ package agent
import ( import (
"os" "os"
"path/filepath"
"ollie/paths"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -19,8 +20,7 @@ type modelsConfig struct {
// loadModelsConfig reads and parses the models config file. // loadModelsConfig reads and parses the models config file.
// Returns zero value if the file doesn't exist or is invalid. // Returns zero value if the file doesn't exist or is invalid.
func loadModelsConfig() modelsConfig { func loadModelsConfig() modelsConfig {
path := filepath.Join(cfgDir(), "models.yaml") data, err := os.ReadFile(paths.CfgDir() + "/models.yaml")
data, err := os.ReadFile(path)
if err != nil { if err != nil {
return modelsConfig{} return modelsConfig{}
} }
@ -30,12 +30,3 @@ func loadModelsConfig() modelsConfig {
} }
return cfg return cfg
} }
// cfgDir returns the ollie config directory, respecting XDG_CONFIG_HOME.
func cfgDir() string {
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
return filepath.Join(xdg, "ollie")
}
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config", "ollie")
}

View File

@ -389,16 +389,3 @@ func infoEvent(text string) Event {
return Event{Role: "info", Content: text + "\n"} return Event{Role: "info", Content: text + "\n"}
} }
// firstSentence returns the first sentence of s (up to the first period or
// newline), trimmed. Falls back to s truncated at 80 chars if no sentence end found.
func firstSentence(s string) string {
for i, r := range s {
if r == '.' || r == '\n' {
return strings.TrimSpace(s[:i+1])
}
}
if len(s) > 80 {
return s[:77] + "..."
}
return s
}

View File

@ -334,40 +334,6 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req rpcRequest, enc *lo
ok := srv.DismissDetached(params.PID) ok := srv.DismissDetached(params.PID)
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok}) enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
case "can_parallelize":
var params struct {
Name string `json:"name"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
return
}
ok := srv.CanParallelize(params.Name)
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})
case "memory_tier":
var params struct {
Name string `json:"name"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
return
}
tier := srv.MemoryTier(params.Name)
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: tier})
case "memory_tier_args":
var params struct {
Name string `json:"name"`
Args json.RawMessage `json:"args"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}})
return
}
tier := srv.MemoryTierArgs(params.Name, params.Args)
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: tier})
case "tool_load": case "tool_load":
var params struct { var params struct {
Name string `json:"name"` Name string `json:"name"`

View File

@ -322,7 +322,7 @@ func writeSessionCtl(ctx HandlerCtx, data []byte) error {
}, },
"save": func(_ []string, _ HandlerCtx) error { "save": func(_ []string, _ HandlerCtx) error {
if ctx.Session.Core != nil { if ctx.Session.Core != nil {
ctx.Session.Core.SaveSession("") ctx.Session.Core.Save()
} }
return nil return nil
}, },

View File

@ -57,10 +57,6 @@ func (rs *RootState) RenameNode(oldName, newName string) {
} }
} }
func state(root *Tree) *RootState {
return root.Data.(*RootState)
}
// Shutdown interrupts all sessions, waits for idle, persists, and closes. // Shutdown interrupts all sessions, waits for idle, persists, and closes.
func Shutdown(root *Tree) { func Shutdown(root *Tree) {
session.Shutdown() session.Shutdown()

View File

@ -155,7 +155,7 @@ func New(cfg Config) *Session {
name = cfg.SessionID[:idx] name = cfg.SessionID[:idx]
} }
a := &Session{ s := &Session{
id: cfg.SessionID, id: cfg.SessionID,
name: name, name: name,
env: make(map[string]string), env: make(map[string]string),
@ -185,18 +185,18 @@ func New(cfg Config) *Session {
SessionID: cfg.SessionID, SessionID: cfg.SessionID,
StartupMsgs: rt.Messages, StartupMsgs: rt.Messages,
ReadPlanStep: cfg.ReadPlanStep, ReadPlanStep: cfg.ReadPlanStep,
Save: a.saveSession, Save: s.saveSession,
Flush: a.flushSave, Flush: s.flushSave,
}) })
a.agents = append(a.agents, ag) s.agents = append(s.agents, ag)
ag.SetSessionEnv(a.id) ag.SetSessionEnv(s.id)
return a return s
} }
// FindAgent returns the agent matching the given name or ID, or nil. // FindAgent returns the agent matching the given name or ID, or nil.
func (a *Session) FindAgent(nameOrID string) *agent.Agent { func (s *Session) FindAgent(nameOrID string) *agent.Agent {
for _, ag := range a.agents { for _, ag := range s.agents {
if ag.Name() == nameOrID || ag.ID() == nameOrID { if ag.Name() == nameOrID || ag.ID() == nameOrID {
return ag return ag
} }
@ -205,37 +205,37 @@ func (a *Session) FindAgent(nameOrID string) *agent.Agent {
} }
// Agents returns the full agent slice. // Agents returns the full agent slice.
func (a *Session) Agents() []*agent.Agent { return a.agents } func (s *Session) Agents() []*agent.Agent { return s.agents }
// AgentAt returns the agent at the given index, or nil. // AgentAt returns the agent at the given index, or nil.
func (a *Session) AgentAt(idx int) *agent.Agent { func (s *Session) AgentAt(idx int) *agent.Agent {
if idx < 0 || idx >= len(a.agents) { if idx < 0 || idx >= len(s.agents) {
return nil return nil
} }
return a.agents[idx] return s.agents[idx]
} }
// AgentCount returns the number of agents. // AgentCount returns the number of agents.
func (a *Session) AgentCount() int { return len(a.agents) } func (s *Session) AgentCount() int { return len(s.agents) }
// SaveFuncs returns save and flush callbacks suitable for use as // SaveFuncs returns save and flush callbacks suitable for use as
// SaveFuncs returns the save and flush callbacks for use in // SaveFuncs returns the save and flush callbacks for use in
// agent.AgentCfg.Save and Flush when adding agents to this session. // agent.AgentCfg.Save and Flush when adding agents to this session.
func (a *Session) SaveFuncs() (save func(), flush func()) { func (s *Session) SaveFuncs() (save func(), flush func()) {
return a.saveSession, a.flushSave return s.saveSession, s.flushSave
} }
// AddAgent appends an agent to the session. // AddAgent appends an agent to the session.
func (a *Session) AddAgent(ag *agent.Agent) { func (s *Session) AddAgent(ag *agent.Agent) {
a.agents = append(a.agents, ag) s.agents = append(s.agents, ag)
ag.SetSessionEnv(a.id) ag.SetSessionEnv(s.id)
} }
// RemoveAgent removes an agent by ID and closes it. // RemoveAgent removes an agent by ID and closes it.
// Returns false if the agent was not found. // Returns false if the agent was not found.
func (a *Session) RemoveAgent(id string) bool { func (s *Session) RemoveAgent(id string) bool {
idx := -1 idx := -1
for i, ag := range a.agents { for i, ag := range s.agents {
if ag.ID() == id { if ag.ID() == id {
idx = i idx = i
break break
@ -244,61 +244,61 @@ func (a *Session) RemoveAgent(id string) bool {
if idx < 0 { if idx < 0 {
return false return false
} }
a.agents[idx].Close() s.agents[idx].Close()
a.agents = append(a.agents[:idx], a.agents[idx+1:]...) s.agents = append(s.agents[:idx], s.agents[idx+1:]...)
return true return true
} }
// Close releases resources for this session. // Close releases resources for this session.
func (a *Session) Close() { func (s *Session) Close() {
a.log.Debug("Close() session=%q", a.id) s.log.Debug("Close() session=%q", s.id)
a.flushSave() s.flushSave()
for _, ag := range a.agents { for _, ag := range s.agents {
ag.Close() ag.Close()
} }
if a.id != "" { if s.id != "" {
os.RemoveAll(filepath.Join(ollieTmpDir(), a.id)) //nolint:errcheck os.RemoveAll(filepath.Join(ollieTmpDir(), s.id)) //nolint:errcheck
} }
} }
// SetEnv stores a session-scoped variable and propagates to all agents. // SetEnv stores a session-scoped variable and propagates to all agents.
func (a *Session) SetEnv(key, value string) { func (s *Session) SetEnv(key, value string) {
a.envMu.Lock() s.envMu.Lock()
a.env[key] = value s.env[key] = value
a.envMu.Unlock() s.envMu.Unlock()
for _, ag := range a.agents { for _, ag := range s.agents {
ag.SetEnv(key, value) ag.SetEnv(key, value)
} }
} }
// Remote returns the remote target (e.g., SSH host) if any. // Remote returns the remote target (e.g., SSH host) if any.
func (a *Session) Remote() string { func (s *Session) Remote() string {
return a.remote return s.remote
} }
// SetRemote sets the remote target for this session. // SetRemote sets the remote target for this session.
func (a *Session) SetRemote(remote string) { func (s *Session) SetRemote(remote string) {
a.remote = remote s.remote = remote
} }
// SetSessionID renames the session. // SetSessionID renames the session.
func (a *Session) SetSessionID(newID string) error { func (s *Session) SetSessionID(newID string) error {
oldID := a.id oldID := s.id
if oldID == newID { if oldID == newID {
return nil return nil
} }
if a.sessionsDir != "" && oldID != "" { if s.sessionsDir != "" && oldID != "" {
for _, suffix := range []string{".json", ".compaction.jsonl"} { for _, suffix := range []string{".json", ".compaction.jsonl"} {
oldPath := a.activeSessionPath(oldID, suffix) oldPath := s.activeSessionPath(oldID, suffix)
if _, err := os.Stat(oldPath); err == nil { if _, err := os.Stat(oldPath); err == nil {
if err := os.Rename(oldPath, a.activeSessionPath(newID, suffix)); err != nil { if err := os.Rename(oldPath, s.activeSessionPath(newID, suffix)); err != nil {
return fmt.Errorf("rename %s: %w", suffix, err) return fmt.Errorf("rename %s: %w", suffix, err)
} }
} }
} }
} }
a.id = newID s.id = newID
for _, ag := range a.agents { for _, ag := range s.agents {
ag.RenamePreamble(oldID, newID) ag.RenamePreamble(oldID, newID)
} }
oldTemp := filepath.Join(ollieTmpDir(), oldID) oldTemp := filepath.Join(ollieTmpDir(), oldID)
@ -306,161 +306,160 @@ func (a *Session) SetSessionID(newID string) error {
if _, err := os.Stat(oldTemp); err == nil { if _, err := os.Stat(oldTemp); err == nil {
os.Rename(oldTemp, newTemp) //nolint:errcheck os.Rename(oldTemp, newTemp) //nolint:errcheck
} }
for _, ag := range a.agents { for _, ag := range s.agents {
ag.SetSessionEnv(newID) ag.SetSessionEnv(newID)
} }
return nil return nil
} }
func (a *Session) activeSessionPath(id, suffix string) string { func (s *Session) activeSessionPath(id, suffix string) string {
return filepath.Join(a.sessionsDir, "active", id+suffix) return filepath.Join(s.sessionsDir, "active", id+suffix)
} }
func (a *Session) saveSession() { func (s *Session) saveSession() {
a.saveMu.Lock() s.saveMu.Lock()
a.saveDirty = true s.saveDirty = true
if a.saveTimer == nil { if s.saveTimer == nil {
a.saveTimer = time.AfterFunc(2*time.Second, a.flushSave) s.saveTimer = time.AfterFunc(2*time.Second, s.flushSave)
} }
a.saveMu.Unlock() s.saveMu.Unlock()
} }
func (a *Session) flushSave() { func (s *Session) flushSave() {
a.saveMu.Lock() s.saveMu.Lock()
dirty := a.saveDirty dirty := s.saveDirty
a.saveDirty = false s.saveDirty = false
if a.saveTimer != nil { if s.saveTimer != nil {
a.saveTimer.Stop() s.saveTimer.Stop()
a.saveTimer = nil s.saveTimer = nil
} }
a.saveMu.Unlock() s.saveMu.Unlock()
if !dirty || a.id == "" || a.sessionsDir == "" { if !dirty || s.id == "" || s.sessionsDir == "" {
return return
} }
// Use PersistSession for consistent format (writes Name field correctly) // Use PersistSession for consistent format (writes Name field correctly)
if err := PersistSession(a.Name()); err != nil { if err := PersistSession(s.Name()); err != nil {
a.log.Error("session save: %v", err) s.log.Error("session save: %v", err)
} }
} }
// SaveSession writes the current session state. // Save writes the current session state via PersistSession.
// The path argument is ignored; session is saved via PersistSession. func (s *Session) Save() error {
func (a *Session) SaveSession(path string) error { return PersistSession(s.Name())
return PersistSession(a.Name())
} }
// --- Identity accessors --- // --- Identity accessors ---
// ID returns the immutable session UUID. // ID returns the immutable session UUID.
func (a *Session) ID() string { return a.id } func (s *Session) ID() string { return s.id }
// Name returns the mutable friendly name. // Name returns the mutable friendly name.
func (a *Session) Name() string { func (s *Session) Name() string {
a.mu.RLock() s.mu.RLock()
defer a.mu.RUnlock() defer s.mu.RUnlock()
return a.name return s.name
} }
// SetName sets the friendly name. // SetName sets the friendly name.
func (a *Session) SetName(name string) { func (s *Session) SetName(name string) {
a.mu.Lock() s.mu.Lock()
defer a.mu.Unlock() defer s.mu.Unlock()
a.name = name s.name = name
} }
// Uname returns the immutable user principal. // Uname returns the immutable user principal.
func (a *Session) Uname() string { return a.uname } func (s *Session) Uname() string { return s.uname }
// SetUname sets the user principal (should only be called during init). // SetUname sets the user principal (should only be called during init).
func (a *Session) SetUname(uname string) { func (s *Session) SetUname(uname string) {
a.mu.Lock() s.mu.Lock()
defer a.mu.Unlock() defer s.mu.Unlock()
a.uname = uname s.uname = uname
} }
// --- Context lifecycle --- // --- Context lifecycle ---
// Ctx returns the session context. // Ctx returns the session context.
func (a *Session) Ctx() context.Context { return a.ctx } func (s *Session) Ctx() context.Context { return s.ctx }
// Cancel cancels the session context. // Cancel cancels the session context.
func (a *Session) Cancel() { func (s *Session) Cancel() {
if a.cancel != nil { if s.cancel != nil {
a.cancel() s.cancel()
} }
} }
// SetContext sets the session context and cancel func. // SetContext sets the session context and cancel func.
func (a *Session) SetContext(ctx context.Context, cancel context.CancelFunc) { func (s *Session) SetContext(ctx context.Context, cancel context.CancelFunc) {
a.ctx = ctx s.ctx = ctx
a.cancel = cancel s.cancel = cancel
} }
// --- Tool server lifecycle --- // --- Tool server lifecycle ---
// Proc returns the tool server process. // Proc returns the tool server process.
func (a *Session) Proc() *toolsrv.Process { func (s *Session) Proc() *toolsrv.Process {
a.mu.RLock() s.mu.RLock()
defer a.mu.RUnlock() defer s.mu.RUnlock()
return a.proc return s.proc
} }
// SetProc sets the tool server process. // SetProc sets the tool server process.
func (a *Session) SetProc(proc *toolsrv.Process) { func (s *Session) SetProc(proc *toolsrv.Process) {
a.mu.Lock() s.mu.Lock()
defer a.mu.Unlock() defer s.mu.Unlock()
a.proc = proc s.proc = proc
} }
// Keeper returns the process keeper. // Keeper returns the process keeper.
func (a *Session) Keeper() *toolsrv.ProcessKeeper { func (s *Session) Keeper() *toolsrv.ProcessKeeper {
a.mu.RLock() s.mu.RLock()
defer a.mu.RUnlock() defer s.mu.RUnlock()
return a.keeper return s.keeper
} }
// SetKeeper sets the process keeper. // SetKeeper sets the process keeper.
func (a *Session) SetKeeper(keeper *toolsrv.ProcessKeeper) { func (s *Session) SetKeeper(keeper *toolsrv.ProcessKeeper) {
a.mu.Lock() s.mu.Lock()
defer a.mu.Unlock() defer s.mu.Unlock()
a.keeper = keeper s.keeper = keeper
} }
// ToolsConn returns the tool server connection. // ToolsConn returns the tool server connection.
func (a *Session) ToolsConn() toolsrv.Runner { func (s *Session) ToolsConn() toolsrv.Runner {
a.mu.RLock() s.mu.RLock()
defer a.mu.RUnlock() defer s.mu.RUnlock()
return a.toolsConn return s.toolsConn
} }
// SetToolsConn sets the tool server connection. // SetToolsConn sets the tool server connection.
func (a *Session) SetToolsConn(conn toolsrv.Runner) { func (s *Session) SetToolsConn(conn toolsrv.Runner) {
a.mu.Lock() s.mu.Lock()
defer a.mu.Unlock() defer s.mu.Unlock()
a.toolsConn = conn s.toolsConn = conn
} }
// IsPaused returns true if the session is paused. // IsPaused returns true if the session is paused.
func (a *Session) IsPaused() bool { func (s *Session) IsPaused() bool {
a.mu.RLock() s.mu.RLock()
defer a.mu.RUnlock() defer s.mu.RUnlock()
return a.paused return s.paused
} }
// SetPaused sets the paused state directly (used during restore). // SetPaused sets the paused state directly (used during restore).
func (a *Session) SetPaused(paused bool) { func (s *Session) SetPaused(paused bool) {
a.mu.Lock() s.mu.Lock()
defer a.mu.Unlock() defer s.mu.Unlock()
a.paused = paused s.paused = paused
} }
// IsConnected returns true if the tool server connection is alive. // IsConnected returns true if the tool server connection is alive.
// Returns false if the session is paused. // Returns false if the session is paused.
func (a *Session) IsConnected() bool { func (s *Session) IsConnected() bool {
a.mu.RLock() s.mu.RLock()
paused := a.paused paused := s.paused
conn := a.toolsConn conn := s.toolsConn
a.mu.RUnlock() s.mu.RUnlock()
if paused || conn == nil { if paused || conn == nil {
return false return false
} }
@ -473,61 +472,61 @@ func (a *Session) IsConnected() bool {
// Pause stops the tool server to save resources. // Pause stops the tool server to save resources.
// It cancels the session context (stopping all agent operations), // It cancels the session context (stopping all agent operations),
// kills the tool server process, and marks the session as paused. // kills the tool server process, and marks the session as paused.
func (a *Session) Pause() error { func (s *Session) Pause() error {
a.mu.Lock() s.mu.Lock()
defer a.mu.Unlock() defer s.mu.Unlock()
if a.paused { if s.paused {
return fmt.Errorf("session already paused") return fmt.Errorf("session already paused")
} }
// Cancel the session context to stop all agent operations. // Cancel the session context to stop all agent operations.
if a.cancel != nil { if s.cancel != nil {
a.cancel() s.cancel()
} }
// Close the tool server process. // Close the tool server process.
if a.keeper != nil { if s.keeper != nil {
a.keeper.Close() s.keeper.Close()
} else if a.proc != nil { } else if s.proc != nil {
a.proc.Close() s.proc.Close()
} }
// Close any existing connection. // Close any existing connection.
if a.toolsConn != nil { if s.toolsConn != nil {
if c, ok := a.toolsConn.(interface{ Close() }); ok { if c, ok := s.toolsConn.(interface{ Close() }); ok {
c.Close() c.Close()
} }
a.toolsConn = nil s.toolsConn = nil
} }
a.paused = true s.paused = true
// Persist the paused state. // Persist the paused state.
go a.saveSession() go s.saveSession()
return nil return nil
} }
// Resume restarts the tool server after a Pause. // Resume restarts the tool server after a Pause.
// It creates a fresh session context and respawns the tool server. // It creates a fresh session context and respawns the tool server.
// For sessions restored in paused state (no keeper), it sets up the tool server from scratch. // For sessions restored in paused state (no keeper), it sets up the tool server from scratch.
func (a *Session) Resume() error { func (s *Session) Resume() error {
a.mu.Lock() s.mu.Lock()
defer a.mu.Unlock() defer s.mu.Unlock()
if !a.paused { if !s.paused {
return fmt.Errorf("session not paused") return fmt.Errorf("session not paused")
} }
// Create a fresh context since the old one was cancelled on pause. // Create a fresh context since the old one was cancelled on pause.
ctx, cancel := context.WithCancel(serverCtx) ctx, cancel := context.WithCancel(serverCtx)
a.ctx = ctx s.ctx = ctx
a.cancel = cancel s.cancel = cancel
// If we have a keeper, use it to respawn. Otherwise set up from scratch. // If we have a keeper, use it to respawn. Otherwise set up from scratch.
if a.keeper != nil { if s.keeper != nil {
a.keeper.SetContext(ctx) s.keeper.SetContext(ctx)
conn, err := a.keeper.Dial() conn, err := s.keeper.Dial()
if err != nil { if err != nil {
return fmt.Errorf("resume failed: %w", err) return fmt.Errorf("resume failed: %w", err)
} }
a.toolsConn = conn s.toolsConn = conn
} else { } else {
// Session was restored paused without infra - set up per-agent. // Session was restored paused without infra - set up per-agent.
for i, ag := range a.agents { for i, ag := range s.agents {
cwd := ag.Cwd() cwd := ag.Cwd()
if cwd == "" { if cwd == "" {
cwd, _ = os.Getwd() cwd, _ = os.Getwd()
@ -535,18 +534,18 @@ func (a *Session) Resume() error {
// First agent spawns the tool server, others reuse // First agent spawns the tool server, others reuse
var reuseFrom *InfraConfig var reuseFrom *InfraConfig
if a.proc != nil { if s.proc != nil {
reuseFrom = &InfraConfig{ reuseFrom = &InfraConfig{
Proc: a.proc, Proc: s.proc,
Keeper: a.keeper, Keeper: s.keeper,
ToolsConn: a.toolsConn, ToolsConn: s.toolsConn,
} }
} }
infra, err := SetupToolServer(ToolServerConfig{ infra, err := SetupToolServer(ToolServerConfig{
Ctx: ctx, Ctx: ctx,
CWD: cwd, CWD: cwd,
RemoteTarget: a.remote, RemoteTarget: s.remote,
Yolo: pkgYolo, Yolo: pkgYolo,
ReuseFrom: reuseFrom, ReuseFrom: reuseFrom,
}) })
@ -556,45 +555,45 @@ func (a *Session) Resume() error {
// First agent sets session infra // First agent sets session infra
if i == 0 { if i == 0 {
a.proc = infra.Proc s.proc = infra.Proc
a.keeper = infra.Keeper s.keeper = infra.Keeper
a.toolsConn = infra.ToolsConn s.toolsConn = infra.ToolsConn
} }
ag.SetToolServer(infra.NewToolServer, infra.ToolsConn) ag.SetToolServer(infra.NewToolServer, infra.ToolsConn)
} }
} }
a.paused = false s.paused = false
// Persist the resumed state. // Persist the resumed state.
go a.saveSession() go s.saveSession()
return nil return nil
} }
// --- Tool loading --- // --- Tool loading ---
// SetDisallowTools sets the tool disallow list. // SetDisallowTools sets the tool disallow list.
func (a *Session) SetDisallowTools(disallow map[string]struct{}) { func (s *Session) SetDisallowTools(disallow map[string]struct{}) {
a.mu.Lock() s.mu.Lock()
defer a.mu.Unlock() defer s.mu.Unlock()
a.disallowTools = disallow s.disallowTools = disallow
} }
// LoadTool loads a tool into the tool server. // LoadTool loads a tool into the tool server.
func (a *Session) LoadTool(name string, ag *agent.Agent) error { func (s *Session) LoadTool(name string, ag *agent.Agent) error {
name = strings.TrimSpace(name) name = strings.TrimSpace(name)
if name == "" { if name == "" {
return nil return nil
} }
a.mu.RLock() s.mu.RLock()
_, blocked := a.disallowTools[name] _, blocked := s.disallowTools[name]
a.mu.RUnlock() s.mu.RUnlock()
if blocked { if blocked {
return fmt.Errorf("tool %q is disallowed for this session", name) return fmt.Errorf("tool %q is disallowed for this session", name)
} }
var runner toolsrv.Runner var runner toolsrv.Runner
if a.toolsConn != nil { if s.toolsConn != nil {
runner = a.toolsConn runner = s.toolsConn
} else if ag != nil { } else if ag != nil {
runner = ag.ToolServer() runner = ag.ToolServer()
} }

View File

@ -151,42 +151,6 @@ func (c *Conn) DismissDetached(pid int) bool {
return ok return ok
} }
// CanParallelize implements ParallelClassifier.
func (c *Conn) CanParallelize(name string) bool {
params, _ := json.Marshal(map[string]string{"name": name})
resp, err := c.call("can_parallelize", params)
if err != nil {
return false
}
var ok bool
json.Unmarshal(resp, &ok)
return ok
}
// MemoryTier implements MemoryTierClassifier.
func (c *Conn) MemoryTier(name string) string {
params, _ := json.Marshal(map[string]string{"name": name})
resp, err := c.call("memory_tier", params)
if err != nil {
return "hot"
}
var tier string
json.Unmarshal(resp, &tier)
return tier
}
// MemoryTierArgs implements MemoryTierClassifier.
func (c *Conn) MemoryTierArgs(name string, args json.RawMessage) string {
params, _ := json.Marshal(map[string]any{"name": name, "args": args})
resp, err := c.call("memory_tier_args", params)
if err != nil {
return "hot"
}
var tier string
json.Unmarshal(resp, &tier)
return tier
}
// Close shuts down the connection and subprocess. // Close shuts down the connection and subprocess.
func (c *Conn) Close() { func (c *Conn) Close() {
c.fail(fmt.Errorf("connection closed")) c.fail(fmt.Errorf("connection closed"))

View File

@ -130,40 +130,6 @@ func (ts *testServer) handleRPC(ctx context.Context, req rpcRequest) rpcResponse
ts.srv.SetAllowTools(params.Names) ts.srv.SetAllowTools(params.Names)
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(`true`)} return rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(`true`)}
case "can_parallelize":
var params struct {
Name string `json:"name"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}
}
ok := ts.srv.CanParallelize(params.Name)
result, _ := json.Marshal(ok)
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: result}
case "memory_tier":
var params struct {
Name string `json:"name"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}
}
tier := ts.srv.MemoryTier(params.Name)
result, _ := json.Marshal(tier)
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: result}
case "memory_tier_args":
var params struct {
Name string `json:"name"`
Args json.RawMessage `json:"args"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}
}
tier := ts.srv.MemoryTierArgs(params.Name, params.Args)
result, _ := json.Marshal(tier)
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: result}
case "detach": case "detach":
ok := ts.srv.Detach() ok := ts.srv.Detach()
result, _ := json.Marshal(ok) result, _ := json.Marshal(ok)
@ -449,78 +415,6 @@ func TestRPC_SetAllowTools(t *testing.T) {
} }
} }
func TestRPC_CanParallelize(t *testing.T) {
ts := newTestServer(t)
defer ts.srv.Close()
ctx := context.Background()
params, _ := json.Marshal(map[string]string{"name": "unknown_tool"})
req := rpcRequest{JSONRPC: "2.0", ID: 1, Method: "can_parallelize", Params: params}
resp := ts.handleRPC(ctx, req)
if resp.Error != nil {
t.Fatalf("unexpected error: %v", resp.Error.Message)
}
var result bool
if err := json.Unmarshal(resp.Result, &result); err != nil {
t.Fatalf("unmarshal result: %v", err)
}
// Unknown tools default to not parallel-read-safe
if result {
t.Error("expected false for unknown tool")
}
}
func TestRPC_MemoryTier(t *testing.T) {
ts := newTestServer(t)
defer ts.srv.Close()
ctx := context.Background()
params, _ := json.Marshal(map[string]string{"name": "unknown_tool"})
req := rpcRequest{JSONRPC: "2.0", ID: 1, Method: "memory_tier", Params: params}
resp := ts.handleRPC(ctx, req)
if resp.Error != nil {
t.Fatalf("unexpected error: %v", resp.Error.Message)
}
var tier string
if err := json.Unmarshal(resp.Result, &tier); err != nil {
t.Fatalf("unmarshal result: %v", err)
}
// Unknown tools default to "hot"
if tier != "hot" {
t.Errorf("expected tier='hot', got %q", tier)
}
}
func TestRPC_MemoryTierArgs(t *testing.T) {
ts := newTestServer(t)
defer ts.srv.Close()
ctx := context.Background()
params, _ := json.Marshal(map[string]any{
"name": "some_tool",
"args": json.RawMessage(`{"foo": "bar"}`),
})
req := rpcRequest{JSONRPC: "2.0", ID: 1, Method: "memory_tier_args", Params: params}
resp := ts.handleRPC(ctx, req)
if resp.Error != nil {
t.Fatalf("unexpected error: %v", resp.Error.Message)
}
var tier string
if err := json.Unmarshal(resp.Result, &tier); err != nil {
t.Fatalf("unmarshal result: %v", err)
}
// Unknown tools default to "hot"
if tier != "hot" {
t.Errorf("expected tier='hot', got %q", tier)
}
}
func TestRPC_Detach_NoRunningProcess(t *testing.T) { func TestRPC_Detach_NoRunningProcess(t *testing.T) {
ts := newTestServer(t) ts := newTestServer(t)
defer ts.srv.Close() defer ts.srv.Close()
@ -700,17 +594,6 @@ func TestRPC_PipeConnection(t *testing.T) {
// Test set cwd // Test set cwd
conn.SetCWD("/tmp/test") conn.SetCWD("/tmp/test")
// Test is_parallel_read
if conn.CanParallelize("unknown") {
t.Error("expected CanParallelize=false for unknown tool")
}
// Test result_tier
tier := conn.MemoryTier("unknown")
if tier != "hot" {
t.Errorf("expected tier='hot', got %q", tier)
}
// Clean up // Clean up
cancel() cancel()
conn.Close() conn.Close()

View File

@ -1,64 +0,0 @@
package toolsrv
import (
"encoding/json"
)
// MemoryTier implements MemoryTierClassifier. Looks up the tool's tier
// from the registry, then from its .meta file.
func (e *Server) MemoryTier(name string) string {
if e.toolRegistry != nil && e.sessionID != "" {
if info, ok := e.toolRegistry.Lookup(e.sessionID, name); ok && info.Tier != "" {
return info.Tier
}
}
m, err := LoadMetaFile(name)
if err == nil && m != nil && m.Tier != "" {
return m.Tier
}
return "hot"
}
// OutputFormat returns the source-fence language for a tool's output.
// Empty means the output should be wrapped in a plaintext fence.
func (e *Server) OutputFormat(name string) string {
if e.toolRegistry != nil && e.sessionID != "" {
if info, ok := e.toolRegistry.Lookup(e.sessionID, name); ok {
return info.OutputFormat
}
}
m, err := LoadMetaFile(name)
if err == nil && m != nil {
if resolved := m.Resolve(); resolved != nil {
return resolved.OutputFormat
}
}
return ""
}
// MemoryTierArgs classifies the tier using both the outer tool name and its
// arguments. For shell, it returns warm. For promoted tools, it
// delegates to MemoryTier.
func (e *Server) MemoryTierArgs(name string, args json.RawMessage) string {
switch name {
case "shell":
return "warm"
default:
return e.MemoryTier(name)
}
}
// CanParallelize implements ParallelClassifier. Returns true when the
// named tool is marked readOnly in its .meta file.
func (e *Server) CanParallelize(name string) bool {
if e.toolRegistry != nil && e.sessionID != "" {
if info, ok := e.toolRegistry.Lookup(e.sessionID, name); ok {
return info.ReadOnly
}
}
m, err := LoadMetaFile(name)
if err == nil && m != nil {
return m.ReadOnly
}
return false
}