diff --git a/cmd/olliesrv/internal/agent/agent.go b/cmd/olliesrv/internal/agent/agent.go index 88e47e6..f29403d 100644 --- a/cmd/olliesrv/internal/agent/agent.go +++ b/cmd/olliesrv/internal/agent/agent.go @@ -56,6 +56,13 @@ type Agent struct { save func() // trigger debounced persistence flush func() // immediately flush persistence onStateChange func(agentID, state string) // optional callback for state changes + + // Chat log — the rendered output stream, exposed via 9P. + chatMu sync.RWMutex + chatLog []byte + chatVers uint32 + chatCond *sync.Cond + plan []byte } // Backend returns the active backend from the runtime. @@ -116,6 +123,56 @@ func (ag *Agent) SetName(name string) { // ID returns the agent's unique identity. func (ag *Agent) ID() string { return ag.id } +// --- Chat log methods --- + +// AppendChat appends data to the chat log and notifies stream readers. +func (ag *Agent) AppendChat(data []byte) { + if len(data) == 0 { + return + } + ag.chatMu.Lock() + ag.chatLog = append(ag.chatLog, data...) + ag.chatVers++ + ag.chatMu.Unlock() + ag.chatCond.Broadcast() +} + +// EnsureTrailingNewline appends a newline if the log doesn't end with one. +func (ag *Agent) EnsureTrailingNewline() { + ag.chatMu.Lock() + if len(ag.chatLog) > 0 && ag.chatLog[len(ag.chatLog)-1] != '\n' { + ag.chatLog = append(ag.chatLog, '\n') + } + ag.chatVers++ + ag.chatMu.Unlock() +} + +// ChatMu returns the chat log mutex for external locking (streaming). +func (ag *Agent) ChatMu() *sync.RWMutex { return &ag.chatMu } + +// ChatCond returns the condvar for blocking chat readers. +func (ag *Agent) ChatCond() *sync.Cond { return ag.chatCond } + +// ChatLog returns the raw chat log bytes (caller must hold ChatMu.RLock). +func (ag *Agent) ChatLog() []byte { return ag.chatLog } + +// Plan returns a copy of the plan. +func (ag *Agent) Plan() []byte { + ag.chatMu.RLock() + p := make([]byte, len(ag.plan)) + copy(p, ag.plan) + ag.chatMu.RUnlock() + return p +} + +// SetPlan replaces the plan. +func (ag *Agent) SetPlan(data []byte) { + ag.chatMu.Lock() + ag.plan = make([]byte, len(data)) + copy(ag.plan, data) + ag.chatMu.Unlock() +} + // BackendName returns the name of the active backend. func (ag *Agent) BackendName() string { if ag.runtime.Backend == nil { @@ -651,6 +708,7 @@ func NewAgent(cfg AgentParams) *Agent { state: "idle", } ag.signalCh = make(chan struct{}) + ag.chatCond = sync.NewCond(ag.chatMu.RLocker()) ag.SetSessionEnv(cfg.SessionID) return ag }