From ece8d22e753cf7e54a1d383d70aee7a3be5e88a1 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 17 Apr 2026 09:20:27 +0200 Subject: [PATCH 1/5] 9p: add ollie/tmp backed by OLLIE_TMP_PATH flatdirstore Replaces /tmp/ollie per-session scratch space with a 9P-served tmp/ directory backed by OLLIE_TMP_PATH (~/.local/share/ollie/tmp by default). Markers are now global, hash-addressed by absolute file path, and shared across all sessions. OLLIE_TMP_PATH is always set by ensureEnv(). Co-Authored-By: Claude Sonnet 4.6 --- internal/p9/server.go | 73 +++++++++++++++++++++++++++++++++++++------ main.go | 1 + 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/internal/p9/server.go b/internal/p9/server.go index 856f6b2..aebbe34 100644 --- a/internal/p9/server.go +++ b/internal/p9/server.go @@ -29,6 +29,7 @@ // ctxsz (read) context size vs window // sk/ (dir) skills // t/ (dir) tool scripts +// tmp/ (dir) file-edit markers (r/w, backed by OLLIE_TMP_PATH) // tr/ (dir) transcripts (ro; write to s/{id}/chat to save) package p9 @@ -147,6 +148,7 @@ type Server struct { sessionStore Store batchStore *BatchStore transcriptStore Store + tmpStore Store } // New creates a new Server. @@ -155,10 +157,12 @@ func New() *Server { os.MkdirAll(memDir, 0755) //nolint:errcheck transcriptDir := defaultTranscriptDir() os.MkdirAll(transcriptDir, 0755) //nolint:errcheck + tmpDir := defaultTmpDir() + os.MkdirAll(tmpDir, 0755) //nolint:errcheck agentsDir := agent.DefaultAgentsDir() s := &Server{ - sessions: make(map[string]*session), - agentsDir: agentsDir, + sessions: make(map[string]*session), + agentsDir: agentsDir, sessionsDir: agent.DefaultSessionsDir(), agentStore: NewFlatDirStore(agentsDir, 0644), promptStore: NewFlatDirStore(agent.DefaultPromptsDir(), 0444), @@ -167,6 +171,7 @@ func New() *Server { toolStore: NewToolStore(), skillStore: NewSkillStore(), transcriptStore: NewFlatDirStore(transcriptDir, 0444), + tmpStore: NewFlatDirStore(tmpDir, 0600), } s.sessionStore = &SessionStore{srv: s} s.batchStore = NewBatchStore(s) @@ -200,6 +205,15 @@ func defaultMemDir() string { return home + "/.config/ollie/memory" } +// defaultTmpDir returns the tmp directory from OLLIE_TMP_PATH or the default. +func defaultTmpDir() string { + if p := os.Getenv("OLLIE_TMP_PATH"); p != "" { + return p + } + home, _ := os.UserHomeDir() + return home + "/.local/share/ollie/tmp" +} + // sessionFileStore returns a SessionFileStore for the given session ID. func (s *Server) sessionFileStore(sessID string) (*SessionFileStore, bool) { s.mu.RLock() @@ -356,6 +370,12 @@ func (s *Server) pathType(path string) string { return "file" } } + case len(parts) == 1 && parts[0] == "tmp": + return "dir" + case len(parts) == 2 && parts[0] == "tmp": + if _, err := s.tmpStore.Stat(parts[1]); err == nil { + return "file" + } case len(parts) == 1 && parts[0] == "backends": return "file" case len(parts) == 1 && parts[0] == "help": @@ -529,6 +549,10 @@ func (s *Server) create(cs *connState, fc *plan9.Fcall) *plan9.Fcall { if err := s.toolStore.Create(fc.Name); err != nil { return errFcall(fc, err.Error()) } + case "/tmp": + if err := s.tmpStore.Create(fc.Name); err != nil { + return errFcall(fc, err.Error()) + } case "/sk": if err := s.skillStore.Create(fc.Name); err != nil { return errFcall(fc, err.Error()) @@ -674,6 +698,16 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall { return s.readSlice(fc, content) } + // Tmp files are served from the tmp store. + if strings.HasPrefix(path, "/tmp/") { + plog.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count) + content, err := s.tmpStore.Get(pathBase(path)) + if err != nil { + return errFcall(fc, err.Error()) + } + return s.readSlice(fc, content) + } + // Transcript files are served from the transcript store. if strings.HasPrefix(path, "/tr/") { plog.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count) @@ -892,13 +926,6 @@ func (s *Server) renameSession(oldID, newID string) error { c.mu.Unlock() } - // Rename the session tmpdir so isread markers remain valid. - oldTemp := "/tmp/ollie/" + oldID - newTemp := "/tmp/ollie/" + newID - if _, err := os.Stat(oldTemp); err == nil { - os.Rename(oldTemp, newTemp) //nolint:errcheck - } - sess.appendChat([]byte(fmt.Sprintf("(session renamed: %s -> %s)\n", oldID, newID))) plog.Info("renamed session %s -> %s", oldID, newID) return nil @@ -971,6 +998,8 @@ func (s *Server) remove(cs *connState, fc *plan9.Fcall) *plan9.Fcall { err = s.skillStore.Delete(pathBase(path)) case strings.HasPrefix(path, "/t/"): err = s.toolStore.Delete(pathBase(path)) + case strings.HasPrefix(path, "/tmp/"): + err = s.tmpStore.Delete(pathBase(path)) case strings.HasPrefix(path, "/tr/"): err = s.transcriptStore.Delete(pathBase(path)) case strings.HasPrefix(path, "/b/") && path != "/b/new": @@ -1043,6 +1072,11 @@ func (s *Server) handleWrite(path, input string) error { return s.toolStore.Put(pathBase(path), []byte(input)) } + // Tmp file writes go to the tmp store. + if strings.HasPrefix(path, "/tmp/") { + return s.tmpStore.Put(pathBase(path), []byte(input)) + } + // Session file writes: /s/{sessid}/{file} if strings.HasPrefix(path, "/s/") { parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3) @@ -1280,6 +1314,7 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte { dirs = append(dirs, makeDir("s", "/s", true, plan9.DMDIR|0555)) dirs = append(dirs, makeDir("sk", "/sk", true, plan9.DMDIR|0555)) dirs = append(dirs, makeDir("t", "/t", true, plan9.DMDIR|0777)) + dirs = append(dirs, makeDir("tmp", "/tmp", true, plan9.DMDIR|0755)) dirs = append(dirs, makeDir("tr", "/tr", true, plan9.DMDIR|0555)) } else if path == "/a" { entries, _ := s.agentStore.List() @@ -1337,6 +1372,18 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte { } dirs = append(dirs, makeDir(e.Name(), "/t/"+e.Name(), false, mode)) } + } else if path == "/tmp" { + entries, _ := s.tmpStore.List() + for _, e := range entries { + if !e.IsDir() { + d := makeDir(e.Name(), "/tmp/"+e.Name(), false, 0600) + if info, err := e.Info(); err == nil { + d.Atime = uint32(info.ModTime().Unix()) + d.Mtime = uint32(info.ModTime().Unix()) + } + dirs = append(dirs, d) + } + } } else if path == "/tr" { entries, _ := s.transcriptStore.List() for _, e := range entries { @@ -1440,7 +1487,7 @@ func (s *Server) makeStat(path string) plan9.Dir { qid.Type = QTDir if path == "/t" { mode = plan9.DMDIR | 0777 - } else if path == "/a" || path == "/m" || path == "/pl" { + } else if path == "/a" || path == "/m" || path == "/pl" || path == "/tmp" { mode = plan9.DMDIR | 0755 } else { mode = plan9.DMDIR | 0555 @@ -1571,6 +1618,12 @@ func (s *Server) makeStat(path string) plan9.Dir { if content, err := s.toolStore.Get(base); err == nil { dir.Length = uint64(len(content)) } + case strings.HasPrefix(path, "/tmp/"): + if info, err := s.tmpStore.Stat(base); err == nil { + dir.Length = uint64(info.Size()) + dir.Atime = uint32(info.ModTime().Unix()) + dir.Mtime = uint32(info.ModTime().Unix()) + } case strings.HasPrefix(path, "/tr/"): if info, err := s.transcriptStore.Stat(base); err == nil { dir.Length = uint64(info.Size()) diff --git a/main.go b/main.go index 2cf4dec..1a4febd 100644 --- a/main.go +++ b/main.go @@ -148,6 +148,7 @@ func ensureEnv() { "OLLIE_SKILLS_PATH": filepath.Join(home, ".config", "ollie", "skills"), "OLLIE_PLAN_PATH": filepath.Join(home, ".config", "ollie", "planning"), "OLLIE_MEMORY_PATH": filepath.Join(home, ".config", "ollie", "memory"), + "OLLIE_TMP_PATH": filepath.Join(home, ".local", "share", "ollie", "tmp"), } for k, v := range defaults { if os.Getenv(k) == "" { From 9cf6ba8d51e5858ceadd727eeb8b4cf358a0cc30 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 17 Apr 2026 09:29:00 +0200 Subject: [PATCH 2/5] 9p: update ollie/tmp doc comment Co-Authored-By: Claude Sonnet 4.6 --- internal/p9/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/p9/server.go b/internal/p9/server.go index aebbe34..39e6aa2 100644 --- a/internal/p9/server.go +++ b/internal/p9/server.go @@ -29,7 +29,7 @@ // ctxsz (read) context size vs window // sk/ (dir) skills // t/ (dir) tool scripts -// tmp/ (dir) file-edit markers (r/w, backed by OLLIE_TMP_PATH) +// tmp/ (dir) tool dumping ground (r/w, backed by OLLIE_TMP_PATH) // tr/ (dir) transcripts (ro; write to s/{id}/chat to save) package p9 From 231e9ebcc58e16499755b247d0c6c3eb19f7caca Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 17 Apr 2026 13:05:23 +0200 Subject: [PATCH 3/5] s/ls, ollie-tmp-path --- internal/p9/server.go | 8 +++++--- internal/p9/sessionstore.go | 5 +++++ main.go | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/p9/server.go b/internal/p9/server.go index 39e6aa2..971e0fe 100644 --- a/internal/p9/server.go +++ b/internal/p9/server.go @@ -609,8 +609,8 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall { } } - // s/new and s/idx are served from the session store. - if path == "/s/new" || path == "/s/idx" { + // s/new, s/idx, and s/ls are served from the session store. + if path == "/s/new" || path == "/s/idx" || path == "/s/ls" { plog.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count) content, err := s.sessionStore.Get(pathBase(path)) if err != nil { @@ -1503,6 +1503,8 @@ func (s *Server) makeStat(path string) plan9.Dir { default: if path == "/backends" || path == "/help" || path == "/s/idx" { mode = 0444 + } else if path == "/s/ls" { + mode = 0555 } else if path == "/s/new" { mode = 0666 } else if strings.HasPrefix(path, "/a/") { @@ -1592,7 +1594,7 @@ func (s *Server) makeStat(path string) plan9.Dir { // that check stat before reading (cat, 9pfuse, etc.) see non-zero size. if dir.Length == 0 && !isDir { switch { - case path == "/s/new" || path == "/s/idx": + case path == "/s/new" || path == "/s/idx" || path == "/s/ls": if content, err := s.sessionStore.Get(base); err == nil { dir.Length = uint64(len(content)) } diff --git a/internal/p9/sessionstore.go b/internal/p9/sessionstore.go index f34c400..f7425be 100644 --- a/internal/p9/sessionstore.go +++ b/internal/p9/sessionstore.go @@ -17,6 +17,7 @@ func (s *SessionStore) List() ([]os.DirEntry, error) { entries := []os.DirEntry{ syntheticEntry("new", 0666), syntheticEntry("idx", 0444), + syntheticEntry("ls", 0555), } s.srv.mu.RLock() for id := range s.srv.sessions { @@ -32,6 +33,8 @@ func (s *SessionStore) Stat(name string) (os.FileInfo, error) { return &syntheticFileInfo{name: "new", mode: 0666}, nil case "idx": return &syntheticFileInfo{name: "idx", mode: 0444}, nil + case "ls": + return &syntheticFileInfo{name: "ls", mode: 0555}, nil } s.srv.mu.RLock() _, ok := s.srv.sessions[name] @@ -48,6 +51,8 @@ func (s *SessionStore) Get(name string) ([]byte, error) { return []byte("name=\ncwd=\nbackend=\nmodel=\nagent=\n"), nil case "idx": return s.index(), nil + case "ls": + return []byte("#!/usr/bin/env bash\ncat ${OLLIE:-$HOME/mnt/ollie}/s/idx\n"), nil } return nil, fmt.Errorf("%s: not a readable file", name) } diff --git a/main.go b/main.go index 1a4febd..b21f364 100644 --- a/main.go +++ b/main.go @@ -148,7 +148,8 @@ func ensureEnv() { "OLLIE_SKILLS_PATH": filepath.Join(home, ".config", "ollie", "skills"), "OLLIE_PLAN_PATH": filepath.Join(home, ".config", "ollie", "planning"), "OLLIE_MEMORY_PATH": filepath.Join(home, ".config", "ollie", "memory"), - "OLLIE_TMP_PATH": filepath.Join(home, ".local", "share", "ollie", "tmp"), + "OLLIE_TMP_PATH": filepath.Join(home, ".local", "share", "ollie", "tmp"), + "OLLIE_TRANSCRIPT_PATH": filepath.Join(home, ".config", "ollie", "transcript"), } for k, v := range defaults { if os.Getenv(k) == "" { From 5286551f4c6a1e4f383995955235a917c65048ad Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 17 Apr 2026 15:31:19 +0200 Subject: [PATCH 4/5] use paths.CfgDir/DataDir for config and data directories Replace hard-coded ~/.config/ollie and ~/.local/share/ollie paths with paths.CfgDir() and paths.DataDir() to support OLLIE_CFG_PATH and OLLIE_DATA_PATH env vars. --- internal/p9/server.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/internal/p9/server.go b/internal/p9/server.go index 971e0fe..dfcc8ba 100644 --- a/internal/p9/server.go +++ b/internal/p9/server.go @@ -50,6 +50,7 @@ import ( "ollie/pkg/backend" "ollie/pkg/config" olog "ollie/pkg/log" + "ollie/pkg/paths" "ollie/pkg/tools" "ollie/pkg/tools/execute" ) @@ -183,8 +184,7 @@ func defaultPlanDir() string { if p := os.Getenv("OLLIE_PLAN_PATH"); p != "" { return p } - home, _ := os.UserHomeDir() - return home + "/.config/ollie/planning" + return paths.CfgDir() + "/planning" } // defaultTranscriptDir returns the transcript directory from OLLIE_TRANSCRIPT_PATH or the default. @@ -192,8 +192,7 @@ func defaultTranscriptDir() string { if p := os.Getenv("OLLIE_TRANSCRIPT_PATH"); p != "" { return p } - home, _ := os.UserHomeDir() - return home + "/.config/ollie/transcript" + return paths.CfgDir() + "/transcript" } // defaultMemDir returns the memory directory from OLLIE_MEMORY_PATH or the default. @@ -201,8 +200,7 @@ func defaultMemDir() string { if p := os.Getenv("OLLIE_MEMORY_PATH"); p != "" { return p } - home, _ := os.UserHomeDir() - return home + "/.config/ollie/memory" + return paths.CfgDir() + "/memory" } // defaultTmpDir returns the tmp directory from OLLIE_TMP_PATH or the default. @@ -210,8 +208,7 @@ func defaultTmpDir() string { if p := os.Getenv("OLLIE_TMP_PATH"); p != "" { return p } - home, _ := os.UserHomeDir() - return home + "/.local/share/ollie/tmp" + return paths.DataDir() + "/tmp" } // sessionFileStore returns a SessionFileStore for the given session ID. @@ -761,8 +758,7 @@ func (s *Server) readSlice(fc *plan9.Fcall, content []byte) *plan9.Fcall { } func (s *Server) helpPath() string { - home, _ := os.UserHomeDir() - return home + "/.config/ollie/help.md" + return paths.CfgDir() + "/help.md" } From 3e7c05a6e13e844b6ef94ff5bf6180057bcb5123 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Sat, 18 Apr 2026 09:49:48 +0200 Subject: [PATCH 5/5] 9p: add s/{id}/params endpoint Expose generation parameters (maxTokens, temperature, frequencyPenalty, presencePenalty) as a readable/writable 9P file. Read returns all four keys as key=value lines; empty value means unset (API default). Write parses the same format, updating only the keys present. Co-Authored-By: Claude Sonnet 4.6 --- internal/p9/sessionfilestore.go | 91 +++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/internal/p9/sessionfilestore.go b/internal/p9/sessionfilestore.go index 8d96207..df86dfd 100644 --- a/internal/p9/sessionfilestore.go +++ b/internal/p9/sessionfilestore.go @@ -3,9 +3,11 @@ package p9 import ( "fmt" "os" + "strconv" "strings" "ollie/pkg/agent" + "ollie/pkg/backend" ) // sessionFileList defines the fixed set of files in a session directory, @@ -30,6 +32,7 @@ var sessionFileList = []struct { {"models", 0444}, {"mcp", 0444}, {"systemprompt", 0444}, + {"params", 0666}, } // SessionFileStore implements ReadWriteStore for the files within a single @@ -152,10 +155,96 @@ func (s *SessionFileStore) Put(name string, data []byte) error { s.sess.mu.Lock() s.sess.mutableVers["cwd"].update(s.sess.core.CWD()) s.sess.mu.Unlock() + + case "params": + if s.sess.core.IsRunning() { + return fmt.Errorf("cannot change params while agent is running") + } + params, err := parseParams(input, s.sess.core.GenerationParams()) + if err != nil { + return err + } + return s.sess.core.SetGenerationParams(params) } return nil } +func formatParams(p backend.GenerationParams) string { + var sb strings.Builder + fmt.Fprintf(&sb, "maxTokens=%d\n", p.MaxTokens) + if p.Temperature != nil { + fmt.Fprintf(&sb, "temperature=%g\n", *p.Temperature) + } else { + fmt.Fprintf(&sb, "temperature=\n") + } + if p.FrequencyPenalty != nil { + fmt.Fprintf(&sb, "frequencyPenalty=%g\n", *p.FrequencyPenalty) + } else { + fmt.Fprintf(&sb, "frequencyPenalty=\n") + } + if p.PresencePenalty != nil { + fmt.Fprintf(&sb, "presencePenalty=%g\n", *p.PresencePenalty) + } else { + fmt.Fprintf(&sb, "presencePenalty=\n") + } + return sb.String() +} + +func parseParams(input string, current backend.GenerationParams) (backend.GenerationParams, error) { + p := current + for _, line := range strings.Split(input, "\n") { + k, v, ok := strings.Cut(line, "=") + if !ok { + continue + } + k = strings.TrimSpace(k) + v = strings.TrimSpace(v) + switch k { + case "maxTokens": + if v == "" { + p.MaxTokens = 0 + } else { + n, err := strconv.Atoi(v) + if err != nil { + return p, fmt.Errorf("invalid maxTokens: %s", v) + } + p.MaxTokens = n + } + case "temperature": + if v == "" { + p.Temperature = nil + } else { + f, err := strconv.ParseFloat(v, 64) + if err != nil { + return p, fmt.Errorf("invalid temperature: %s", v) + } + p.Temperature = &f + } + case "frequencyPenalty": + if v == "" { + p.FrequencyPenalty = nil + } else { + f, err := strconv.ParseFloat(v, 64) + if err != nil { + return p, fmt.Errorf("invalid frequencyPenalty: %s", v) + } + p.FrequencyPenalty = &f + } + case "presencePenalty": + if v == "" { + p.PresencePenalty = nil + } else { + f, err := strconv.ParseFloat(v, 64) + if err != nil { + return p, fmt.Errorf("invalid presencePenalty: %s", v) + } + p.PresencePenalty = &f + } + } + } + return p, nil +} + // content returns the string content of a simple readable session file. func (s *SessionFileStore) content(name string) string { s.sess.mu.RLock() @@ -183,6 +272,8 @@ func (s *SessionFileStore) content(name string) string { return s.sess.core.SystemPrompt() case "offset": return fmt.Sprintf("%d\n", s.sess.chatOffset) + case "params": + return formatParams(s.sess.core.GenerationParams()) } return "" }