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) == "" {