From 90cde3a30e4ea2e2f07aeae74907f6618aa46909 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Tue, 14 Apr 2026 08:24:09 +0200 Subject: [PATCH] 9p: add ollie/m directory backed by OLLIE_MEMORY_PATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire /m as a read-write directory that maps to s.memDir (OLLIE_MEMORY_PATH or ~/.local/share/ollie/memory). Handles fileType, create, read, write, rename, remove, stat, and directory listing — mirrors /pl exactly. Inject OLLIE_MEMORY_PATH into the execute server env alongside OLLIE_PLAN_PATH. Co-Authored-By: Claude Sonnet 4.6 --- internal/p9/server.go | 78 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/internal/p9/server.go b/internal/p9/server.go index 77fdece..8e0c61e 100644 --- a/internal/p9/server.go +++ b/internal/p9/server.go @@ -6,6 +6,7 @@ // a/ (dir) agent configs (r/w, backed by ~/.config/ollie/agents/) // backends (read) list of ollie-provided backends // help (read) help file (backed by ~/.config/ollie/help.md) +// m/ (dir) memories (r/w, backed by OLLIE_MEMORY_PATH) // p/ (dir) prompt templates // pl/ (dir) plans // s/ (dir) session directory @@ -142,6 +143,7 @@ type Server struct { sessionsDir string promptsDir string planDir string // planning directory for /pl + memDir string // memory directory for /m } // New creates a new Server. @@ -152,6 +154,7 @@ func New() *Server { sessionsDir: agent.DefaultSessionsDir(), promptsDir: agent.DefaultPromptsDir(), planDir: defaultPlanDir(), + memDir: defaultMemDir(), } } @@ -164,6 +167,15 @@ func defaultPlanDir() string { return home + "/.config/ollie/planning" } +// defaultMemDir returns the memory directory from OLLIE_MEMORY_PATH or the default. +func defaultMemDir() string { + if p := os.Getenv("OLLIE_MEMORY_PATH"); p != "" { + return p + } + home, _ := os.UserHomeDir() + return home + "/.local/share/ollie/memory" +} + // Serve handles a single 9P connection. func (s *Server) Serve(conn net.Conn) { defer conn.Close() @@ -261,6 +273,8 @@ func (s *Server) pathType(path string) string { return "dir" case len(parts) == 1 && parts[0] == "p": return "dir" + case len(parts) == 1 && parts[0] == "m": + return "dir" case len(parts) == 1 && parts[0] == "pl": return "dir" case len(parts) == 1 && parts[0] == "sk": @@ -279,6 +293,10 @@ func (s *Server) pathType(path string) string { if _, err := os.Stat(s.promptsDir + "/" + parts[1]); err == nil { return "file" } + case len(parts) == 2 && parts[0] == "m": + if _, err := os.Stat(s.memDir + "/" + parts[1]); err == nil { + return "file" + } case len(parts) == 2 && parts[0] == "pl": if _, err := os.Stat(s.planDir + "/" + parts[1]); err == nil { return "file" @@ -444,6 +462,11 @@ func (s *Server) create(cs *connState, fc *plan9.Fcall) *plan9.Fcall { if err := os.WriteFile(s.promptsDir+"/"+fc.Name, nil, 0644); err != nil { return errFcall(fc, err.Error()) } + case "/m": + os.MkdirAll(s.memDir, 0755) //nolint:errcheck + if err := os.WriteFile(s.memDir+"/"+fc.Name, nil, 0644); err != nil { + return errFcall(fc, err.Error()) + } case "/pl": os.MkdirAll(s.planDir, 0755) //nolint:errcheck if err := os.WriteFile(s.planDir+"/"+fc.Name, nil, 0644); err != nil { @@ -538,6 +561,15 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall { return s.readSlice(fc, content) } + // Memory files are served from disk. + if strings.HasPrefix(path, "/m/") { + content, err := os.ReadFile(s.memDir + "/" + pathBase(path)) + if err != nil { + return errFcall(fc, err.Error()) + } + return s.readSlice(fc, content) + } + // Plan files are served from disk. if strings.HasPrefix(path, "/pl/") { content, err := os.ReadFile(s.planDir + "/" + pathBase(path)) @@ -791,6 +823,17 @@ func (s *Server) wstat(cs *connState, fc *plan9.Fcall) *plan9.Fcall { f.qid.Path = qidPath(f.path) cs.mu.Unlock() + case strings.HasPrefix(f.path, "/m/"): + oldPath := s.memDir + "/" + oldName + newPath := s.memDir + "/" + newDir.Name + if err := os.Rename(oldPath, newPath); err != nil { + return errFcall(fc, err.Error()) + } + cs.mu.Lock() + f.path = "/m/" + newDir.Name + f.qid.Path = qidPath(f.path) + cs.mu.Unlock() + case strings.HasPrefix(f.path, "/pl/"): oldPath := s.planDir + "/" + oldName newPath := s.planDir + "/" + newDir.Name @@ -920,6 +963,8 @@ func (s *Server) remove(cs *connState, fc *plan9.Fcall) *plan9.Fcall { err = os.Remove(s.agentsDir + "/" + pathBase(path)) case strings.HasPrefix(path, "/p/"): err = os.Remove(s.promptsDir + "/" + pathBase(path)) + case strings.HasPrefix(path, "/m/"): + err = os.Remove(s.memDir + "/" + pathBase(path)) case strings.HasPrefix(path, "/pl/"): err = os.Remove(s.planDir + "/" + pathBase(path)) case strings.HasPrefix(path, "/sk/"): @@ -965,6 +1010,12 @@ func (s *Server) handleWrite(path, input string) error { return os.WriteFile(s.agentsDir+"/"+pathBase(path), []byte(input), 0644) } + // Memory file writes go directly to disk. + if strings.HasPrefix(path, "/m/") { + os.MkdirAll(s.memDir, 0755) //nolint:errcheck + return os.WriteFile(s.memDir+"/"+pathBase(path), []byte(input), 0644) + } + // Plan file writes go directly to disk. if strings.HasPrefix(path, "/pl/") { os.MkdirAll(s.planDir, 0755) //nolint:errcheck @@ -1211,6 +1262,7 @@ func (s *Server) createSession(args []string) error { if srv, ok := env.Dispatcher().GetServer("execute"); ok { if es, ok := srv.(tools.EnvSetter); ok { es.SetEnv("OLLIE_SESSION_ID", sessID) + es.SetEnv("OLLIE_MEMORY_PATH", s.memDir) es.SetEnv("OLLIE_PLAN_PATH", s.planDir) } } @@ -1316,6 +1368,7 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte { dirs = append(dirs, makeDir("a", "/a", true, plan9.DMDIR|0755)) dirs = append(dirs, makeDir("backends", "/backends", false, 0444)) dirs = append(dirs, makeDir("help", "/help", false, 0444)) + dirs = append(dirs, makeDir("m", "/m", true, plan9.DMDIR|0755)) dirs = append(dirs, makeDir("p", "/p", true, plan9.DMDIR|0755)) dirs = append(dirs, makeDir("pl", "/pl", true, plan9.DMDIR|0755)) dirs = append(dirs, makeDir("s", "/s", true, plan9.DMDIR|0555)) @@ -1335,6 +1388,18 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte { dirs = append(dirs, makeDir(e.Name(), "/p/"+e.Name(), false, 0666)) } } + } else if path == "/m" { + entries, _ := os.ReadDir(s.memDir) + for _, e := range entries { + if !e.IsDir() { + d := makeDir(e.Name(), "/m/"+e.Name(), false, 0666) + 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 == "/pl" { entries, _ := os.ReadDir(s.planDir) for _, e := range entries { @@ -1444,7 +1509,7 @@ func (s *Server) makeStat(path string) plan9.Dir { qid.Type = QTDir if path == "/t" { mode = plan9.DMDIR | 0777 - } else if path == "/a" || path == "/p" || path == "/pl" { + } else if path == "/a" || path == "/m" || path == "/p" || path == "/pl" { mode = plan9.DMDIR | 0755 } else { mode = plan9.DMDIR | 0555 @@ -1466,6 +1531,8 @@ func (s *Server) makeStat(path string) plan9.Dir { mode = 0666 } else if strings.HasPrefix(path, "/p/") { mode = 0666 + } else if strings.HasPrefix(path, "/m/") { + mode = 0666 } else if strings.HasPrefix(path, "/pl/") { mode = 0666 } else if strings.HasPrefix(path, "/sk/") { @@ -1527,7 +1594,14 @@ func (s *Server) makeStat(path string) plan9.Dir { } } - // For plan files, report real size and timestamps from disk. + // For memory and plan files, report real size and timestamps from disk. + if strings.HasPrefix(path, "/m/") { + if info, err := os.Stat(s.memDir + "/" + base); err == nil { + dir.Length = uint64(info.Size()) + dir.Atime = uint32(info.ModTime().Unix()) + dir.Mtime = uint32(info.ModTime().Unix()) + } + } if strings.HasPrefix(path, "/pl/") { if info, err := os.Stat(s.planDir + "/" + base); err == nil { dir.Length = uint64(info.Size())