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 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-04-17 09:20:27 +02:00
parent f17774163c
commit ece8d22e75
2 changed files with 64 additions and 10 deletions

View File

@ -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())

View File

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