diff --git a/session/manager.go b/session/manager.go index a09878f..5d35942 100644 --- a/session/manager.go +++ b/session/manager.go @@ -131,29 +131,57 @@ func (s *Manager) list() ([]os.DirEntry, error) { return entries, nil } -// Readdir lists entries in a subdirectory (e.g. "{id}", "{id}/peer"). +// Readdir lists entries in a subdirectory. func (s *Manager) Readdir(name string) ([]os.DirEntry, error) { - parts := strings.SplitN(name, "/", 3) + parts := strings.SplitN(name, "/", 4) sessID := parts[0] sess := s.Session(sessID) if sess == nil { return nil, fmt.Errorf("session not found: %s", sessID) } - // {id} — list session files + peer/ + // {id} — list session-level files + agent/ if len(parts) == 1 { - sfs, err := s.openStore(sess) - if err != nil { - return nil, err + entries := []os.DirEntry{ + fs.FileEntry("plan", 0666), + fs.FileEntry("env", 0444), + fs.DirEntry("agent", 0755), } - entries, err := sfs.List() - if err != nil { - return nil, err - } - // Append peer/ and proc/ directories - entries = append(entries, fs.DirEntry("peer", 0755)) - entries = append(entries, fs.DirEntry("proc", 0755)) return entries, nil } + // {id}/agent — list agent IDs + if parts[1] == "agent" { + if len(parts) == 2 { + // Currently single-agent: use the agent's ID + aid := sess.Core.Agent().ID() + if aid == "" { + aid = "0" // fallback for sessions without explicit agent ID + } + entries := []os.DirEntry{fs.DirEntry(aid, 0755)} + return entries, nil + } + // {id}/agent/{aid} — list agent files + proc/ + if len(parts) == 3 { + afs, err := s.openAgentStore(sess) + if err != nil { + return nil, err + } + entries, err := afs.List() + if err != nil { + return nil, err + } + entries = append(entries, fs.DirEntry("proc", 0755)) + return entries, nil + } + // {id}/agent/{aid}/proc — list detached process PIDs + if len(parts) == 4 && parts[3] == "proc" { + procs := sess.Core.ListDetached() + entries := make([]os.DirEntry, len(procs)) + for i, p := range procs { + entries[i] = fs.FileEntry(fmt.Sprintf("%d", p.PID), 0666) + } + return entries, nil + } + } // {id}/peer — list peer session IDs if len(parts) == 2 && parts[1] == "peer" { peers, _ := s.PeerList(sessID) @@ -163,15 +191,6 @@ func (s *Manager) Readdir(name string) ([]os.DirEntry, error) { } return entries, nil } - // {id}/proc — list detached process PIDs - if len(parts) == 2 && parts[1] == "proc" { - procs := sess.Core.ListDetached() - entries := make([]os.DirEntry, len(procs)) - for i, p := range procs { - entries[i] = fs.FileEntry(fmt.Sprintf("%d", p.PID), 0666) - } - return entries, nil - } return nil, fmt.Errorf("%s: not a directory", name) } @@ -275,7 +294,7 @@ func (s *Manager) stat(name string) (os.FileInfo, error) { if mode, ok := fs.Perms[fs.PathSessions].Files[name]; ok { return &fs.SyntheticFileInfo{Name_: name, Mode_: mode}, nil } - parts := strings.SplitN(name, "/", 3) + parts := strings.SplitN(name, "/", 4) sessID := parts[0] s.mu.RLock() sess, ok := s.sessions[sessID] @@ -287,6 +306,25 @@ func (s *Manager) stat(name string) (os.FileInfo, error) { if len(parts) == 1 { return &fs.SyntheticFileInfo{Name_: sessID, Mode_: fs.Perms[fs.PathSessionDir].DirMode, IsDir_: true}, nil } + // Agent directory: {id}/agent + if parts[1] == "agent" { + if len(parts) == 2 { + return &fs.SyntheticFileInfo{Name_: "agent", Mode_: 0755, IsDir_: true}, nil + } + // Agent instance: {id}/agent/{aid} + if len(parts) == 3 { + return &fs.SyntheticFileInfo{Name_: parts[2], Mode_: 0755, IsDir_: true}, nil + } + // Agent file: {id}/agent/{aid}/{file} + if parts[3] == "proc" { + return &fs.SyntheticFileInfo{Name_: "proc", Mode_: 0755, IsDir_: true}, nil + } + afs, err := s.openAgentStore(sess) + if err != nil { + return nil, err + } + return afs.Stat(parts[3]) + } // Peer directory: {id}/peer if parts[1] == "peer" { if len(parts) == 2 { @@ -302,15 +340,7 @@ func (s *Manager) stat(name string) (os.FileInfo, error) { } return &fs.SyntheticFileInfo{Name_: peerID, Mode_: 0666}, nil } - // Proc directory: {id}/proc - if parts[1] == "proc" { - if len(parts) == 2 { - return &fs.SyntheticFileInfo{Name_: "proc", Mode_: 0755, IsDir_: true}, nil - } - // Proc file: {id}/proc/{pid} — read-only output - return &fs.SyntheticFileInfo{Name_: parts[2], Mode_: 0444}, nil - } - // Session file: {id}/{file} + // Session file: {id}/{file} (plan, env) sfs, err := s.openStore(sess) if err != nil { return nil, err @@ -414,8 +444,8 @@ func (s *Manager) openEntry(name string) (fs.File, error) { }, nil } - // Hierarchical paths: {id}/{file} or {id}/t/{tool} - parts = strings.SplitN(name, "/", 3) + // Hierarchical paths: {id}/{file} or {id}/agent/{aid}/{file} + parts = strings.SplitN(name, "/", 4) sessID := parts[0] sess := s.Session(sessID) if sess == nil { @@ -424,6 +454,20 @@ func (s *Manager) openEntry(name string) (fs.File, error) { if len(parts) == 1 { return nil, fmt.Errorf("%s: is a directory", name) } + + // Agent file: {id}/agent/{aid}/{file} + if parts[1] == "agent" { + if len(parts) < 4 { + return nil, fmt.Errorf("%s: is a directory", name) + } + // parts[2] = aid, parts[3] = file + afs, err := s.openAgentStore(sess) + if err != nil { + return nil, err + } + return afs.Open(parts[3]) + } + // Session file: {id}/{file} sfs, err := s.openStore(sess) if err != nil { @@ -523,6 +567,22 @@ func (s *Manager) openStore(sess *Session) (*fs.Tree, error) { ), nil } +func (s *Manager) openAgentStore(sess *Session) (*fs.Tree, error) { + var resetElev func() + if s.cfg.ResetElevation != nil { + id := sess.id + resetElev = func() { s.cfg.ResetElevation(id) } + } + return NewAgentTree( + sess, + s.cfg.Log, + nil, // no transcript saving + s.cfg.InvalidateModels, + resetElev, + s.cfg.ToolRegistry, + ), nil +} + // InterruptAll interrupts every active session. func (s *Manager) InterruptAll() { s.mu.RLock()