agent: delegate sessionID, cwd, env, uname to session.Session

Continue moving session concerns to the session package:
- sessionID → sess.ID()/SetID()
- cwd → sess.CWD()/SetCWD()
- env/envMu → sess.SetEnv()/Env()
- uname → sess.Uname()

Mutex responsibilities are cleanly split:
- session.Session.mu protects: state, reply, cwd, id
- session.Session.envMu protects: env map
- agent.mu protects: history, runtime (agent-specific state)
This commit is contained in:
Levi Neely 2026-07-29 18:31:18 +02:00
parent 64f3f18573
commit 6396167361
3 changed files with 55 additions and 68 deletions

View File

@ -175,9 +175,9 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
return
}
d := s.newDispatcher()
env := []string{"OLLIE_SESSION_ID=" + s.sessionID, "OLLIE_UNAME=" + s.uname}
env := []string{"OLLIE_SESSION_ID=" + s.sess.ID(), "OLLIE_UNAME=" + s.sess.Uname()}
env = append(env, s.promptEnvExtra...)
rt := BuildRuntime(cfg, d, s.cwd, env, s.baseLayers...)
rt := BuildRuntime(cfg, d, s.sess.CWD(), env, s.baseLayers...)
if rt.CfgBackend != "" {
newBe, err := s.newBackend(rt.CfgBackend)
if err != nil {
@ -226,8 +226,8 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
s.emit(infoEvent("nothing to compact"))
return
}
if s.sessionsDir != "" && s.sessionID != "" {
histPath := s.activeSessionPath(s.sessionID, ".compaction.jsonl")
if s.sessionsDir != "" && s.sess.ID() != "" {
histPath := s.activeSessionPath(s.sess.ID(), ".compaction.jsonl")
if err := os.MkdirAll(filepath.Dir(histPath), 0700); err != nil {
s.emit(infoEvent("compaction history save: " + err.Error()))
} else if data, err := json.Marshal(snapshot); err == nil {
@ -351,7 +351,7 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool {
continue
}
marker := " "
if file.id == s.sessionID {
if file.id == s.sess.ID() {
marker = "* "
}
goal := ""

View File

@ -318,14 +318,11 @@ type agent struct {
agentName string
agentsDir string
sessionsDir string
sessionID string
uname string
newDispatcher func() tools.Dispatcher
newBackend func(string) (backend.Backend, error)
readPlanStep func() string
listHandlers map[string]func() []string
turnError func(ctx context.Context, errType, errMsg string) HookResult // overridable for tests
cwd string
remote string // SSH target for remote execution
promptEnvExtra []string // PRIME_* vars for prompt resolution
baseLayers []string // system prompt, operational model, env block — for /agent reloads
@ -334,8 +331,6 @@ type agent struct {
toolCallCount atomic.Int64
pendingInject atomic.Pointer[string]
mu sync.RWMutex
envMu sync.RWMutex
env map[string]string // session-scoped env vars
changeMu sync.Mutex
changeCond *sync.Cond
submitMu sync.Mutex // serializes Submit calls (commands + turns)
@ -360,12 +355,7 @@ func (s *agent) ToolCallCount() int64 {
// SetEnv stores a session-scoped variable and propagates it to the execute server.
func (s *agent) SetEnv(key, value string) {
s.envMu.Lock()
if s.env == nil {
s.env = make(map[string]string)
}
s.env[key] = value
s.envMu.Unlock()
s.sess.SetEnv(key, value)
if s.runtime == nil || s.runtime.Dispatcher == nil {
return
}
@ -378,14 +368,14 @@ func (s *agent) SetEnv(key, value string) {
// pushSessionEnv injects OLLIE_SESSION_ID into the execute server subprocess env.
func (s *agent) pushSessionEnv() {
if s.runtime == nil || s.runtime.Dispatcher == nil || s.sessionID == "" {
if s.runtime == nil || s.runtime.Dispatcher == nil || s.sess.ID() == "" {
return
}
if srv, ok := s.runtime.Dispatcher.GetServer("execute"); ok {
if es, ok := srv.(tools.EnvSetter); ok {
es.SetEnv("OLLIE_SESSION_ID", s.sessionID)
if s.uname != "" {
es.SetEnv("OLLIE_UNAME", s.uname)
es.SetEnv("OLLIE_SESSION_ID", s.sess.ID())
if s.sess.Uname() != "" {
es.SetEnv("OLLIE_UNAME", s.sess.Uname())
}
}
}
@ -393,7 +383,7 @@ func (s *agent) pushSessionEnv() {
// pushLockDir sets the flock directory on the execute server to the session tmpdir.
func (s *agent) pushLockDir() {
if s.runtime == nil || s.runtime.Dispatcher == nil || s.sessionID == "" {
if s.runtime == nil || s.runtime.Dispatcher == nil || s.sess.ID() == "" {
return
}
@ -488,9 +478,6 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
agentName: cfg.AgentName,
agentsDir: cfg.AgentsDir,
sessionsDir: cfg.SessionsDir,
sessionID: cfg.SessionID,
uname: cfg.Uname,
cwd: paths.ExpandHome(cfg.CWD),
remote: cfg.Remote,
promptEnvExtra: cfg.PromptEnvExtra,
baseLayers: cfg.BaseLayers,
@ -505,7 +492,7 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
hookCtx, cancel := context.WithTimeout(context.Background(), time.Duration(hookTimeout)*time.Second)
defer cancel()
return a.runtime.Hooks.Run(hookCtx, HookTurnError, map[string]string{
"session_id": a.sessionID,
"session_id": a.sess.ID(),
"cwd": a.CWD(),
"model": a.runtime.Backend.Model(),
"error_type": errType,
@ -519,7 +506,7 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
// Close releases resources for this session, including its tmpdir.
func (s *agent) Close() {
s.log.Debug("Close() session=%q", s.sessionID)
s.log.Debug("Close() session=%q", s.sess.ID())
s.flushSave()
if s.runtime != nil && s.runtime.Dispatcher != nil {
if srv, ok := s.runtime.Dispatcher.GetServer("execute"); ok {
@ -529,8 +516,8 @@ func (s *agent) Close() {
}
}
}
if s.sessionID != "" {
os.RemoveAll(filepath.Join(ollieTmpDir(), s.sessionID)) //nolint:errcheck
if s.sess.ID() != "" {
os.RemoveAll(filepath.Join(ollieTmpDir(), s.sess.ID())) //nolint:errcheck
}
}
@ -790,9 +777,9 @@ func (s *agent) Reply() string {
// CWD returns the current working directory for tool execution.
func (s *agent) CWD() string {
if s.cwd != "" {
s.log.Debug("CWD() = %q", s.cwd)
return s.cwd
if s.sess.CWD() != "" {
s.log.Debug("CWD() = %q", s.sess.CWD())
return s.sess.CWD()
}
wd, _ := os.Getwd()
s.log.Debug("CWD() = %q (from getwd)", wd)
@ -809,8 +796,8 @@ func (s *agent) SetCWD(dir string) error {
return fmt.Errorf("cwd: %w", err)
}
}
oldCwd := s.cwd
s.cwd = dir
oldCwd := s.sess.CWD()
s.sess.SetCWD(dir)
// Update cwd references in the system prompt.
if oldCwd != "" && dir != "" && oldCwd != dir {
s.runtime.Preamble = strings.ReplaceAll(s.runtime.Preamble, oldCwd, dir)
@ -830,8 +817,8 @@ func (s *agent) SetCWD(dir string) error {
// SetSessionID renames the session. It updates the in-memory ID, renames
// persisted files on disk, and propagates to the execute server env.
func (s *agent) SetSessionID(newID string) error {
s.log.Debug("SetSessionID(%q) old=%q", newID, s.sessionID)
oldID := s.sessionID
s.log.Debug("SetSessionID(%q) old=%q", newID, s.sess.ID())
oldID := s.sess.ID()
if oldID == newID {
return nil
}
@ -846,7 +833,7 @@ func (s *agent) SetSessionID(newID string) error {
}
}
}
s.sessionID = newID
s.sess.SetID(newID)
// Update session ID references in the system prompt.
s.runtime.Preamble = strings.ReplaceAll(s.runtime.Preamble, oldID, newID)
// Rename tmpdir so isread markers remain valid after rename.
@ -888,7 +875,7 @@ func (s *agent) autoWarnLimit(ctx context.Context) int {
// agent-specific prompt with any agentSpawn hook output.
func (s *agent) spawnContext(ctx context.Context) string {
result := s.runtime.Hooks.Run(ctx, HookAgentSpawn, map[string]string{
"session_id": s.sessionID,
"session_id": s.sess.ID(),
"agent": s.agentName,
"cwd": s.CWD(),
"model": s.runtime.Backend.Model(),
@ -910,7 +897,7 @@ func (s *agent) spawnContext(ctx context.Context) string {
// re-injection, post-hook. Returns (n compacted, error). Returns (0, nil) if
// the pre-hook blocked or there was nothing to compact. Caller manages setState.
func (s *agent) runCompact(ctx context.Context, trigger string) (int, error) {
payload := map[string]string{"session_id": s.sessionID, "trigger": trigger, "cwd": s.CWD()}
payload := map[string]string{"session_id": s.sess.ID(), "trigger": trigger, "cwd": s.CWD()}
pre := s.runtime.Hooks.Run(ctx, HookPreCompact, payload, s.log)
if pre.Warning != "" {
s.emit(infoEvent(pre.Warning))
@ -937,7 +924,7 @@ func (s *agent) runCompact(ctx context.Context, trigger string) (int, error) {
return 0, err
}
if n > 0 {
s.auditLog.Debug("compact: removed %d messages trigger=%s session=%s", n, trigger, s.sessionID)
s.auditLog.Debug("compact: removed %d messages trigger=%s session=%s", n, trigger, s.sess.ID())
s.warnedContext = false
if sc := s.spawnContext(ctx); sc != "" {
s.history.appendUserMessage(sc)
@ -982,15 +969,15 @@ func (s *agent) flushSave() {
if !dirty {
return
}
if s.history == nil || s.sessionID == "" || s.sessionsDir == "" {
if s.history == nil || s.sess.ID() == "" || s.sessionsDir == "" {
return
}
path := s.activeSessionPath(s.sessionID, ".json")
path := s.activeSessionPath(s.sess.ID(), ".json")
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
s.log.Error("session save: %v", err)
return
}
if err := s.history.saveToFull(path, s.sessionID, s.agentName,
if err := s.history.saveToFull(path, s.sess.ID(), s.agentName,
s.runtime.Backend.Name(), s.runtime.Backend.Model(), s.CWD(), s.remote); err != nil {
s.log.Error("session save: %v", err)
}
@ -1004,7 +991,7 @@ func (s *agent) SaveSession(path string) error {
if s.history == nil {
return fmt.Errorf("no active session")
}
return s.history.saveToFull(path, s.sessionID, s.agentName,
return s.history.saveToFull(path, s.sess.ID(), s.agentName,
s.runtime.Backend.Name(), s.runtime.Backend.Model(), s.CWD(), s.remote)
}
@ -1240,7 +1227,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
s.emit(Event{Role: "user", Content: input})
hookResult := s.runtime.Hooks.Run(ctx, HookPreTurn, map[string]string{
"session_id": s.sessionID,
"session_id": s.sess.ID(),
"cwd": s.CWD(),
"prompt": input,
}, s.log)
@ -1286,7 +1273,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
s.currentAction.Store(handle)
s.setState("thinking")
s.auditLog.Debug("turn: start input=%s session=%s", auditTruncate(input), s.sessionID)
s.auditLog.Debug("turn: start input=%s session=%s", auditTruncate(input), s.sess.ID())
// Build per-turn agentConfig from the current runtime.
s.cfg = agentConfig{
@ -1348,7 +1335,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
}
s.cfg.PreTool = func(ctx context.Context, name string, args json.RawMessage) HookResult {
return s.runtime.Hooks.Run(ctx, HookPreTool, map[string]string{
"session_id": s.sessionID,
"session_id": s.sess.ID(),
"cwd": s.CWD(),
"tool": name,
"args": string(args),
@ -1356,7 +1343,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
}
s.cfg.PostTool = func(ctx context.Context, name string, args json.RawMessage, result string) HookResult {
return s.runtime.Hooks.Run(ctx, HookPostTool, map[string]string{
"session_id": s.sessionID,
"session_id": s.sess.ID(),
"cwd": s.CWD(),
"tool": name,
"args": string(args),
@ -1478,7 +1465,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
s.history.removeCancelledToolResults()
}
if errors.Is(err, context.Canceled) || errors.Is(err, ErrInterrupted) {
s.auditLog.Debug("turn: interrupted session=%s", s.sessionID)
s.auditLog.Debug("turn: interrupted session=%s", s.sess.ID())
s.saveSession()
return ""
}
@ -1491,7 +1478,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
}
stopResult := s.runtime.Hooks.Run(ctx, HookPostTurn, map[string]string{
"session_id": s.sessionID,
"session_id": s.sess.ID(),
"cwd": s.CWD(),
}, s.log)
if stopResult.Warning != "" {
@ -1506,12 +1493,12 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
if s.history != nil {
s.history.recordTurnCost(s.cfg.Backend.Model())
appendUsageLog(s.sessionID, s.cfg.Backend.Name(), s.cfg.Backend.Model(), s.history)
appendUsageLog(s.sess.ID(), s.cfg.Backend.Name(), s.cfg.Backend.Model(), s.history)
if s.history.LastTurnCostUSD > 0 {
s.emit(Event{Role: "info", Content: fmt.Sprintf("costLast=$%.4f\n", s.history.LastTurnCostUSD)})
}
s.auditLog.Debug("turn: end reply=%s cost=$%.4f session_total=$%.4f session=%s",
auditTruncate(s.sess.Reply()), s.history.LastTurnCostUSD, s.history.SessionCostUSD, s.sessionID)
auditTruncate(s.sess.Reply()), s.history.LastTurnCostUSD, s.history.SessionCostUSD, s.sess.ID())
s.notifyChange()
}
s.saveSession()

View File

@ -603,13 +603,13 @@ func TestCommand_Clear(t *testing.T) {
if c.history == nil {
t.Fatal("session nil after first turn")
}
oldID := c.sessionID
oldID := c.sess.ID()
collectEvents(context.Background(), c, "/clear")
if c.history != nil {
t.Error("session not nil after /clear")
}
if c.sessionID != oldID {
t.Errorf("sessionID changed after /clear: %q → %q; want unchanged", oldID, c.sessionID)
if c.sess.ID() != oldID {
t.Errorf("sessionID changed after /clear: %q → %q; want unchanged", oldID, c.sess.ID())
}
}
@ -1758,13 +1758,13 @@ func TestCore_Usage_WithSession(t *testing.T) {
func TestSetSessionID_Rename(t *testing.T) {
c := newCore(t, nil, nil)
collectEvents(context.Background(), c, "hello") // saves session file
oldID := c.sessionID
oldID := c.sess.ID()
newID := NewSessionID()
if err := c.SetSessionID(newID); err != nil {
t.Fatalf("SetSessionID: %v", err)
}
if c.sessionID != newID {
t.Errorf("sessionID = %q; want %q", c.sessionID, newID)
if c.sess.ID() != newID {
t.Errorf("sessionID = %q; want %q", c.sess.ID(), newID)
}
if _, err := os.Stat(filepath.Join(c.sessionsDir, "active", oldID+".json")); !os.IsNotExist(err) {
t.Errorf("old active session file still exists after rename; err=%v", err)
@ -1803,12 +1803,12 @@ func TestSetSessionID_UpdatesPreamble(t *testing.T) {
func TestSetSessionID_SameID(t *testing.T) {
c := newCore(t, nil, nil)
id := c.sessionID
id := c.sess.ID()
if err := c.SetSessionID(id); err != nil {
t.Fatalf("SetSessionID with same ID: %v", err)
}
if c.sessionID != id {
t.Errorf("sessionID changed: got %q; want %q", c.sessionID, id)
if c.sess.ID() != id {
t.Errorf("sessionID changed: got %q; want %q", c.sess.ID(), id)
}
}
@ -2312,13 +2312,12 @@ func TestSetEnv_StoredInCore(t *testing.T) {
c.SetEnv("FOO", "bar")
c.SetEnv("BAZ", "qux")
c.envMu.RLock()
defer c.envMu.RUnlock()
if c.env["FOO"] != "bar" {
t.Errorf("env[FOO] = %q; want bar", c.env["FOO"])
env := c.sess.Env()
if env["FOO"] != "bar" {
t.Errorf("env[FOO] = %q; want bar", env["FOO"])
}
if c.env["BAZ"] != "qux" {
t.Errorf("env[BAZ] = %q; want qux", c.env["BAZ"])
if env["BAZ"] != "qux" {
t.Errorf("env[BAZ] = %q; want qux", env["BAZ"])
}
}
@ -2327,8 +2326,9 @@ func TestSetEnv_NilDispatcher_NoPanic(t *testing.T) {
c.runtime.Dispatcher = nil
// Must not panic.
c.SetEnv("K", "V")
if c.env["K"] != "V" {
t.Errorf("env[K] = %q; want V", c.env["K"])
env := c.sess.Env()
if env["K"] != "V" {
t.Errorf("env[K] = %q; want V", env["K"])
}
}