9p: add embedded D-Bus protocol adapter
Expose org.ollie.SessionManager on the session bus directly from olliesrv.
The adapter emits StateChanged, ChatUpdated, SessionCreated, SessionKilled,
SessionRenamed, ProcessDetached, and ProcessExited signals. All KDE frontends
work without ollied.
Fixes:
- ListSessions: use Manager.List() instead of broken Readdir("")
- RenameSession: delegate to Manager.RenameSession()
- WatchExisting: start signal watchers for sessions restored at startup
- Introspection XML: declare all methods and signals
This commit is contained in:
parent
6a9ea19abe
commit
f065429d24
|
|
@ -0,0 +1,601 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/godbus/dbus/v5/introspect"
|
||||
|
||||
"ollie/pkg/agent"
|
||||
"ollie/pkg/backend"
|
||||
"olliesrv/session"
|
||||
)
|
||||
|
||||
const (
|
||||
busName = "org.ollie.SessionManager"
|
||||
busPath = "/org/ollie/SessionManager"
|
||||
busIface = "org.ollie.SessionManager"
|
||||
)
|
||||
|
||||
// DBusAdapter exposes a session.Manager over D-Bus.
|
||||
type DBusAdapter struct {
|
||||
conn *dbus.Conn
|
||||
mgr *session.Manager
|
||||
|
||||
mu sync.RWMutex
|
||||
watchers map[string]context.CancelFunc // sessionID -> cancel for state watcher
|
||||
}
|
||||
|
||||
// startDBus connects to the session bus, claims the well-known name, and
|
||||
// exports the adapter. Returns nil (no-op) if the bus is unavailable.
|
||||
func startDBus(mgr *session.Manager) *DBusAdapter {
|
||||
conn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "dbus: session bus unavailable: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
reply, err := conn.RequestName(busName, dbus.NameFlagDoNotQueue)
|
||||
if err != nil || reply != dbus.RequestNameReplyPrimaryOwner {
|
||||
fmt.Fprintf(os.Stderr, "dbus: cannot claim %s (ollied already running?)\n", busName)
|
||||
conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
a := &DBusAdapter{
|
||||
conn: conn,
|
||||
mgr: mgr,
|
||||
watchers: make(map[string]context.CancelFunc),
|
||||
}
|
||||
|
||||
conn.Export(a, busPath, busIface)
|
||||
conn.Export(introspect.Introspectable(introspectXML), busPath, "org.freedesktop.DBus.Introspectable")
|
||||
|
||||
fmt.Printf("dbus: exported %s\n", busName)
|
||||
return a
|
||||
}
|
||||
|
||||
// WatchExisting starts watchers for all sessions that already exist.
|
||||
// Called after the adapter is initialized to cover sessions restored at startup.
|
||||
func (a *DBusAdapter) WatchExisting() {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
entries, _ := a.mgr.List()
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
id := e.Name()
|
||||
sess := a.mgr.Session(id)
|
||||
if sess == nil {
|
||||
continue
|
||||
}
|
||||
a.startWatcher(id, sess)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) Close() {
|
||||
if a == nil || a.conn == nil {
|
||||
return
|
||||
}
|
||||
a.mu.Lock()
|
||||
for _, cancel := range a.watchers {
|
||||
cancel()
|
||||
}
|
||||
a.mu.Unlock()
|
||||
a.conn.Close()
|
||||
}
|
||||
|
||||
// --- Lifecycle callbacks (called by session.Manager) ---
|
||||
|
||||
func (a *DBusAdapter) OnSessionCreated(id string, sess *session.Session) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
a.startWatcher(id, sess)
|
||||
go a.conn.Emit(busPath, busIface+".SessionCreated", id)
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) OnSessionKilled(id string) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
a.stopWatcher(id)
|
||||
go a.conn.Emit(busPath, busIface+".SessionKilled", id)
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) OnSessionRenamed(oldID, newID string) {
|
||||
if a == nil {
|
||||
return
|
||||
}
|
||||
a.stopWatcher(oldID)
|
||||
// The session object is the same, just re-lookup
|
||||
sess := a.mgr.Session(newID)
|
||||
if sess != nil {
|
||||
a.startWatcher(newID, sess)
|
||||
}
|
||||
go a.conn.Emit(busPath, busIface+".SessionRenamed", oldID, newID)
|
||||
}
|
||||
|
||||
// startWatcher launches goroutines for StateChanged and ChatUpdated signals.
|
||||
func (a *DBusAdapter) startWatcher(id string, sess *session.Session) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
a.mu.Lock()
|
||||
a.watchers[id] = cancel
|
||||
a.mu.Unlock()
|
||||
|
||||
// State watcher
|
||||
go func() {
|
||||
current := sess.Core.State()
|
||||
for {
|
||||
next, ok := sess.Core.WaitChange(ctx, agent.WatchState, current)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
current = next
|
||||
a.conn.Emit(busPath, busIface+".StateChanged", id, current)
|
||||
if current == "idle" {
|
||||
a.sendNotification("Ollie", "Session "+id+" finished")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Chat watcher (polls log version)
|
||||
go func() {
|
||||
var lastLen int
|
||||
var lastVers uint32
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(150 * time.Millisecond):
|
||||
}
|
||||
l, v := sess.LogInfo()
|
||||
if v == lastVers {
|
||||
continue
|
||||
}
|
||||
if l > lastLen {
|
||||
// Read the new chunk
|
||||
sess.Mu().RLock()
|
||||
chunk := string(sess.Log()[lastLen:])
|
||||
sess.Mu().RUnlock()
|
||||
a.conn.Emit(busPath, busIface+".ChatUpdated", id, int64(lastLen), chunk)
|
||||
}
|
||||
lastLen = l
|
||||
lastVers = v
|
||||
}
|
||||
}()
|
||||
|
||||
// Detached process exit watcher
|
||||
go func() {
|
||||
exitedPIDs := make(map[int]bool) // already-reported PIDs
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(1 * time.Second):
|
||||
}
|
||||
infos := sess.Core.ListDetached()
|
||||
// Clean up stale entries from exitedPIDs
|
||||
activePIDs := make(map[int]bool, len(infos))
|
||||
for _, info := range infos {
|
||||
activePIDs[info.PID] = true
|
||||
}
|
||||
for pid := range exitedPIDs {
|
||||
if !activePIDs[pid] {
|
||||
delete(exitedPIDs, pid)
|
||||
}
|
||||
}
|
||||
for _, info := range infos {
|
||||
if info.Exited && !exitedPIDs[info.PID] {
|
||||
exitedPIDs[info.PID] = true
|
||||
a.conn.Emit(busPath, busIface+".ProcessExited", id, int32(info.PID), int32(info.ExitCode))
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) stopWatcher(id string) {
|
||||
a.mu.Lock()
|
||||
if cancel, ok := a.watchers[id]; ok {
|
||||
cancel()
|
||||
delete(a.watchers, id)
|
||||
}
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
// --- D-Bus exported methods ---
|
||||
|
||||
func (a *DBusAdapter) CreateSession(cwd, backendName, modelName, agentName string) (string, *dbus.Error) {
|
||||
var args []string
|
||||
if cwd != "" {
|
||||
args = append(args, "cwd="+cwd)
|
||||
}
|
||||
if backendName != "" {
|
||||
args = append(args, "backend="+backendName)
|
||||
}
|
||||
if modelName != "" {
|
||||
args = append(args, "model="+modelName)
|
||||
}
|
||||
if agentName != "" {
|
||||
args = append(args, "agent="+agentName)
|
||||
}
|
||||
if cwd == "" {
|
||||
dir, _ := os.Getwd()
|
||||
args = append(args, "cwd="+dir)
|
||||
}
|
||||
id, err := a.mgr.CreateSession(args)
|
||||
if err != nil {
|
||||
return "", dbus.MakeFailedError(err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) ListSessions() ([]string, *dbus.Error) {
|
||||
entries, _ := a.mgr.List()
|
||||
var result []string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
id := e.Name()
|
||||
sess := a.mgr.Session(id)
|
||||
if sess == nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, fmt.Sprintf("%s\t%s\t%s\t%s",
|
||||
id, sess.Core.State(), sess.Core.ModelName(), sess.Core.AgentName()))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) KillSession(sessionID string) (bool, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return false, nil
|
||||
}
|
||||
a.mgr.KillSession(sessionID)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) RenameSession(sessionID, newName string) (bool, *dbus.Error) {
|
||||
if err := a.mgr.RenameSession(sessionID, newName); err != nil {
|
||||
return false, dbus.MakeFailedError(err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) Submit(sessionID, prompt string) (bool, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return false, nil
|
||||
}
|
||||
go sess.Core.Submit(sess.Ctx, prompt)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) Interrupt(sessionID string) (bool, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return false, nil
|
||||
}
|
||||
sess.Core.Interrupt(agent.ErrInterrupted)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) GetState(sessionID string) (string, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return "", nil
|
||||
}
|
||||
return sess.Core.State(), nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) GetChat(sessionID string, offset int64) (string, int64, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return "", offset, nil
|
||||
}
|
||||
sess.Mu().RLock()
|
||||
log := sess.Log()
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
if offset > int64(len(log)) {
|
||||
offset = int64(len(log))
|
||||
}
|
||||
text := string(log[offset:])
|
||||
newOffset := int64(len(log))
|
||||
sess.Mu().RUnlock()
|
||||
return text, newOffset, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) GetUsage(sessionID string) (string, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return "", nil
|
||||
}
|
||||
return sess.Core.Usage(), nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) GetCost(sessionID string) (string, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return "", nil
|
||||
}
|
||||
return sess.Core.Cost(), nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) GetConfig(sessionID string) (string, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return "", nil
|
||||
}
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "model=%s\n", sess.Core.ModelName())
|
||||
fmt.Fprintf(&sb, "backend=%s\n", sess.Core.BackendName())
|
||||
fmt.Fprintf(&sb, "agent=%s\n", sess.Core.AgentName())
|
||||
fmt.Fprintf(&sb, "cwd=%s\n", sess.Core.CWD())
|
||||
fmt.Fprintf(&sb, "state=%s\n", sess.Core.State())
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) SetConfig(sessionID, key, value string) (bool, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return false, nil
|
||||
}
|
||||
switch key {
|
||||
case "model":
|
||||
sess.Core.Submit(sess.Ctx, "/model "+value)
|
||||
case "cwd":
|
||||
if err := sess.Core.SetCWD(value); err != nil {
|
||||
return false, nil
|
||||
}
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) GetContext(sessionID string) (string, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return "", nil
|
||||
}
|
||||
msgs := sess.Core.Context()
|
||||
var sb strings.Builder
|
||||
enc := json.NewEncoder(&sb)
|
||||
enc.SetEscapeHTML(false)
|
||||
for _, msg := range msgs {
|
||||
enc.Encode(msg)
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) ListBackends() ([]string, *dbus.Error) {
|
||||
return backend.Backends(), nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) ListModels(sessionID string) ([]string, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return nil, nil
|
||||
}
|
||||
raw := sess.CachedListModels()
|
||||
models := strings.Split(strings.TrimSpace(raw), "\n")
|
||||
if len(models) == 1 && models[0] == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) ListAgents() ([]string, *dbus.Error) {
|
||||
var result []string
|
||||
for _, dir := range agent.AgentsDirs() {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if strings.HasSuffix(name, ".json") {
|
||||
result = append(result, strings.TrimSuffix(name, ".json"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// --- Peers ---
|
||||
|
||||
func (a *DBusAdapter) PeerAdd(sessionID, peerID string) (bool, *dbus.Error) {
|
||||
if err := a.mgr.PeerAdd(sessionID, peerID); err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) PeerRemove(sessionID, peerID string) (bool, *dbus.Error) {
|
||||
if err := a.mgr.PeerRemove(sessionID, peerID); err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) PeerList(sessionID string) ([]string, *dbus.Error) {
|
||||
peers, err := a.mgr.PeerList(sessionID)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return peers, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) PeerSubmit(sessionID, peerID, prompt string) (bool, *dbus.Error) {
|
||||
if err := a.mgr.PeerSubmit(sessionID, peerID, prompt); err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// --- Detached processes ---
|
||||
|
||||
func (a *DBusAdapter) DetachProcess(sessionID string) (bool, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return false, dbus.MakeFailedError(fmt.Errorf("session not found: %s", sessionID))
|
||||
}
|
||||
if !sess.Core.Detach() {
|
||||
return false, dbus.MakeFailedError(fmt.Errorf("no running process to detach"))
|
||||
}
|
||||
a.conn.Emit(busPath, busIface+".ProcessDetached", sessionID)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) ListDetached(sessionID string) ([]string, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return nil, dbus.MakeFailedError(fmt.Errorf("session not found: %s", sessionID))
|
||||
}
|
||||
infos := sess.Core.ListDetached()
|
||||
out := make([]string, len(infos))
|
||||
for i, info := range infos {
|
||||
status := "running"
|
||||
if info.Exited {
|
||||
status = fmt.Sprintf("exited(%d)", info.ExitCode)
|
||||
}
|
||||
out[i] = fmt.Sprintf("%d\t%s\t%d\t%s", info.PID, info.Command, info.Started, status)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) SignalDetached(sessionID string, pid int32, signal string) (bool, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return false, dbus.MakeFailedError(fmt.Errorf("session not found: %s", sessionID))
|
||||
}
|
||||
var sig int
|
||||
switch strings.ToUpper(signal) {
|
||||
case "TERM", "SIGTERM", "15":
|
||||
sig = 15
|
||||
case "KILL", "SIGKILL", "9":
|
||||
sig = 9
|
||||
default:
|
||||
return false, dbus.MakeFailedError(fmt.Errorf("unsupported signal: %s", signal))
|
||||
}
|
||||
if err := sess.Core.SignalDetached(int(pid), sig); err != nil {
|
||||
return false, dbus.MakeFailedError(err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) GetDetachedOutput(sessionID string, pid int32) (string, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return "", dbus.MakeFailedError(fmt.Errorf("session not found: %s", sessionID))
|
||||
}
|
||||
output, err := sess.Core.GetDetachedOutput(int(pid))
|
||||
if err != nil {
|
||||
return "", dbus.MakeFailedError(err)
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) DismissDetached(sessionID string, pid int32) (bool, *dbus.Error) {
|
||||
sess := a.mgr.Session(sessionID)
|
||||
if sess == nil {
|
||||
return false, dbus.MakeFailedError(fmt.Errorf("session not found: %s", sessionID))
|
||||
}
|
||||
return sess.Core.DismissDetached(int(pid)), nil
|
||||
}
|
||||
|
||||
// --- Generate / Route ---
|
||||
|
||||
func (a *DBusAdapter) Generate(prompt, system, backendName, model string) (string, *dbus.Error) {
|
||||
result, err := backend.Generate(context.Background(), backend.GenerateRequest{
|
||||
Prompt: prompt,
|
||||
System: system,
|
||||
Backend: backendName,
|
||||
Model: model,
|
||||
})
|
||||
if err != nil {
|
||||
return "", dbus.MakeFailedError(err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *DBusAdapter) Route(task, backendName string) (string, *dbus.Error) {
|
||||
result, err := backend.Route(context.Background(), backend.RouteRequest{
|
||||
Task: task,
|
||||
Backend: backendName,
|
||||
})
|
||||
if err != nil {
|
||||
return "", dbus.MakeFailedError(err)
|
||||
}
|
||||
return fmt.Sprintf("backend=%s model=%s", result.Backend, result.Model), nil
|
||||
}
|
||||
|
||||
// --- Notifications ---
|
||||
|
||||
func (a *DBusAdapter) sendNotification(title, body string) {
|
||||
go func() {
|
||||
obj := a.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
|
||||
obj.Call("org.freedesktop.Notifications.Notify", 0,
|
||||
"olliesrv", uint32(0), "system-run", title, body,
|
||||
[]string{}, map[string]dbus.Variant{}, int32(5000))
|
||||
}()
|
||||
}
|
||||
|
||||
// Introspection XML.
|
||||
var introspectXML = `<node>
|
||||
<interface name="` + busIface + `">
|
||||
<method name="CreateSession">
|
||||
<arg direction="in" type="s" name="cwd"/>
|
||||
<arg direction="in" type="s" name="backend"/>
|
||||
<arg direction="in" type="s" name="model"/>
|
||||
<arg direction="in" type="s" name="agent"/>
|
||||
<arg direction="out" type="s"/>
|
||||
</method>
|
||||
<method name="ListSessions"><arg direction="out" type="as"/></method>
|
||||
<method name="KillSession"><arg direction="in" type="s" name="sessionID"/><arg direction="out" type="b"/></method>
|
||||
<method name="RenameSession"><arg direction="in" type="s" name="sessionID"/><arg direction="in" type="s" name="newName"/><arg direction="out" type="b"/></method>
|
||||
<method name="Submit"><arg direction="in" type="s" name="sessionID"/><arg direction="in" type="s" name="prompt"/><arg direction="out" type="b"/></method>
|
||||
<method name="Interrupt"><arg direction="in" type="s" name="sessionID"/><arg direction="out" type="b"/></method>
|
||||
<method name="GetState"><arg direction="in" type="s" name="sessionID"/><arg direction="out" type="s"/></method>
|
||||
<method name="GetChat"><arg direction="in" type="s" name="sessionID"/><arg direction="in" type="x" name="offset"/><arg direction="out" type="s"/><arg direction="out" type="x"/></method>
|
||||
<method name="GetUsage"><arg direction="in" type="s" name="sessionID"/><arg direction="out" type="s"/></method>
|
||||
<method name="GetCost"><arg direction="in" type="s" name="sessionID"/><arg direction="out" type="s"/></method>
|
||||
<method name="GetConfig"><arg direction="in" type="s" name="sessionID"/><arg direction="out" type="s"/></method>
|
||||
<method name="SetConfig"><arg direction="in" type="s" name="sessionID"/><arg direction="in" type="s" name="key"/><arg direction="in" type="s" name="value"/><arg direction="out" type="b"/></method>
|
||||
<method name="GetContext"><arg direction="in" type="s" name="sessionID"/><arg direction="out" type="s"/></method>
|
||||
<method name="ListBackends"><arg direction="out" type="as"/></method>
|
||||
<method name="ListModels"><arg direction="in" type="s" name="sessionID"/><arg direction="out" type="as"/></method>
|
||||
<method name="ListAgents"><arg direction="out" type="as"/></method>
|
||||
<method name="PeerAdd"><arg direction="in" type="s" name="sessionID"/><arg direction="in" type="s" name="peerID"/><arg direction="out" type="b"/></method>
|
||||
<method name="PeerRemove"><arg direction="in" type="s" name="sessionID"/><arg direction="in" type="s" name="peerID"/><arg direction="out" type="b"/></method>
|
||||
<method name="PeerList"><arg direction="in" type="s" name="sessionID"/><arg direction="out" type="as"/></method>
|
||||
<method name="PeerSubmit"><arg direction="in" type="s" name="sessionID"/><arg direction="in" type="s" name="peerID"/><arg direction="in" type="s" name="prompt"/><arg direction="out" type="b"/></method>
|
||||
<method name="DetachProcess"><arg direction="in" type="s" name="sessionID"/><arg direction="out" type="b"/></method>
|
||||
<method name="ListDetached"><arg direction="in" type="s" name="sessionID"/><arg direction="out" type="as"/></method>
|
||||
<method name="SignalDetached"><arg direction="in" type="s" name="sessionID"/><arg direction="in" type="i" name="pid"/><arg direction="in" type="s" name="signal"/><arg direction="out" type="b"/></method>
|
||||
<method name="GetDetachedOutput"><arg direction="in" type="s" name="sessionID"/><arg direction="in" type="i" name="pid"/><arg direction="out" type="s"/></method>
|
||||
<method name="DismissDetached"><arg direction="in" type="s" name="sessionID"/><arg direction="in" type="i" name="pid"/><arg direction="out" type="b"/></method>
|
||||
<method name="Generate"><arg direction="in" type="s" name="prompt"/><arg direction="in" type="s" name="system"/><arg direction="in" type="s" name="backend"/><arg direction="in" type="s" name="model"/><arg direction="out" type="s"/></method>
|
||||
<method name="Route"><arg direction="in" type="s" name="task"/><arg direction="in" type="s" name="backend"/><arg direction="out" type="s"/></method>
|
||||
<signal name="SessionCreated"><arg type="s" name="sessionID"/></signal>
|
||||
<signal name="SessionKilled"><arg type="s" name="sessionID"/></signal>
|
||||
<signal name="SessionRenamed"><arg type="s" name="oldID"/><arg type="s" name="newID"/></signal>
|
||||
<signal name="StateChanged"><arg type="s" name="sessionID"/><arg type="s" name="state"/></signal>
|
||||
<signal name="ChatUpdated"><arg type="s" name="sessionID"/><arg type="x" name="offset"/><arg type="s" name="text"/></signal>
|
||||
<signal name="ProcessDetached"><arg type="s" name="sessionID"/></signal>
|
||||
<signal name="ProcessExited"><arg type="s" name="sessionID"/><arg type="i" name="pid"/><arg type="i" name="exitCode"/></signal>
|
||||
</interface>
|
||||
</node>`
|
||||
1
go.mod
1
go.mod
|
|
@ -9,6 +9,7 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/simonfxr/pubsub v0.0.5 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -4,6 +4,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7
|
|||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/hanwen/go-fuse/v2 v2.10.1 h1:QAqZuc9+aBtTou+OPruU/hkYQYCkgPtQd2QaepHkTTs=
|
||||
github.com/hanwen/go-fuse/v2 v2.10.1/go.mod h1:aU7NkGYZUmuJrZapoI3mEcNve7PZTySUOLBuch/vR6U=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
|
|
|
|||
25
main.go
25
main.go
|
|
@ -226,6 +226,9 @@ func runServer(sockPath, pidPath string) {
|
|||
agentsDirs := agent.AgentsDirs()
|
||||
sessionsDir := paths.CfgDir() + "/sessions"
|
||||
|
||||
// D-Bus adapter (initialized after manager so callbacks can reference it).
|
||||
var dbusAdapter *DBusAdapter
|
||||
|
||||
mgr := session.NewManager(session.ManagerConfig{
|
||||
AgentsDir: agentsDirs[0],
|
||||
SessionsDir: sessionsDir,
|
||||
|
|
@ -235,6 +238,21 @@ func runServer(sockPath, pidPath string) {
|
|||
Yolo: *yolo,
|
||||
NoMount: *tcpAddr != "",
|
||||
InvalidateModels: modelCache.Invalidate,
|
||||
OnSessionCreated: func(id string, sess *session.Session) {
|
||||
if dbusAdapter != nil {
|
||||
dbusAdapter.OnSessionCreated(id, sess)
|
||||
}
|
||||
},
|
||||
OnSessionKilled: func(id string) {
|
||||
if dbusAdapter != nil {
|
||||
dbusAdapter.OnSessionKilled(id)
|
||||
}
|
||||
},
|
||||
OnSessionRenamed: func(oldID, newID string) {
|
||||
if dbusAdapter != nil {
|
||||
dbusAdapter.OnSessionRenamed(oldID, newID)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
srv := server.New(server.Config{
|
||||
|
|
@ -244,6 +262,10 @@ func runServer(sockPath, pidPath string) {
|
|||
InvalidateModels: modelCache.Invalidate,
|
||||
})
|
||||
|
||||
// Start D-Bus adapter (best-effort; pure 9P still works without it).
|
||||
dbusAdapter = startDBus(mgr)
|
||||
dbusAdapter.WatchExisting()
|
||||
|
||||
// Prefetch models asynchronously at startup
|
||||
go modelCache.Refresh()
|
||||
|
||||
|
|
@ -290,6 +312,9 @@ func runServer(sockPath, pidPath string) {
|
|||
|
||||
srv.InterruptAll()
|
||||
fmt.Println("shutting down")
|
||||
if dbusAdapter != nil {
|
||||
dbusAdapter.Close()
|
||||
}
|
||||
srv.Shutdown()
|
||||
listener.Close() //nolint:errcheck
|
||||
if tcpListener != nil {
|
||||
|
|
|
|||
|
|
@ -158,6 +158,12 @@ func (sess *Session) LogInfo() (length int, vers uint32) {
|
|||
return len(sess.log), sess.logVers
|
||||
}
|
||||
|
||||
// Mu returns the session's mutex for external synchronization (e.g. D-Bus adapter).
|
||||
func (sess *Session) Mu() *sync.RWMutex { return &sess.mu }
|
||||
|
||||
// Log returns the raw chat log bytes. Caller must hold Mu().RLock().
|
||||
func (sess *Session) Log() []byte { return sess.log }
|
||||
|
||||
|
||||
|
||||
var sessionStoreOrder = []string{"new", "idx", "ls", "kill", "sh", "b", "bfg", "bbg", "cleanup"}
|
||||
|
|
@ -188,6 +194,13 @@ type ManagerConfig struct {
|
|||
NoMount bool
|
||||
// InvalidateModels clears the model cache, forcing a refresh.
|
||||
InvalidateModels func()
|
||||
// OnSessionCreated is called after a new session is added to the manager.
|
||||
// Receives the session ID and the Session pointer.
|
||||
OnSessionCreated func(id string, sess *Session)
|
||||
// OnSessionKilled is called after a session is removed from the manager.
|
||||
OnSessionKilled func(id string)
|
||||
// OnSessionRenamed is called after a session is renamed.
|
||||
OnSessionRenamed func(oldID, newID string)
|
||||
}
|
||||
|
||||
// Manager manages session lifecycle and exposes sessions as a Tree.
|
||||
|
|
@ -238,6 +251,11 @@ func (s *Manager) AddSession(sess *Session) {
|
|||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// List returns all root-level entries (fixed files + session directories).
|
||||
func (s *Manager) List() ([]os.DirEntry, error) {
|
||||
return s.list()
|
||||
}
|
||||
|
||||
func (s *Manager) list() ([]os.DirEntry, error) {
|
||||
entries := make([]os.DirEntry, 0, len(sessionStoreOrder))
|
||||
for _, name := range sessionStoreOrder {
|
||||
|
|
@ -504,7 +522,8 @@ func (s *Manager) openEntry(name string) (fs.File, error) {
|
|||
return []byte("name=\ncwd=\nbackend=\nmodel=\nagent=\nmaxTokens=\nmaxCompletionTokens=\ntemperature=\ntopP=\ntopK=\nminP=\ntopA=\nfrequencyPenalty=\npresencePenalty=\nrepetitionPenalty=\nreasoning=\nreasoningEffort=\nincludeReasoning=\nresponseFormat=\nstop=\nverbosity=\n"), nil
|
||||
},
|
||||
WriteFn: func(data []byte) error {
|
||||
return s.createSession(strings.Fields(strings.TrimSpace(string(data))))
|
||||
_, err := s.CreateSession(strings.Fields(strings.TrimSpace(string(data))))
|
||||
return err
|
||||
},
|
||||
BlockingReadFn: notBlocking,
|
||||
}, nil
|
||||
|
|
@ -791,6 +810,9 @@ func (s *Manager) restoreSession(ps *agent.PersistedSession) error {
|
|||
s.mu.Unlock()
|
||||
|
||||
s.cfg.Log.Info("restored session %s (backend=%s model=%s agent=%s)", sessID, backendName, modelName, agentName)
|
||||
if s.cfg.OnSessionCreated != nil {
|
||||
s.cfg.OnSessionCreated(sessID, sess)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -825,6 +847,9 @@ func (s *Manager) KillSession(id string) {
|
|||
sess.Core.Close()
|
||||
s.removePersistedSession(id)
|
||||
s.cfg.Log.Info("killed session %s", id)
|
||||
if s.cfg.OnSessionKilled != nil {
|
||||
s.cfg.OnSessionKilled(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -851,7 +876,9 @@ func (s *Manager) index() []byte {
|
|||
return []byte(sb.String())
|
||||
}
|
||||
|
||||
func (s *Manager) createSession(args []string) error {
|
||||
// CreateSession creates a new agent session from key=value args.
|
||||
// Returns the session ID on success.
|
||||
func (s *Manager) CreateSession(args []string) (string, error) {
|
||||
name := ""
|
||||
backendOverride := ""
|
||||
modelOverride := ""
|
||||
|
|
@ -860,7 +887,7 @@ func (s *Manager) createSession(args []string) error {
|
|||
for _, arg := range args {
|
||||
k, v, ok := strings.Cut(arg, "=")
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid option %q (expected key=value)", arg)
|
||||
return "", fmt.Errorf("invalid option %q (expected key=value)", arg)
|
||||
}
|
||||
if v == "" {
|
||||
continue
|
||||
|
|
@ -877,7 +904,7 @@ func (s *Manager) createSession(args []string) error {
|
|||
case "cwd":
|
||||
cwd = v
|
||||
default:
|
||||
return fmt.Errorf("unknown option %q (valid: name, backend, model, agent, cwd)", k)
|
||||
return "", fmt.Errorf("unknown option %q (valid: name, backend, model, agent, cwd)", k)
|
||||
}
|
||||
}
|
||||
if agentName == "default" {
|
||||
|
|
@ -888,7 +915,7 @@ func (s *Manager) createSession(args []string) error {
|
|||
|
||||
cwd = paths.ExpandHome(os.ExpandEnv(cwd))
|
||||
if cwd == "" {
|
||||
return fmt.Errorf("cwd is required (e.g. new cwd=/path/to/project)")
|
||||
return "", fmt.Errorf("cwd is required (e.g. new cwd=/path/to/project)")
|
||||
}
|
||||
|
||||
sessID := name
|
||||
|
|
@ -900,7 +927,7 @@ func (s *Manager) createSession(args []string) error {
|
|||
_, exists := s.sessions[sessID]
|
||||
s.mu.RUnlock()
|
||||
if exists {
|
||||
return fmt.Errorf("session already exists: %s", sessID)
|
||||
return "", fmt.Errorf("session already exists: %s", sessID)
|
||||
}
|
||||
|
||||
var core agent.Core
|
||||
|
|
@ -910,7 +937,7 @@ func (s *Manager) createSession(args []string) error {
|
|||
var err error
|
||||
core, err = s.cfg.NewCore(sessID, agentName, cwd)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
cfg := LoadAgentConfig(s.cfg.AgentsDir, agentName, nil)
|
||||
|
|
@ -925,7 +952,7 @@ func (s *Manager) createSession(args []string) error {
|
|||
|
||||
be, err := backend.NewWithName(backendOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("backend: %w", err)
|
||||
return "", fmt.Errorf("backend: %w", err)
|
||||
}
|
||||
|
||||
if modelOverride == "" {
|
||||
|
|
@ -936,7 +963,7 @@ func (s *Manager) createSession(args []string) error {
|
|||
}
|
||||
|
||||
if err := s.cfg.MkdirAll(s.cfg.SessionsDir, fs.PermMkdirPrivate); err != nil {
|
||||
return fmt.Errorf("sessions dir: %w", err)
|
||||
return "", fmt.Errorf("sessions dir: %w", err)
|
||||
}
|
||||
|
||||
var execOpts []execute.Option
|
||||
|
|
@ -1000,7 +1027,15 @@ func (s *Manager) createSession(args []string) error {
|
|||
|
||||
s.cfg.Log.Info("new session %s (backend=%s model=%s agent=%s)",
|
||||
sessID, core.BackendName(), core.ModelName(), core.AgentName())
|
||||
return nil
|
||||
if s.cfg.OnSessionCreated != nil {
|
||||
s.cfg.OnSessionCreated(sessID, sess)
|
||||
}
|
||||
return sessID, nil
|
||||
}
|
||||
|
||||
// RenameSession renames a session (exported for D-Bus adapter).
|
||||
func (s *Manager) RenameSession(old, new string) error {
|
||||
return s.renameSession(old, new)
|
||||
}
|
||||
|
||||
func (s *Manager) renameSession(old, new string) error {
|
||||
|
|
@ -1035,6 +1070,9 @@ func (s *Manager) renameSession(old, new string) error {
|
|||
|
||||
sess.AppendLog([]byte(fmt.Sprintf(":: session renamed: %s -> %s\n", oldID, newID)))
|
||||
s.cfg.Log.Info("renamed session %s -> %s", oldID, newID)
|
||||
if s.cfg.OnSessionRenamed != nil {
|
||||
s.cfg.OnSessionRenamed(oldID, newID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -1212,6 +1250,9 @@ func (s *Manager) findOrCreateCopilot(cwd, backendName, modelName string) string
|
|||
s.mu.Lock()
|
||||
s.sessions[sessID] = sess
|
||||
s.mu.Unlock()
|
||||
if s.cfg.OnSessionCreated != nil {
|
||||
s.cfg.OnSessionCreated(sessID, sess)
|
||||
}
|
||||
return sessID
|
||||
}
|
||||
|
||||
|
|
|
|||
Reference in New Issue