session: split immutable ID from mutable Name; add id/name files to 9P

This commit is contained in:
Ollie Agent 2026-08-01 17:31:16 +02:00
parent 7403e1f5bb
commit 89f0ea4179
7 changed files with 95 additions and 80 deletions

View File

@ -113,9 +113,14 @@ func agentPath(sid string) string {
// --- session list window --- // --- session list window ---
type sessionEntry struct {
id string // full UUID
name string // display name
}
type sessionList struct { type sessionList struct {
win *acme.Win win *acme.Win
allSIDs []string entries []sessionEntry
mu sync.Mutex mu sync.Mutex
} }
@ -134,42 +139,43 @@ func newSessionList() error {
} }
func (sl *sessionList) refresh() { func (sl *sessionList) refresh() {
data, err := read9p("session/idx") fsys := dial9p()
fid, err := fsys.Open("session", plan9.OREAD)
if err != nil { if err != nil {
sl.win.Errf("read idx: %v", err) sl.win.Errf("list sessions: %v", err)
return return
} }
dirs, err := fid.Dirreadall()
fid.Close()
if err != nil {
sl.win.Errf("list sessions: %v", err)
return
}
sl.win.Addr(",") sl.win.Addr(",")
sl.win.Write("data", nil) sl.win.Write("data", nil)
raw := strings.TrimSpace(string(data))
if raw == "" {
sl.win.Ctl("clean")
return
}
lines := strings.Split(raw, "\n")
sort.Strings(lines)
sl.mu.Lock() sl.mu.Lock()
sl.allSIDs = nil sl.entries = nil
var display strings.Builder var display strings.Builder
for _, l := range lines { for _, d := range dirs {
if s := strings.TrimSpace(l); s != "" { if d.Mode&plan9.DMDIR == 0 {
id, rest, _ := strings.Cut(s, "\t") continue
sl.allSIDs = append(sl.allSIDs, id)
short := id
if idx := strings.IndexByte(id, '-'); idx > 0 {
short = id[:idx]
}
display.WriteString(short)
if rest != "" {
display.WriteByte('\t')
display.WriteString(rest)
}
display.WriteByte('\n')
} }
id := d.Name
name := id
if d, err := read9p("session/" + id + "/name"); err == nil {
if n := strings.TrimSpace(string(d)); n != "" {
name = n
}
}
sl.entries = append(sl.entries, sessionEntry{id: id, name: name})
display.WriteString(name)
display.WriteByte('\n')
} }
sort.Slice(sl.entries, func(i, j int) bool {
return sl.entries[i].name < sl.entries[j].name
})
sl.mu.Unlock() sl.mu.Unlock()
sl.win.Write("body", []byte(display.String())) sl.win.Write("body", []byte(display.String()))
@ -183,13 +189,9 @@ func (sl *sessionList) resolveFullSID(text string) string {
} }
sl.mu.Lock() sl.mu.Lock()
defer sl.mu.Unlock() defer sl.mu.Unlock()
for _, sid := range sl.allSIDs { for _, e := range sl.entries {
short := sid if e.name == text || e.id == text {
if idx := strings.IndexByte(sid, '-'); idx > 0 { return e.id
short = sid[:idx]
}
if short == text || sid == text {
return sid
} }
} }
return "" return ""
@ -252,18 +254,11 @@ func openChat(sid string) {
log.Printf("open chat %s: %v", sid, err) log.Printf("open chat %s: %v", sid, err)
return return
} }
short := sid winName := "ollie/sessions"
if idx := strings.IndexByte(sid, '-'); idx > 0 { if data, err := read9p("session/" + sid + "/name"); err == nil {
short = sid[:idx] name := strings.TrimSpace(string(data))
} if name != "" {
winName := "ollie/sessions/" + short winName = name + "/+Ollie"
if data, err := read9p("session/idx"); err == nil {
for _, line := range strings.Split(string(data), "\n") {
fields := strings.Split(line, "\t")
if len(fields) >= 3 && fields[0] == sid {
winName = fields[2] + "/+Ollie"
break
}
} }
} }
w.Name("%s", winName) w.Name("%s", winName)

View File

@ -70,9 +70,12 @@ func Create(rs *rootState, args []string) (string, error) {
} }
} }
sessID := name sessID := coresession.NewSessionID()
if sessID == "" { if name == "" {
sessID = coresession.NewSessionID() name = sessID
if idx := strings.IndexByte(sessID, '-'); idx > 0 {
name = sessID[:idx]
}
} }
rs.mu.RLock() rs.mu.RLock()
@ -246,6 +249,7 @@ func Create(rs *rootState, args []string) (string, error) {
sessionCtx, sessionCancel := context.WithCancel(rs.cfg.Ctx) sessionCtx, sessionCancel := context.WithCancel(rs.cfg.Ctx)
sess := NewSession(sessID, core, sessionCtx, sessionCancel) sess := NewSession(sessID, core, sessionCtx, sessionCancel)
sess.SetName(name)
sessPtr = sess sessPtr = sess
sess.AgentLog = al sess.AgentLog = al
sess.proc = proc sess.proc = proc

View File

@ -113,6 +113,18 @@ func (h *sessionHelper) fileSpec(name string, mode os.FileMode) FileSpec {
fs := FileSpec{Name: name, Mode: mode} fs := FileSpec{Name: name, Mode: mode}
switch name { switch name {
case "id":
fs.Read = func() ([]byte, error) { return []byte(h.sess.id), nil }
case "name":
fs.Read = func() ([]byte, error) { return []byte(h.sess.Name()), nil }
fs.Write = func(data []byte) error {
newName := strings.TrimSpace(string(data))
if newName == "" {
return nil
}
h.sess.SetName(newName)
return nil
}
case "env": case "env":
fs.Read = func() ([]byte, error) { return []byte(h.envContent()), nil } fs.Read = func() ([]byte, error) { return []byte(h.envContent()), nil }
case "ctl": case "ctl":

View File

@ -90,6 +90,8 @@ var Perms = map[string]Perm{
"toolsrv.loaded": 0444, "toolsrv.loaded": 0444,
"toolsrv.rev": 0444, "toolsrv.rev": 0444,
"tools": 0666, "tools": 0666,
"name": 0666,
"id": 0444,
}, },
}, },
} }

View File

@ -39,33 +39,19 @@ func KillFromRoot(root *Tree, id string) {
Kill(state(root), id) Kill(state(root), id)
} }
// Rename renames a session. // Rename renames a session by setting its Name. The immutable ID stays unchanged.
func Rename(rs *rootState, old, new string) error { func Rename(rs *rootState, old, new string) error {
rs.mu.Lock() rs.mu.RLock()
sess, ok := rs.sessions[old] sess, ok := rs.sessions[old]
rs.mu.RUnlock()
if !ok { if !ok {
rs.mu.Unlock()
return fmt.Errorf("session not found: %s", old) return fmt.Errorf("session not found: %s", old)
} }
if _, exists := rs.sessions[new]; exists {
rs.mu.Unlock()
return fmt.Errorf("session already exists: %s", new)
}
if sess.Core.Agent().IsRunning() { if sess.Core.Agent().IsRunning() {
rs.mu.Unlock()
return fmt.Errorf("cannot rename while agent is running") return fmt.Errorf("cannot rename while agent is running")
} }
if err := sess.Core.SetSessionID(new); err != nil { sess.SetName(new)
rs.mu.Unlock()
return err
}
sess.id = new
rs.sessions[new] = sess
delete(rs.sessions, old)
rs.mu.Unlock()
rs.cfg.Log.Info("renamed session %s -> %s", old, new) rs.cfg.Log.Info("renamed session %s -> %s", old, new)
broadcastEvents() broadcastEvents()
return nil return nil

View File

@ -486,17 +486,18 @@ func buildIndex(rs *rootState) []byte {
ids = append(ids, id) ids = append(ids, id)
} }
sort.Strings(ids) sort.Strings(ids)
for _, id := range ids { for _, id := range ids {
sess := rs.sessions[id] sess := rs.sessions[id]
sess.mu.RLock() sess.mu.RLock()
st := sess.Core.Agent().State() st := sess.Core.Agent().State()
cwd := sess.Core.CWD() cwd := sess.Core.CWD()
be := sess.Core.Agent().BackendName() be := sess.Core.Agent().BackendName()
model := sess.Core.Agent().ModelName() model := sess.Core.Agent().ModelName()
ag := sess.Core.Agent().Name() ag := sess.Core.Agent().Name()
sess.mu.RUnlock() name := sess.name
fmt.Fprintf(&sb, "%s\t%s\t%s\t%s\t%s\t%s\n", id, st, cwd, be, model, ag) sess.mu.RUnlock()
} fmt.Fprintf(&sb, "%s\t%s\t%s\t%s\t%s\t%s\n", name, st, cwd, be, model, ag)
}
rs.mu.RUnlock() rs.mu.RUnlock()
return []byte(sb.String()) return []byte(sb.String())
} }

View File

@ -15,7 +15,8 @@ import (
// Agent-scoped state (log, plan, etc.) lives in AgentLog. // Agent-scoped state (log, plan, etc.) lives in AgentLog.
type Session struct { type Session struct {
mu sync.RWMutex mu sync.RWMutex
id string id string // immutable UUID, set at creation, never changes
name string // mutable friendly name, defaults to id
uname string // immutable user principal (numeric UID), set at creation uname string // immutable user principal (numeric UID), set at creation
Core *session.Session Core *session.Session
SessionCtx context.Context SessionCtx context.Context
@ -53,7 +54,11 @@ func NewAgentLog(remote string) *AgentLog {
} }
func NewSession(id string, core *session.Session, ctx context.Context, cancel context.CancelFunc) *Session { func NewSession(id string, core *session.Session, ctx context.Context, cancel context.CancelFunc) *Session {
sess := &Session{id: id, Core: core, SessionCtx: ctx, cancel: cancel} name := id
if idx := strings.IndexByte(id, '-'); idx > 0 {
name = id[:idx]
}
sess := &Session{id: id, name: name, Core: core, SessionCtx: ctx, cancel: cancel}
return sess return sess
} }
@ -92,6 +97,16 @@ func (sess *Session) InvalidateModelsCache() {
func (sess *Session) RunnableID() string { return sess.id } func (sess *Session) RunnableID() string { return sess.id }
func (sess *Session) Uname() string { return sess.uname } func (sess *Session) Uname() string { return sess.uname }
func (sess *Session) Name() string {
sess.mu.RLock()
defer sess.mu.RUnlock()
return sess.name
}
func (sess *Session) SetName(name string) {
sess.mu.Lock()
defer sess.mu.Unlock()
sess.name = name
}
func (sess *Session) Cancel() { func (sess *Session) Cancel() {
sess.cancel() sess.cancel()