diff --git a/pkg/agent/core.go b/pkg/agent/core.go index 01c214e..d76c00f 100644 --- a/pkg/agent/core.go +++ b/pkg/agent/core.go @@ -15,6 +15,7 @@ import ( "strings" "sync" "sync/atomic" + "syscall" "time" "github.com/simonfxr/pubsub" @@ -460,6 +461,85 @@ func (s *agent) Close() { } } +// execServer returns the execute server if available, or nil. +func (s *agent) execServer() interface{} { + if s.runtime == nil || s.runtime.Dispatcher == nil { + return nil + } + srv, _ := s.runtime.Dispatcher.GetServer("execute") + return srv +} + +func (s *agent) Detach() bool { + if srv := s.execServer(); srv != nil { + if d, ok := srv.(interface{ Detach() bool }); ok { + return d.Detach() + } + } + return false +} + +func (s *agent) ListDetached() []DetachedInfo { + if srv := s.execServer(); srv != nil { + type listDetacher interface { + ListDetachedRaw() []any + } + if ld, ok := srv.(listDetacher); ok { + raw := ld.ListDetachedRaw() + out := make([]DetachedInfo, 0, len(raw)) + for _, r := range raw { + if m, ok := r.(map[string]any); ok { + di := DetachedInfo{} + if v, ok := m["pid"].(int); ok { di.PID = v } + if v, ok := m["command"].(string); ok { di.Command = v } + if v, ok := m["started"].(int64); ok { di.Started = v } + if v, ok := m["exited"].(bool); ok { di.Exited = v } + if v, ok := m["exit_code"].(int); ok { di.ExitCode = v } + out = append(out, di) + } + } + return out + } + } + return nil +} + +func (s *agent) SignalDetached(pid, signal int) error { + if srv := s.execServer(); srv != nil { + type signaler interface { + SignalDetached(int, syscall.Signal) error + } + if sg, ok := srv.(signaler); ok { + return sg.SignalDetached(pid, syscall.Signal(signal)) + } + } + return fmt.Errorf("no execute server available") +} + +func (s *agent) GetDetachedOutput(pid int) (string, error) { + if srv := s.execServer(); srv != nil { + type outputGetter interface { + GetDetachedOutput(int) (string, error) + } + if og, ok := srv.(outputGetter); ok { + return og.GetDetachedOutput(pid) + } + } + return "", fmt.Errorf("no execute server available") +} + +func (s *agent) DismissDetached(pid int) bool { + if srv := s.execServer(); srv != nil { + type dismisser interface { + DismissDetached(int) bool + } + if d, ok := srv.(dismisser); ok { + return d.DismissDetached(pid) + } + } + return false +} + func (s *agent) AgentName() string { v := s.agentName s.log.Debug("AgentName() = %q", v) diff --git a/pkg/agent/types.go b/pkg/agent/types.go index 3ffeb44..50feaf2 100644 --- a/pkg/agent/types.go +++ b/pkg/agent/types.go @@ -146,6 +146,31 @@ type Core interface { // Close releases resources associated with the session, including its // temporary directory under /tmp/ollie/. Close() + + // Detach detaches the currently running process from the agent. + // The process continues running; returns false if nothing is executing. + Detach() bool + + // ListDetached returns info about all detached processes. + ListDetached() []DetachedInfo + + // SignalDetached sends a signal to a detached process by PID. + SignalDetached(pid, signal int) error + + // GetDetachedOutput returns the ring buffer output for a detached process. + GetDetachedOutput(pid int) (string, error) + + // DismissDetached removes an exited process from the list. + DismissDetached(pid int) bool +} + +// DetachedInfo describes a detached process for external consumers. +type DetachedInfo struct { + PID int + Command string + Started int64 // unix timestamp + Exited bool + ExitCode int } // PromptFIFO is a simple thread-safe FIFO for queued prompts. diff --git a/pkg/tools/execute/detach.go b/pkg/tools/execute/detach.go index 077b4a6..6ab19ec 100644 --- a/pkg/tools/execute/detach.go +++ b/pkg/tools/execute/detach.go @@ -25,6 +25,27 @@ type DetachedProcess struct { mu sync.Mutex } +// Info returns a plain-data snapshot of this process for external consumers. +type DetachedInfoData struct { + PID int + Command string + Started int64 // unix timestamp + Exited bool + ExitCode int +} + +func (p *DetachedProcess) Info() DetachedInfoData { + p.mu.Lock() + defer p.mu.Unlock() + return DetachedInfoData{ + PID: p.PID, + Command: p.Command, + Started: p.Started.Unix(), + Exited: p.Exited, + ExitCode: p.ExitCode, + } +} + // Output returns the current contents of the ring buffer. func (p *DetachedProcess) Output() string { p.mu.Lock() diff --git a/pkg/tools/execute/server.go b/pkg/tools/execute/server.go index 8bc50bf..f4d2f79 100644 --- a/pkg/tools/execute/server.go +++ b/pkg/tools/execute/server.go @@ -786,6 +786,36 @@ func (e *Server) ListDetached() []*DetachedProcess { return out } +// ListDetachedInfo returns plain-data snapshots of all detached processes. +func (e *Server) ListDetachedInfo() []DetachedInfoData { + e.detachMu.Lock() + defer e.detachMu.Unlock() + out := make([]DetachedInfoData, len(e.detached)) + for i, p := range e.detached { + out[i] = p.Info() + } + return out +} + +// ListDetachedRaw returns detached process info as []any (each element is map[string]any) +// for consumption by packages that can't import this package directly. +func (e *Server) ListDetachedRaw() []any { + e.detachMu.Lock() + defer e.detachMu.Unlock() + out := make([]any, len(e.detached)) + for i, p := range e.detached { + info := p.Info() + out[i] = map[string]any{ + "pid": info.PID, + "command": info.Command, + "started": info.Started, + "exited": info.Exited, + "exit_code": info.ExitCode, + } + } + return out +} + // SignalDetached sends a signal to a detached process by PID. func (e *Server) SignalDetached(pid int, sig syscall.Signal) error { e.detachMu.Lock()