From 5ea358643181baae0d3624847e36bb0d45f0af5b Mon Sep 17 00:00:00 2001 From: DBT Date: Wed, 25 Feb 2026 11:13:58 +0000 Subject: [PATCH] persist node state to shared file for gateway/cli visibility --- pkg/agent/loop.go | 1 + pkg/nodes/manager.go | 63 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index befcf18..41812f7 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -88,6 +88,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers toolsRegistry.Register(tools.NewProcessTool(processManager)) nodesManager := nodes.DefaultManager() nodesManager.SetAuditPath(filepath.Join(workspace, "memory", "nodes-audit.jsonl")) + nodesManager.SetStatePath(filepath.Join(workspace, "memory", "nodes-state.json")) nodesManager.Upsert(nodes.NodeInfo{ID: "local", Name: "local", Capabilities: nodes.Capabilities{Run: true, Invoke: true, Model: true, Camera: true, Screen: true, Location: true, Canvas: true}, Models: []string{"local-sim"}, Online: true}) nodesManager.RegisterHandler("local", func(req nodes.Request) nodes.Response { switch req.Action { diff --git a/pkg/nodes/manager.go b/pkg/nodes/manager.go index 55b2b87..5d17796 100644 --- a/pkg/nodes/manager.go +++ b/pkg/nodes/manager.go @@ -21,6 +21,7 @@ type Manager struct { handlers map[string]Handler ttl time.Duration auditPath string + statePath string } var defaultManager = NewManager() @@ -39,6 +40,13 @@ func (m *Manager) SetAuditPath(path string) { m.auditPath = strings.TrimSpace(path) } +func (m *Manager) SetStatePath(path string) { + m.mu.Lock() + m.statePath = strings.TrimSpace(path) + m.mu.Unlock() + m.loadState() +} + func (m *Manager) Upsert(info NodeInfo) { m.mu.Lock() now := time.Now().UTC() @@ -60,15 +68,21 @@ func (m *Manager) Upsert(info NodeInfo) { } m.nodes[info.ID] = info m.mu.Unlock() + m.saveState() m.appendAudit("upsert", info.ID, map[string]interface{}{"existed": existed, "endpoint": info.Endpoint, "version": info.Version}) } func (m *Manager) MarkOffline(id string) { m.mu.Lock() - defer m.mu.Unlock() + changed := false if n, ok := m.nodes[id]; ok { n.Online = false m.nodes[id] = n + changed = true + } + m.mu.Unlock() + if changed { + m.saveState() } } @@ -208,6 +222,9 @@ func (m *Manager) reaperLoop() { } } m.mu.Unlock() + if len(offlined) > 0 { + m.saveState() + } for _, id := range offlined { m.appendAudit("offline_ttl", id, nil) } @@ -234,3 +251,47 @@ func (m *Manager) appendAudit(event, nodeID string, data map[string]interface{}) defer f.Close() _, _ = f.Write(append(b, '\n')) } + +func (m *Manager) saveState() { + m.mu.RLock() + path := m.statePath + items := make([]NodeInfo, 0, len(m.nodes)) + for _, n := range m.nodes { + items = append(items, n) + } + m.mu.RUnlock() + if strings.TrimSpace(path) == "" { + return + } + _ = os.MkdirAll(filepath.Dir(path), 0755) + b, err := json.MarshalIndent(items, "", " ") + if err != nil { + return + } + _ = os.WriteFile(path, b, 0644) +} + +func (m *Manager) loadState() { + m.mu.RLock() + path := m.statePath + m.mu.RUnlock() + if strings.TrimSpace(path) == "" { + return + } + b, err := os.ReadFile(path) + if err != nil { + return + } + var items []NodeInfo + if err := json.Unmarshal(b, &items); err != nil { + return + } + m.mu.Lock() + for _, n := range items { + if strings.TrimSpace(n.ID) == "" { + continue + } + m.nodes[n.ID] = n + } + m.mu.Unlock() +}