agent: expose detach methods on Core interface

Adds Detach, ListDetached, SignalDetached, GetDetachedOutput,
DismissDetached to the Core interface. Implemented on agent struct
via interface type assertions to the execute server.

Also adds ListDetachedRaw() to execute.Server for cross-package
consumption without import cycles.
This commit is contained in:
Levi Neely 2026-07-17 16:49:29 +02:00
parent 16d057741c
commit 5df603a9e3
4 changed files with 156 additions and 0 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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()

View File

@ -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()