session: enforce agent/session split in file trees

- NewSessionTree now only serves plan + env (session-level)
- NewAgentTree serves all 21 agent-level files
- Tests updated to use agent tree for agent files
- Removed kill/rename/save from ctl (session ops via rm/mv)
- TestSessionManagerOpenStore tests session-level file (plan)
This commit is contained in:
Levi Neely 2026-07-29 20:56:09 +02:00
parent f7e084106c
commit c07aef6bb9
2 changed files with 29 additions and 66 deletions

View File

@ -160,9 +160,8 @@ var _ sync.Mutex // suppress unused import
func newTestSessionFileStore(t *testing.T, sess *session.Session) *fs.Tree { func newTestSessionFileStore(t *testing.T, sess *session.Session) *fs.Tree {
t.Helper() t.Helper()
sink := testSink() sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"), // Use agent tree for agent-level file tests
func() {}, sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func(id string) error { return nil },
func(data []byte) error { return nil }, func(data []byte) error { return nil },
func() {}, func() {},
nil, nil,
@ -174,6 +173,7 @@ func newTestSessionFileStore(t *testing.T, sess *session.Session) *fs.Tree {
func newTestSessionFileStoreWith(t *testing.T, sess *session.Session, kill func(), rename func(string) error, save func([]byte) error) *fs.Tree { func newTestSessionFileStoreWith(t *testing.T, sess *session.Session, kill func(), rename func(string) error, save func([]byte) error) *fs.Tree {
t.Helper() t.Helper()
sink := testSink() sink := testSink()
// Session tree for kill/rename/save operations
return session.NewSessionTree(sess, sink.NewLogger("test"), kill, rename, save, func() {}, nil, nil) return session.NewSessionTree(sess, sink.NewLogger("test"), kill, rename, save, func() {}, nil, nil)
} }
@ -390,23 +390,23 @@ func TestSessionFileStoreReadableContract(t *testing.T) {
sess := testSession("s1") sess := testSession("s1")
defer sess.Cancel() defer sess.Cancel()
sink := testSink() sink := testSink()
_ = session.NewSessionTree(sess, sink.NewLogger("test"), _ = session.NewAgentTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil) func([]byte) error { return nil }, func() {}, nil, nil)
} }
func TestSessionFileStoreList(t *testing.T) { func TestSessionFileStoreList(t *testing.T) {
sess := testSession("s1") sess := testSession("s1")
defer sess.Cancel() defer sess.Cancel()
sink := testSink() sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"), sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil) func([]byte) error { return nil }, func() {}, nil, nil)
entries, err := sf.List() entries, err := sf.List()
if err != nil { if err != nil {
t.Fatalf("List: %v", err) t.Fatalf("List: %v", err)
} }
if len(entries) != len(session.FileList) { if len(entries) != len(session.AgentFileList) {
t.Errorf("List() returned %d entries; want %d", len(entries), len(session.FileList)) t.Errorf("List() returned %d entries; want %d", len(entries), len(session.AgentFileList))
} }
} }
@ -415,8 +415,8 @@ func TestSessionFileStoreStatChat(t *testing.T) {
defer sess.Cancel() defer sess.Cancel()
sess.AppendLog([]byte("hello")) sess.AppendLog([]byte("hello"))
sink := testSink() sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"), sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil) func([]byte) error { return nil }, func() {}, nil, nil)
fi, err := sf.Stat("chat") fi, err := sf.Stat("chat")
if err != nil { if err != nil {
@ -432,8 +432,8 @@ func TestSessionFileStoreGetChat(t *testing.T) {
defer sess.Cancel() defer sess.Cancel()
sess.AppendLog([]byte("hello")) sess.AppendLog([]byte("hello"))
sink := testSink() sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"), sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil) func([]byte) error { return nil }, func() {}, nil, nil)
data := testStoreRead(t, sf, "chat") data := testStoreRead(t, sf, "chat")
if string(data) != "hello" { if string(data) != "hello" {
@ -445,8 +445,8 @@ func TestSessionFileStoreGetContent(t *testing.T) {
sess := testSession("s1") sess := testSession("s1")
defer sess.Cancel() defer sess.Cancel()
sink := testSink() sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"), sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil) func([]byte) error { return nil }, func() {}, nil, nil)
for _, name := range []string{"cfg", "offset", "usage", "ctxsz", "models", "systemprompt"} { for _, name := range []string{"cfg", "offset", "usage", "ctxsz", "models", "systemprompt"} {
if _, err := sf.Open(name); err != nil { if _, err := sf.Open(name); err != nil {
@ -459,8 +459,8 @@ func TestSessionFileStorePutCwd(t *testing.T) {
sess := testSession("s1") sess := testSession("s1")
defer sess.Cancel() defer sess.Cancel()
sink := testSink() sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"), sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil) func([]byte) error { return nil }, func() {}, nil, nil)
// Create the directory first so SetCWD validates it // Create the directory first so SetCWD validates it
os.MkdirAll("/tmp/newpath", 0755) os.MkdirAll("/tmp/newpath", 0755)
@ -475,8 +475,8 @@ func TestSessionFileStorePutEmpty(t *testing.T) {
sess := testSession("s1") sess := testSession("s1")
defer sess.Cancel() defer sess.Cancel()
sink := testSink() sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"), sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil, nil) func([]byte) error { return nil }, func() {}, nil, nil)
// Empty write is a no-op // Empty write is a no-op
e, err := sf.Open("cfg") e, err := sf.Open("cfg")
@ -581,9 +581,9 @@ func TestSessionFileStoreWriteChat(t *testing.T) {
sess := testSession("s1") sess := testSession("s1")
defer sess.Cancel() defer sess.Cancel()
var saved []byte var saved []byte
sf := newTestSessionFileStoreWith(t, sess, sink := testSink()
func() {}, func(string) error { return nil }, sf := session.NewAgentTree(sess, sink.NewLogger("test"),
func(data []byte) error { saved = data; return nil }) func(data []byte) error { saved = data; return nil }, func() {}, nil, nil)
testStoreWrite(t, sf, "chat", []byte("transcript data")) testStoreWrite(t, sf, "chat", []byte("transcript data"))
if string(saved) != "transcript data" { if string(saved) != "transcript data" {
@ -679,40 +679,6 @@ func TestSessionFileStoreHandleCtl(t *testing.T) {
t.Error("ctl stop should interrupt (session should not be running)") t.Error("ctl stop should interrupt (session should not be running)")
} }
// kill
var killed bool
sf2 := newTestSessionFileStoreWith(t, sess,
func() { killed = true },
func(string) error { return nil },
func([]byte) error { return nil })
testStoreWrite(t, sf2, "ctl", []byte("kill"))
if !killed {
t.Error("ctl kill should call kill callback")
}
// rn (rename)
var renamed string
sf3 := newTestSessionFileStoreWith(t, sess,
func() {},
func(id string) error { renamed = id; return nil },
func([]byte) error { return nil })
testStoreWrite(t, sf3, "ctl", []byte("rn newname"))
if renamed != "newname" {
t.Errorf("renamed = %q; want newname", renamed)
}
// save
sess.AppendLog([]byte("log data"))
var saved []byte
sf4 := newTestSessionFileStoreWith(t, sess,
func() {},
func(string) error { return nil },
func(data []byte) error { saved = data; return nil })
testStoreWrite(t, sf4, "ctl", []byte("save"))
if !strings.Contains(string(saved), "log data") {
t.Errorf("saved = %q; want to contain log data", saved)
}
// slash commands forwarded to Submit // slash commands forwarded to Submit
sess5 := testSession("s5") sess5 := testSession("s5")
defer sess5.Cancel() defer sess5.Cancel()
@ -949,17 +915,14 @@ func TestSessionManagerOpenStore(t *testing.T) {
defer sess.Cancel() defer sess.Cancel()
s.AddSession(sess) s.AddSession(sess)
// Access a session file through the manager // Access a session-level file through the manager
e, err := s.Tree().Open("s1/cfg") e, err := s.Tree().Open("s1/plan")
if err != nil { if err != nil {
t.Fatalf("Open(s1/cfg): %v", err) t.Fatalf("Open(s1/plan): %v", err)
} }
data, err := e.Read() _, err = e.Read()
if err != nil { if err != nil {
t.Fatalf("Read(s1/cfg): %v", err) t.Fatalf("Read(s1/plan): %v", err)
}
if !strings.Contains(string(data), "name=s1") {
t.Errorf("cfg = %q; want to contain name=s1", data)
} }
} }

View File

@ -66,8 +66,8 @@ func sessionFilePerms() map[string]os.FileMode {
func NewSessionTree(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error, invalidateModels func(), resetElevation func(), toolRegistry *tools.Registry) *fs.Tree { func NewSessionTree(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error, invalidateModels func(), resetElevation func(), toolRegistry *tools.Registry) *fs.Tree {
h := &sessionHelper{sess: sess, log: log, kill: kill, rename: rename, saveTranscript: saveTranscript, invalidateModels: invalidateModels, resetElevation: resetElevation, toolRegistry: toolRegistry} h := &sessionHelper{sess: sess, log: log, kill: kill, rename: rename, saveTranscript: saveTranscript, invalidateModels: invalidateModels, resetElevation: resetElevation, toolRegistry: toolRegistry}
perms := sessionFilePerms() perms := sessionFilePerms()
specs := make([]fs.FileSpec, len(FileList)) specs := make([]fs.FileSpec, len(SessionFileList))
for i, f := range FileList { for i, f := range SessionFileList {
specs[i] = h.fileSpec(f.Name, perms[f.Name]) specs[i] = h.fileSpec(f.Name, perms[f.Name])
specs[i].OneShot = f.OneShot specs[i].OneShot = f.OneShot
specs[i].Async = f.Async specs[i].Async = f.Async