session/session.go: session logic in its own package
- session.Manager (was SessionManager) - session.ManagerConfig (was SessionManagerConfig) - session.Session, session.NewSession - session.FileStore, session.NewFileStore - session.FileMode, session.FileList - session.FormatEvent, session.FormatParams, session.ParseParams - server.go imports olliesrv/session - Clean package boundary: session/ = lifecycle, store/ = abstraction
This commit is contained in:
parent
5ca93e685f
commit
d0ce8a108e
121
main_test.go
121
main_test.go
|
|
@ -13,6 +13,7 @@ import (
|
|||
"ollie/pkg/agent"
|
||||
"ollie/pkg/backend"
|
||||
olog "ollie/pkg/log"
|
||||
"olliesrv/session"
|
||||
"olliesrv/store"
|
||||
)
|
||||
|
||||
|
|
@ -106,15 +107,15 @@ func (c *blockingCore) Submit(ctx context.Context, input string, handler agent.E
|
|||
|
||||
func testSink() *olog.Sink { return olog.NewSink(io.Discard, io.Discard, olog.LevelError) }
|
||||
|
||||
func testSession(id string) *Session {
|
||||
func testSession(id string) *session.Session {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return NewSession(id, &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp"}, ctx, cancel)
|
||||
return session.NewSession(id, &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp"}, ctx, cancel)
|
||||
}
|
||||
|
||||
func newTestSessionManager(t *testing.T) *SessionManager {
|
||||
func newTestSessionManager(t *testing.T) *session.Manager {
|
||||
t.Helper()
|
||||
sink := testSink()
|
||||
return NewSessionManager(SessionManagerConfig{
|
||||
return session.NewManager(session.ManagerConfig{
|
||||
Log: sink.NewLogger("test"),
|
||||
Sink: sink,
|
||||
ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil },
|
||||
|
|
@ -122,10 +123,10 @@ func newTestSessionManager(t *testing.T) *SessionManager {
|
|||
})
|
||||
}
|
||||
|
||||
func newTestSessionManagerWithCore(t *testing.T) *SessionManager {
|
||||
func newTestSessionManagerWithCore(t *testing.T) *session.Manager {
|
||||
t.Helper()
|
||||
sink := testSink()
|
||||
return NewSessionManager(SessionManagerConfig{
|
||||
return session.NewManager(session.ManagerConfig{
|
||||
Log: sink.NewLogger("test"),
|
||||
Sink: sink,
|
||||
ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil },
|
||||
|
|
@ -157,11 +158,11 @@ var _ = strings.Contains // suppress unused import
|
|||
var _ = fmt.Sprintf // suppress unused import
|
||||
var _ = os.Remove // suppress unused import
|
||||
|
||||
func newTestSessionFileStore(t *testing.T, sess *Session) (*SessionFileStore, *stubCore) {
|
||||
func newTestSessionFileStore(t *testing.T, sess *session.Session) (*session.FileStore, *stubCore) {
|
||||
t.Helper()
|
||||
sink := testSink()
|
||||
core := sess.Core.(*stubCore)
|
||||
sf := NewSessionFileStore(sess, sink.NewLogger("test"),
|
||||
sf := session.NewFileStore(sess, sink.NewLogger("test"),
|
||||
func() {},
|
||||
func(id string) error { return nil },
|
||||
func(data []byte) error { return nil },
|
||||
|
|
@ -169,12 +170,12 @@ func newTestSessionFileStore(t *testing.T, sess *Session) (*SessionFileStore, *s
|
|||
return sf, core
|
||||
}
|
||||
|
||||
func newTestSessionFileStoreWith(t *testing.T, sess *Session, kill func(), rename func(string) error, save func([]byte) error) *SessionFileStore {
|
||||
func newTestSessionFileStoreWith(t *testing.T, sess *session.Session, kill func(), rename func(string) error, save func([]byte) error) *session.FileStore {
|
||||
t.Helper()
|
||||
sink := testSink()
|
||||
return NewSessionFileStore(sess, sink.NewLogger("test"), kill, rename, save)
|
||||
return session.NewFileStore(sess, sink.NewLogger("test"), kill, rename, save)
|
||||
}
|
||||
// ===== Session =====
|
||||
// ===== session.Session =====
|
||||
|
||||
func TestSessionAppendLog(t *testing.T) {
|
||||
sess := testSession("s1")
|
||||
|
|
@ -194,15 +195,15 @@ func TestSessionAppendLog(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSessionManagerFileMode(t *testing.T) {
|
||||
if m, ok := SessionManagerFileMode("new"); !ok || m != 0666 {
|
||||
t.Errorf("SessionManagerFileMode(new) = %o, %v", m, ok)
|
||||
if m, ok := session.FileMode("new"); !ok || m != 0666 {
|
||||
t.Errorf("session.FileMode(new) = %o, %v", m, ok)
|
||||
}
|
||||
if _, ok := SessionManagerFileMode("bogus"); ok {
|
||||
t.Error("SessionManagerFileMode(bogus) should be false")
|
||||
if _, ok := session.FileMode("bogus"); ok {
|
||||
t.Error("session.FileMode(bogus) should be false")
|
||||
}
|
||||
}
|
||||
|
||||
// ===== SessionManager =====
|
||||
// ===== session.Manager =====
|
||||
|
||||
func TestSessionManagerReadableContract(t *testing.T) {
|
||||
}
|
||||
|
|
@ -301,13 +302,13 @@ func TestSessionManagerDeleteAndKill(t *testing.T) {
|
|||
func TestSessionManagerSession(t *testing.T) {
|
||||
s := newTestSessionManager(t)
|
||||
if s.Session("nope") != nil {
|
||||
t.Error("Session(nonexistent) should be nil")
|
||||
t.Error("session.Session(nonexistent) should be nil")
|
||||
}
|
||||
sess := testSession("s1")
|
||||
defer sess.Cancel()
|
||||
s.AddSession(sess)
|
||||
if s.Session("s1") == nil {
|
||||
t.Error("Session(s1) should not be nil")
|
||||
t.Error("session.Session(s1) should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -335,7 +336,7 @@ func TestSessionManagerShutdown(t *testing.T) {
|
|||
func TestSessionManagerRename(t *testing.T) {
|
||||
sink := testSink()
|
||||
var renamed [2]string
|
||||
s := NewSessionManager(SessionManagerConfig{
|
||||
s := session.NewManager(session.ManagerConfig{
|
||||
Log: sink.NewLogger("test"),
|
||||
Sink: sink,
|
||||
ReadFile: func(string) ([]byte, error) { return nil, nil },
|
||||
|
|
@ -388,13 +389,13 @@ func TestSessionManagerRenameErrors(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// ===== SessionFileStore =====
|
||||
// ===== session.FileStore =====
|
||||
|
||||
func TestSessionFileStoreReadableContract(t *testing.T) {
|
||||
sess := testSession("s1")
|
||||
defer sess.Cancel()
|
||||
sink := testSink()
|
||||
_ = NewSessionFileStore(sess, sink.NewLogger("test"),
|
||||
_ = session.NewFileStore(sess, sink.NewLogger("test"),
|
||||
func() {}, func(string) error { return nil }, func([]byte) error { return nil })
|
||||
}
|
||||
|
||||
|
|
@ -402,15 +403,15 @@ func TestSessionFileStoreList(t *testing.T) {
|
|||
sess := testSession("s1")
|
||||
defer sess.Cancel()
|
||||
sink := testSink()
|
||||
sf := NewSessionFileStore(sess, sink.NewLogger("test"),
|
||||
sf := session.NewFileStore(sess, sink.NewLogger("test"),
|
||||
func() {}, func(string) error { return nil }, func([]byte) error { return nil })
|
||||
|
||||
entries, err := sf.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(entries) != len(SessionFileList) {
|
||||
t.Errorf("List() returned %d entries; want %d", len(entries), len(SessionFileList))
|
||||
if len(entries) != len(session.FileList) {
|
||||
t.Errorf("List() returned %d entries; want %d", len(entries), len(session.FileList))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -419,7 +420,7 @@ func TestSessionFileStoreStatChat(t *testing.T) {
|
|||
defer sess.Cancel()
|
||||
sess.AppendLog([]byte("hello"))
|
||||
sink := testSink()
|
||||
sf := NewSessionFileStore(sess, sink.NewLogger("test"),
|
||||
sf := session.NewFileStore(sess, sink.NewLogger("test"),
|
||||
func() {}, func(string) error { return nil }, func([]byte) error { return nil })
|
||||
|
||||
fi, err := sf.Stat("chat")
|
||||
|
|
@ -436,7 +437,7 @@ func TestSessionFileStoreGetChat(t *testing.T) {
|
|||
defer sess.Cancel()
|
||||
sess.AppendLog([]byte("hello"))
|
||||
sink := testSink()
|
||||
sf := NewSessionFileStore(sess, sink.NewLogger("test"),
|
||||
sf := session.NewFileStore(sess, sink.NewLogger("test"),
|
||||
func() {}, func(string) error { return nil }, func([]byte) error { return nil })
|
||||
|
||||
data := testStoreRead(t, sf, "chat")
|
||||
|
|
@ -449,7 +450,7 @@ func TestSessionFileStoreGetContent(t *testing.T) {
|
|||
sess := testSession("s1")
|
||||
defer sess.Cancel()
|
||||
sink := testSink()
|
||||
sf := NewSessionFileStore(sess, sink.NewLogger("test"),
|
||||
sf := session.NewFileStore(sess, sink.NewLogger("test"),
|
||||
func() {}, func(string) error { return nil }, func([]byte) error { return nil })
|
||||
|
||||
for _, name := range []string{"cfg", "offset", "usage", "ctxsz", "models", "systemprompt"} {
|
||||
|
|
@ -463,7 +464,7 @@ func TestSessionFileStorePutCwd(t *testing.T) {
|
|||
sess := testSession("s1")
|
||||
defer sess.Cancel()
|
||||
sink := testSink()
|
||||
sf := NewSessionFileStore(sess, sink.NewLogger("test"),
|
||||
sf := session.NewFileStore(sess, sink.NewLogger("test"),
|
||||
func() {}, func(string) error { return nil }, func([]byte) error { return nil })
|
||||
|
||||
testStoreWrite(t, sf, "cfg", []byte("cwd=/new/path"))
|
||||
|
|
@ -477,7 +478,7 @@ func TestSessionFileStorePutEmpty(t *testing.T) {
|
|||
sess := testSession("s1")
|
||||
defer sess.Cancel()
|
||||
sink := testSink()
|
||||
sf := NewSessionFileStore(sess, sink.NewLogger("test"),
|
||||
sf := session.NewFileStore(sess, sink.NewLogger("test"),
|
||||
func() {}, func(string) error { return nil }, func([]byte) error { return nil })
|
||||
|
||||
// Empty write is a no-op
|
||||
|
|
@ -490,7 +491,7 @@ func TestSessionFileStorePutEmpty(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// ===== SessionFileStore: content, writeFile, handleCtl, blockingRead, makePublish =====
|
||||
// ===== session.FileStore: content, writeFile, handleCtl, blockingRead, makePublish =====
|
||||
|
||||
func TestSessionFileStoreContentAllFields(t *testing.T) {
|
||||
sess := testSession("s1")
|
||||
|
|
@ -712,7 +713,7 @@ func TestSessionFileStoreHandleCtl(t *testing.T) {
|
|||
core5 := &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp"}
|
||||
ctx5, cancel5 := context.WithCancel(context.Background())
|
||||
defer cancel5()
|
||||
sess5 := NewSession("s5", core5, ctx5, cancel5)
|
||||
sess5 := session.NewSession("s5", core5, ctx5, cancel5)
|
||||
sf5, _ := newTestSessionFileStore(t, sess5)
|
||||
for _, cmd := range []string{"compact", "clear", "help", "history", "tools", "skills"} {
|
||||
testStoreWrite(t, sf5, "ctl", []byte(cmd))
|
||||
|
|
@ -740,7 +741,7 @@ func TestSessionFileStoreHandleCtlErrors(t *testing.T) {
|
|||
func TestSessionFileStoreBlockingRead(t *testing.T) {
|
||||
core := &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp", waitCh: make(chan string, 1)}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sess := NewSession("s1", core, ctx, cancel)
|
||||
sess := session.NewSession("s1", core, ctx, cancel)
|
||||
defer cancel()
|
||||
sf, _ := newTestSessionFileStore(t, sess)
|
||||
|
||||
|
|
@ -795,7 +796,7 @@ func TestSessionFileStoreBlockingReadAllWaitFiles(t *testing.T) {
|
|||
// to avoid blocking forever in wait loops.
|
||||
core := &stubCore{state: "thinking", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp", usage: "0", ctxsz: "0", waitCh: make(chan string, 1)}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sess := NewSession("s1", core, ctx, cancel)
|
||||
sess := session.NewSession("s1", core, ctx, cancel)
|
||||
defer cancel()
|
||||
sf, _ := newTestSessionFileStore(t, sess)
|
||||
|
||||
|
|
@ -820,7 +821,7 @@ func TestSessionFileStoreBlockingReadIdleTimeout(t *testing.T) {
|
|||
core := &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp"}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
sess := NewSession("s1", core, ctx, cancel)
|
||||
sess := session.NewSession("s1", core, ctx, cancel)
|
||||
sf, _ := newTestSessionFileStore(t, sess)
|
||||
|
||||
e, err := sf.Open("statewait")
|
||||
|
|
@ -865,7 +866,7 @@ func TestSessionFileStoreMakePublishMultipleEvents(t *testing.T) {
|
|||
core2 := &publishCore{stubCore: core}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
sess := NewSession("s1", core2, ctx, cancel)
|
||||
sess := session.NewSession("s1", core2, ctx, cancel)
|
||||
sf := newTestSessionFileStoreWith(t, sess,
|
||||
func() {}, func(string) error { return nil }, func([]byte) error { return nil })
|
||||
|
||||
|
|
@ -940,24 +941,24 @@ func TestSessionManagerOpenStore(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// ===== LoadAgentConfig =====
|
||||
// ===== session.LoadAgentConfig =====
|
||||
|
||||
func TestLoadAgentConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
os.WriteFile(filepath.Join(dir, "test.json"), []byte(`{}`), 0644)
|
||||
|
||||
cfg := LoadAgentConfig(dir, "test", nil)
|
||||
cfg := session.LoadAgentConfig(dir, "test", nil)
|
||||
if cfg == nil {
|
||||
t.Error("LoadAgentConfig should return non-nil for existing file")
|
||||
t.Error("session.LoadAgentConfig should return non-nil for existing file")
|
||||
}
|
||||
|
||||
cfg = LoadAgentConfig(dir, "nonexistent", nil)
|
||||
cfg = session.LoadAgentConfig(dir, "nonexistent", nil)
|
||||
if cfg != nil {
|
||||
t.Error("LoadAgentConfig should return nil for missing file")
|
||||
t.Error("session.LoadAgentConfig should return nil for missing file")
|
||||
}
|
||||
}
|
||||
|
||||
// ===== FormatEvent =====
|
||||
// ===== session.FormatEvent =====
|
||||
|
||||
func TestFormatEvent(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
|
|
@ -974,9 +975,9 @@ func TestFormatEvent(t *testing.T) {
|
|||
{agent.Event{Role: "info", Content: "note"}, ":: note"},
|
||||
{agent.Event{Role: "unknown"}, ""},
|
||||
} {
|
||||
got := string(FormatEvent(tc.ev))
|
||||
got := string(session.FormatEvent(tc.ev))
|
||||
if got != tc.want {
|
||||
t.Errorf("FormatEvent(%q) = %q; want %q", tc.ev.Role, got, tc.want)
|
||||
t.Errorf("session.FormatEvent(%q) = %q; want %q", tc.ev.Role, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1009,7 +1010,7 @@ func TestFileEntryDirEntry(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// ===== FormatParams / ParseParams =====
|
||||
// ===== session.FormatParams / session.ParseParams =====
|
||||
|
||||
func TestFormatParamsRoundTrip(t *testing.T) {
|
||||
temp := 0.7
|
||||
|
|
@ -1021,10 +1022,10 @@ func TestFormatParamsRoundTrip(t *testing.T) {
|
|||
FrequencyPenalty: &freq,
|
||||
PresencePenalty: &pres,
|
||||
}
|
||||
out := FormatParams(p)
|
||||
got, err := ParseParams(out, backend.GenerationParams{})
|
||||
out := session.FormatParams(p)
|
||||
got, err := session.ParseParams(out, backend.GenerationParams{})
|
||||
if err != nil {
|
||||
t.Fatalf("ParseParams: %v", err)
|
||||
t.Fatalf("session.ParseParams: %v", err)
|
||||
}
|
||||
if got.MaxTokens != 1024 {
|
||||
t.Errorf("MaxTokens = %d; want 1024", got.MaxTokens)
|
||||
|
|
@ -1042,10 +1043,10 @@ func TestFormatParamsRoundTrip(t *testing.T) {
|
|||
|
||||
func TestFormatParamsNilOptionals(t *testing.T) {
|
||||
p := backend.GenerationParams{MaxTokens: 512}
|
||||
out := FormatParams(p)
|
||||
got, err := ParseParams(out, backend.GenerationParams{})
|
||||
out := session.FormatParams(p)
|
||||
got, err := session.ParseParams(out, backend.GenerationParams{})
|
||||
if err != nil {
|
||||
t.Fatalf("ParseParams: %v", err)
|
||||
t.Fatalf("session.ParseParams: %v", err)
|
||||
}
|
||||
if got.MaxTokens != 512 {
|
||||
t.Errorf("MaxTokens = %d; want 512", got.MaxTokens)
|
||||
|
|
@ -1058,9 +1059,9 @@ func TestFormatParamsNilOptionals(t *testing.T) {
|
|||
func TestParseParamsClearWithEmpty(t *testing.T) {
|
||||
temp := 1.0
|
||||
p := backend.GenerationParams{MaxTokens: 100, Temperature: &temp}
|
||||
got, err := ParseParams("maxTokens=\ntemperature=\nfrequencyPenalty=\npresencePenalty=\n", p)
|
||||
got, err := session.ParseParams("maxTokens=\ntemperature=\nfrequencyPenalty=\npresencePenalty=\n", p)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseParams: %v", err)
|
||||
t.Fatalf("session.ParseParams: %v", err)
|
||||
}
|
||||
if got.MaxTokens != 0 {
|
||||
t.Errorf("MaxTokens = %d; want 0", got.MaxTokens)
|
||||
|
|
@ -1071,21 +1072,21 @@ func TestParseParamsClearWithEmpty(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseParamsErrors(t *testing.T) {
|
||||
if _, err := ParseParams("maxTokens=bad", backend.GenerationParams{}); err == nil {
|
||||
if _, err := session.ParseParams("maxTokens=bad", backend.GenerationParams{}); err == nil {
|
||||
t.Error("expected error for invalid maxTokens")
|
||||
}
|
||||
if _, err := ParseParams("temperature=bad", backend.GenerationParams{}); err == nil {
|
||||
if _, err := session.ParseParams("temperature=bad", backend.GenerationParams{}); err == nil {
|
||||
t.Error("expected error for invalid temperature")
|
||||
}
|
||||
if _, err := ParseParams("frequencyPenalty=bad", backend.GenerationParams{}); err == nil {
|
||||
if _, err := session.ParseParams("frequencyPenalty=bad", backend.GenerationParams{}); err == nil {
|
||||
t.Error("expected error for invalid frequencyPenalty")
|
||||
}
|
||||
if _, err := ParseParams("presencePenalty=bad", backend.GenerationParams{}); err == nil {
|
||||
if _, err := session.ParseParams("presencePenalty=bad", backend.GenerationParams{}); err == nil {
|
||||
t.Error("expected error for invalid presencePenalty")
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Session.Interrupt =====
|
||||
// ===== session.Session.Interrupt =====
|
||||
|
||||
func TestSessionInterrupt(t *testing.T) {
|
||||
sess := testSession("i1")
|
||||
|
|
@ -1094,7 +1095,7 @@ func TestSessionInterrupt(t *testing.T) {
|
|||
// stubCore.Interrupt is a no-op; just ensure it doesn't panic
|
||||
}
|
||||
|
||||
// ===== SessionManager.createSession =====
|
||||
// ===== session.Manager.createSession =====
|
||||
|
||||
func TestSessionManagerCreateSessionViaWrite(t *testing.T) {
|
||||
s := newTestSessionManagerWithCore(t)
|
||||
|
|
@ -1153,7 +1154,7 @@ func TestSessionManagerCreateSessionEnvExpansion(t *testing.T) {
|
|||
t.Setenv("HOME", home)
|
||||
var gotCwd string
|
||||
sink := testSink()
|
||||
s := NewSessionManager(SessionManagerConfig{
|
||||
s := session.NewManager(session.ManagerConfig{
|
||||
Log: sink.NewLogger("test"),
|
||||
Sink: sink,
|
||||
ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil },
|
||||
|
|
@ -1180,7 +1181,7 @@ func TestSessionManagerCreateSessionTildeExpansion(t *testing.T) {
|
|||
}
|
||||
var gotCwd string
|
||||
sink := testSink()
|
||||
s := NewSessionManager(SessionManagerConfig{
|
||||
s := session.NewManager(session.ManagerConfig{
|
||||
Log: sink.NewLogger("test"),
|
||||
Sink: sink,
|
||||
ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil },
|
||||
|
|
|
|||
19
server.go
19
server.go
|
|
@ -19,6 +19,7 @@ import (
|
|||
olog "ollie/pkg/log"
|
||||
"ollie/pkg/paths"
|
||||
"ollie/pkg/tools/execute"
|
||||
"olliesrv/session"
|
||||
"olliesrv/store"
|
||||
|
||||
"9fans.net/go/plan9"
|
||||
|
|
@ -70,7 +71,7 @@ type Server struct {
|
|||
utilStore Store
|
||||
pluginStore Store
|
||||
skillStore Store
|
||||
sessionMgr *SessionManager
|
||||
sessionMgr *session.Manager
|
||||
transcriptStore Store
|
||||
tmpStore Store
|
||||
rootStore *store.DirStore
|
||||
|
|
@ -115,7 +116,7 @@ func New(sink *olog.Sink, opts ...ServerOption) *Server {
|
|||
for _, o := range opts {
|
||||
o(s)
|
||||
}
|
||||
s.sessionMgr = NewSessionManager(SessionManagerConfig{
|
||||
s.sessionMgr = session.NewManager(session.ManagerConfig{
|
||||
AgentsDir: agentsDir,
|
||||
SessionsDir: sessionsDir,
|
||||
Log: s.log,
|
||||
|
|
@ -242,7 +243,7 @@ func (s *Server) InGroup(group, user string) bool {
|
|||
func (s *Server) fileOwnerGroup(path string) (uid, gid string) {
|
||||
if strings.HasPrefix(path, "/s/") {
|
||||
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 2)
|
||||
if len(parts) >= 1 && parts[0] != "new" && !isSessionManagerFile(path) {
|
||||
if len(parts) >= 1 && parts[0] != "new" && !isSessionFile(path) {
|
||||
if sess := s.sessionMgr.Session(parts[0]); sess != nil {
|
||||
return sess.Uname(), "agent"
|
||||
}
|
||||
|
|
@ -457,14 +458,14 @@ func (s *Server) handle(cs *connState, fc *plan9.Fcall, ctx context.Context) *pl
|
|||
}
|
||||
}
|
||||
|
||||
// isSessionManagerFile reports whether path is a fixed file directly under /s/
|
||||
// isSessionFile reports whether path is a fixed file directly under /s/
|
||||
// (i.e. /s/<name> where name is in sessionStoreFiles).
|
||||
func isSessionManagerFile(path string) bool {
|
||||
func isSessionFile(path string) bool {
|
||||
name, ok := strings.CutPrefix(path, "/s/")
|
||||
if !ok || strings.Contains(name, "/") {
|
||||
return false
|
||||
}
|
||||
_, ok = SessionManagerFileMode(name)
|
||||
_, ok = session.FileMode(name)
|
||||
return ok
|
||||
}
|
||||
|
||||
|
|
@ -1076,8 +1077,8 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
|||
default:
|
||||
if path == "/backends" || path == "/help" {
|
||||
mode = 0444
|
||||
} else if isSessionManagerFile(path) {
|
||||
mode_, _ := SessionManagerFileMode(base)
|
||||
} else if isSessionFile(path) {
|
||||
mode_, _ := session.FileMode(base)
|
||||
mode = plan9.Perm(mode_)
|
||||
} else if strings.HasPrefix(path, "/a/") {
|
||||
mode = 0666
|
||||
|
|
@ -1147,7 +1148,7 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
|||
// that check stat before reading (cat, 9pfuse, etc.) see non-zero size.
|
||||
if dir.Length == 0 && !isDir {
|
||||
switch {
|
||||
case isSessionManagerFile(path):
|
||||
case isSessionFile(path):
|
||||
if content, err := storeRead(s.sessionMgr.Store(), base); err == nil {
|
||||
dir.Length = uint64(len(content))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -93,15 +93,15 @@ var sessionStoreFiles = map[string]os.FileMode{
|
|||
|
||||
var sessionStoreOrder = []string{"new", "idx", "ls", "kill", "sh", "b", "bfg", "bbg", "cleanup"}
|
||||
|
||||
// SessionManagerFileMode returns the mode for a fixed session store file,
|
||||
// FileMode returns the mode for a fixed session store file,
|
||||
// or 0 and false if the name is not a fixed file.
|
||||
func SessionManagerFileMode(name string) (os.FileMode, bool) {
|
||||
func FileMode(name string) (os.FileMode, bool) {
|
||||
m, ok := sessionStoreFiles[name]
|
||||
return m, ok
|
||||
}
|
||||
|
||||
// SessionManagerConfig holds the dependencies for a SessionManager.
|
||||
type SessionManagerConfig struct {
|
||||
// ManagerConfig holds the dependencies for a Manager.
|
||||
type ManagerConfig struct {
|
||||
AgentsDir string
|
||||
SessionsDir string
|
||||
Log *olog.Logger
|
||||
|
|
@ -123,31 +123,31 @@ type SessionManagerConfig struct {
|
|||
Yolo bool
|
||||
}
|
||||
|
||||
// SessionManager manages session lifecycle and exposes sessions as a DirStore.
|
||||
type SessionManager struct {
|
||||
// Manager manages session lifecycle and exposes sessions as a DirStore.
|
||||
type Manager struct {
|
||||
store *store.DirStore
|
||||
cfg SessionManagerConfig
|
||||
cfg ManagerConfig
|
||||
mu sync.RWMutex
|
||||
sessions map[string]*Session
|
||||
nextUID atomic.Uint32 // incrementing principal counter
|
||||
}
|
||||
|
||||
// Store returns the DirStore view of the session namespace.
|
||||
func (s *SessionManager) Store() *store.DirStore { return s.store }
|
||||
func (s *Manager) Store() *store.DirStore { return s.store }
|
||||
|
||||
// nextUname generates the next uname atomically.
|
||||
func (s *SessionManager) nextUname() string {
|
||||
func (s *Manager) nextUname() string {
|
||||
return fmt.Sprintf("%d", s.nextUID.Add(1))
|
||||
}
|
||||
|
||||
func NewSessionManager(cfg SessionManagerConfig) *SessionManager {
|
||||
func NewManager(cfg ManagerConfig) *Manager {
|
||||
if cfg.ReadFile == nil {
|
||||
cfg.ReadFile = os.ReadFile
|
||||
}
|
||||
if cfg.MkdirAll == nil {
|
||||
cfg.MkdirAll = os.MkdirAll
|
||||
}
|
||||
ss := &SessionManager{
|
||||
ss := &Manager{
|
||||
cfg: cfg,
|
||||
sessions: make(map[string]*Session),
|
||||
}
|
||||
|
|
@ -165,13 +165,13 @@ func NewSessionManager(cfg SessionManagerConfig) *SessionManager {
|
|||
}
|
||||
|
||||
// AddSession inserts a pre-built session into the store.
|
||||
func (s *SessionManager) AddSession(sess *Session) {
|
||||
func (s *Manager) AddSession(sess *Session) {
|
||||
s.mu.Lock()
|
||||
s.sessions[sess.RunnableID()] = sess
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *SessionManager) list() ([]os.DirEntry, error) {
|
||||
func (s *Manager) list() ([]os.DirEntry, error) {
|
||||
entries := make([]os.DirEntry, 0, len(sessionStoreOrder))
|
||||
for _, name := range sessionStoreOrder {
|
||||
entries = append(entries, store.FileEntry(name, sessionStoreFiles[name]))
|
||||
|
|
@ -185,7 +185,7 @@ func (s *SessionManager) list() ([]os.DirEntry, error) {
|
|||
}
|
||||
|
||||
// ListDir lists entries in a subdirectory (e.g. "{id}", "{id}/t", "{id}/t/{subdir}").
|
||||
func (s *SessionManager) ListDir(name string) ([]os.DirEntry, error) {
|
||||
func (s *Manager) ListDir(name string) ([]os.DirEntry, error) {
|
||||
parts := strings.SplitN(name, "/", 3)
|
||||
sessID := parts[0]
|
||||
sess := s.Session(sessID)
|
||||
|
|
@ -221,7 +221,7 @@ func (s *SessionManager) ListDir(name string) ([]os.DirEntry, error) {
|
|||
return nil, fmt.Errorf("%s: not a directory", name)
|
||||
}
|
||||
|
||||
func (s *SessionManager) stat(name string) (os.FileInfo, error) {
|
||||
func (s *Manager) stat(name string) (os.FileInfo, error) {
|
||||
// Top-level fixed files (new, idx, sh, etc.)
|
||||
if mode, ok := sessionStoreFiles[name]; ok {
|
||||
return &store.SyntheticFileInfo{Name_: name, Mode_: mode}, nil
|
||||
|
|
@ -257,7 +257,7 @@ func (s *SessionManager) stat(name string) (os.FileInfo, error) {
|
|||
return sfs.Stat(parts[1])
|
||||
}
|
||||
|
||||
func (s *SessionManager) openEntry(name string) (store.StoreEntry, error) {
|
||||
func (s *Manager) openEntry(name string) (store.StoreEntry, error) {
|
||||
notBlocking := func(context.Context, string) ([]byte, string, error) {
|
||||
return nil, "", fmt.Errorf("blocking read not supported")
|
||||
}
|
||||
|
|
@ -332,7 +332,7 @@ func (s *SessionManager) openEntry(name string) (store.StoreEntry, error) {
|
|||
}
|
||||
|
||||
|
||||
func (s *SessionManager) openFilteredToolIdx(allowed map[string]bool) (store.StoreEntry, error) {
|
||||
func (s *Manager) openFilteredToolIdx(allowed map[string]bool) (store.StoreEntry, error) {
|
||||
base, err := s.cfg.ToolStore.Open("idx")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -366,7 +366,7 @@ func filterToolIdx(data []byte, allowed map[string]bool) []byte {
|
|||
return out
|
||||
}
|
||||
|
||||
func (s *SessionManager) create(name string) error {
|
||||
func (s *Manager) create(name string) error {
|
||||
// Tool create: {id}/t/{rel}
|
||||
parts := strings.SplitN(name, "/", 3)
|
||||
if len(parts) == 3 && parts[1] == "t" && s.cfg.ToolStore != nil {
|
||||
|
|
@ -376,7 +376,7 @@ func (s *SessionManager) create(name string) error {
|
|||
}
|
||||
|
||||
// MkdirAll creates a directory within the session's tool store.
|
||||
func (s *SessionManager) MkdirAll(name string) error {
|
||||
func (s *Manager) MkdirAll(name string) error {
|
||||
parts := strings.SplitN(name, "/", 3)
|
||||
if len(parts) == 3 && parts[1] == "t" {
|
||||
type mkdirAller interface {
|
||||
|
|
@ -389,7 +389,7 @@ func (s *SessionManager) MkdirAll(name string) error {
|
|||
return fmt.Errorf("mkdir not supported: %s", name)
|
||||
}
|
||||
|
||||
func (s *SessionManager) del(name string) error {
|
||||
func (s *Manager) del(name string) error {
|
||||
parts := strings.SplitN(name, "/", 3)
|
||||
sessID := parts[0]
|
||||
// Delete session directory itself.
|
||||
|
|
@ -415,14 +415,14 @@ func (s *SessionManager) del(name string) error {
|
|||
}
|
||||
|
||||
// Session returns the session for the given ID, or nil.
|
||||
func (s *SessionManager) Session(id string) *Session {
|
||||
func (s *Manager) Session(id string) *Session {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.sessions[id]
|
||||
}
|
||||
|
||||
// SessionByUname returns the session with the given uname (principal), or nil.
|
||||
func (s *SessionManager) SessionByUname(uname string) *Session {
|
||||
func (s *Manager) SessionByUname(uname string) *Session {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, sess := range s.sessions {
|
||||
|
|
@ -434,16 +434,16 @@ func (s *SessionManager) SessionByUname(uname string) *Session {
|
|||
}
|
||||
|
||||
// OpenStore returns a DirStore for the given session ID.
|
||||
func (s *SessionManager) OpenStore(id string) (*store.DirStore, error) {
|
||||
func (s *Manager) OpenStore(id string) (*store.DirStore, error) {
|
||||
if sess := s.Session(id); sess != nil {
|
||||
return s.openStore(sess)
|
||||
}
|
||||
return nil, fmt.Errorf("session not found: %s", id)
|
||||
}
|
||||
|
||||
func (s *SessionManager) openStore(sess *Session) (*store.DirStore, error) {
|
||||
func (s *Manager) openStore(sess *Session) (*store.DirStore, error) {
|
||||
id := sess.RunnableID()
|
||||
return NewSessionFileStore(
|
||||
return NewFileStore(
|
||||
sess,
|
||||
s.cfg.Log,
|
||||
func() { s.KillSession(id) },
|
||||
|
|
@ -453,7 +453,7 @@ func (s *SessionManager) openStore(sess *Session) (*store.DirStore, error) {
|
|||
}
|
||||
|
||||
// InterruptAll interrupts every active session.
|
||||
func (s *SessionManager) InterruptAll() {
|
||||
func (s *Manager) InterruptAll() {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for _, sess := range s.sessions {
|
||||
|
|
@ -462,7 +462,7 @@ func (s *SessionManager) InterruptAll() {
|
|||
}
|
||||
|
||||
// Shutdown kills all active sessions.
|
||||
func (s *SessionManager) Shutdown() {
|
||||
func (s *Manager) Shutdown() {
|
||||
s.mu.Lock()
|
||||
ids := make([]string, 0, len(s.sessions))
|
||||
for id := range s.sessions {
|
||||
|
|
@ -474,7 +474,7 @@ func (s *SessionManager) Shutdown() {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *SessionManager) KillSession(id string) {
|
||||
func (s *Manager) KillSession(id string) {
|
||||
s.mu.Lock()
|
||||
sess := s.sessions[id]
|
||||
delete(s.sessions, id)
|
||||
|
|
@ -486,7 +486,7 @@ func (s *SessionManager) KillSession(id string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *SessionManager) index() []byte {
|
||||
func (s *Manager) index() []byte {
|
||||
var sb strings.Builder
|
||||
s.mu.RLock()
|
||||
ids := make([]string, 0, len(s.sessions))
|
||||
|
|
@ -508,7 +508,7 @@ func (s *SessionManager) index() []byte {
|
|||
return []byte(sb.String())
|
||||
}
|
||||
|
||||
func (s *SessionManager) createSession(args []string) error {
|
||||
func (s *Manager) createSession(args []string) error {
|
||||
name := ""
|
||||
backendOverride := ""
|
||||
modelOverride := ""
|
||||
|
|
@ -643,7 +643,7 @@ func (s *SessionManager) createSession(args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *SessionManager) renameSession(old, new string) error {
|
||||
func (s *Manager) renameSession(old, new string) error {
|
||||
// Tool rename: {id}/t/{oldRel} -> newName
|
||||
if strings.Contains(old, "/t/") {
|
||||
parts := strings.SplitN(old, "/", 3)
|
||||
|
|
@ -691,7 +691,7 @@ func (s *SessionManager) renameSession(old, new string) error {
|
|||
|
||||
// OnRename must be called without holding s.mu to avoid deadlock
|
||||
// with connection handlers that hold connState.mu and call into
|
||||
// SessionManager (e.g. SessionByUname during Tattach).
|
||||
// Manager (e.g. SessionByUname during Tattach).
|
||||
if s.cfg.OnRename != nil {
|
||||
s.cfg.OnRename(oldID, newID)
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -14,8 +14,8 @@ import (
|
|||
"olliesrv/store"
|
||||
)
|
||||
|
||||
// SessionFileList defines the fixed set of files in a session directory.
|
||||
var SessionFileList = []struct {
|
||||
// FileList defines the fixed set of files in a session directory.
|
||||
var FileList = []struct {
|
||||
Name string
|
||||
Mode os.FileMode
|
||||
OneShot bool
|
||||
|
|
@ -42,13 +42,13 @@ var SessionFileList = []struct {
|
|||
{"context", 0444, false, false},
|
||||
}
|
||||
|
||||
// SessionFileStore is a DirStore for a session directory.
|
||||
type SessionFileStore = store.DirStore
|
||||
// FileStore is a DirStore for a session directory.
|
||||
type FileStore = store.DirStore
|
||||
|
||||
func NewSessionFileStore(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error) *SessionFileStore {
|
||||
func NewFileStore(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error) *FileStore {
|
||||
h := &sessionHelper{sess: sess, log: log, kill: kill, rename: rename, saveTranscript: saveTranscript}
|
||||
specs := make([]store.FileSpec, len(SessionFileList))
|
||||
for i, f := range SessionFileList {
|
||||
specs := make([]store.FileSpec, len(FileList))
|
||||
for i, f := range FileList {
|
||||
specs[i] = h.fileSpec(f.Name, f.Mode)
|
||||
specs[i].OneShot = f.OneShot
|
||||
specs[i].Async = f.Async
|
||||
Reference in New Issue