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

View File

@ -70,9 +70,12 @@ func Create(rs *rootState, args []string) (string, error) {
}
}
sessID := name
if sessID == "" {
sessID = coresession.NewSessionID()
sessID := coresession.NewSessionID()
if name == "" {
name = sessID
if idx := strings.IndexByte(sessID, '-'); idx > 0 {
name = sessID[:idx]
}
}
rs.mu.RLock()
@ -246,6 +249,7 @@ func Create(rs *rootState, args []string) (string, error) {
sessionCtx, sessionCancel := context.WithCancel(rs.cfg.Ctx)
sess := NewSession(sessID, core, sessionCtx, sessionCancel)
sess.SetName(name)
sessPtr = sess
sess.AgentLog = al
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}
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":
fs.Read = func() ([]byte, error) { return []byte(h.envContent()), nil }
case "ctl":

View File

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

View File

@ -39,33 +39,19 @@ func KillFromRoot(root *Tree, id string) {
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 {
rs.mu.Lock()
rs.mu.RLock()
sess, ok := rs.sessions[old]
rs.mu.RUnlock()
if !ok {
rs.mu.Unlock()
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() {
rs.mu.Unlock()
return fmt.Errorf("cannot rename while agent is running")
}
if err := sess.Core.SetSessionID(new); err != nil {
rs.mu.Unlock()
return err
}
sess.id = new
rs.sessions[new] = sess
delete(rs.sessions, old)
rs.mu.Unlock()
sess.SetName(new)
rs.cfg.Log.Info("renamed session %s -> %s", old, new)
broadcastEvents()
return nil

View File

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

View File

@ -15,7 +15,8 @@ import (
// Agent-scoped state (log, plan, etc.) lives in AgentLog.
type Session struct {
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
Core *session.Session
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 {
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
}
@ -92,6 +97,16 @@ func (sess *Session) InvalidateModelsCache() {
func (sess *Session) RunnableID() string { return sess.id }
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() {
sess.cancel()