From 73531d36f6cc737d56aeadded582a0dc75d21755 Mon Sep 17 00:00:00 2001 From: DBT Date: Mon, 23 Feb 2026 12:41:43 +0000 Subject: [PATCH] add trigger audit stats and expose background trigger visibility --- cmd/clawgo/cmd_status.go | 5 +++ pkg/agent/loop.go | 37 +++++++++++++--- pkg/agent/trigger_audit.go | 87 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 pkg/agent/trigger_audit.go diff --git a/cmd/clawgo/cmd_status.go b/cmd/clawgo/cmd_status.go index 75206e8..8284e9f 100644 --- a/cmd/clawgo/cmd_status.go +++ b/cmd/clawgo/cmd_status.go @@ -85,5 +85,10 @@ func statusCmd() { fmt.Printf("Heartbeat Last Log: %s\n", lines[len(lines)-1]) } } + + triggerStats := filepath.Join(workspace, "memory", "trigger-stats.json") + if data, err := os.ReadFile(triggerStats); err == nil { + fmt.Printf("Trigger Stats: %s\n", strings.TrimSpace(string(data))) + } } } diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 587322e..06e4b8a 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -37,6 +37,7 @@ type AgentLoop struct { compactionTrigger int compactionKeepRecent int heartbeatAckMaxChars int + audit *triggerAudit running bool } @@ -133,6 +134,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers compactionTrigger: cfg.Agents.Defaults.ContextCompaction.TriggerMessages, compactionKeepRecent: cfg.Agents.Defaults.ContextCompaction.KeepRecentMessages, heartbeatAckMaxChars: cfg.Agents.Defaults.Heartbeat.AckMaxChars, + audit: newTriggerAudit(workspace), running: false, } @@ -163,15 +165,22 @@ func (al *AgentLoop) Run(ctx context.Context) error { response = fmt.Sprintf("Error processing message: %v", err) } + trigger := al.getTrigger(msg) + suppressed := false if response != "" { if al.shouldSuppressOutbound(msg, response) { - continue + suppressed = true + } else { + al.bus.PublishOutbound(bus.OutboundMessage{ + Channel: msg.Channel, + ChatID: msg.ChatID, + Content: response, + }) } - al.bus.PublishOutbound(bus.OutboundMessage{ - Channel: msg.Channel, - ChatID: msg.ChatID, - Content: response, - }) + } + al.audit.Record(trigger, msg.Channel, msg.SessionKey, suppressed, err) + if suppressed { + continue } } } @@ -183,6 +192,22 @@ func (al *AgentLoop) Stop() { al.running = false } +func (al *AgentLoop) getTrigger(msg bus.InboundMessage) string { + if msg.Metadata != nil { + if t := strings.TrimSpace(msg.Metadata["trigger"]); t != "" { + return strings.ToLower(t) + } + } + if msg.Channel == "system" { + sid := strings.ToLower(strings.TrimSpace(msg.SenderID)) + if sid != "" { + return sid + } + return "system" + } + return "user" +} + func (al *AgentLoop) shouldSuppressOutbound(msg bus.InboundMessage, response string) bool { if msg.Metadata == nil { return false diff --git a/pkg/agent/trigger_audit.go b/pkg/agent/trigger_audit.go new file mode 100644 index 0000000..98c2c46 --- /dev/null +++ b/pkg/agent/trigger_audit.go @@ -0,0 +1,87 @@ +package agent + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +type TriggerStats struct { + UpdatedAt string `json:"updated_at"` + Counts map[string]int `json:"counts"` +} + +type triggerAudit struct { + workspace string + mu sync.Mutex +} + +type triggerEvent struct { + Time string `json:"time"` + Trigger string `json:"trigger"` + Channel string `json:"channel"` + Session string `json:"session"` + Suppressed bool `json:"suppressed,omitempty"` + Error string `json:"error,omitempty"` +} + +func newTriggerAudit(workspace string) *triggerAudit { + return &triggerAudit{workspace: workspace} +} + +func (ta *triggerAudit) Record(trigger, channel, session string, suppressed bool, err error) { + if ta == nil { + return + } + trigger = normalizeTrigger(trigger) + e := triggerEvent{ + Time: time.Now().UTC().Format(time.RFC3339), + Trigger: trigger, + Channel: channel, + Session: session, + Suppressed: suppressed, + } + if err != nil { + e.Error = err.Error() + } + + ta.mu.Lock() + defer ta.mu.Unlock() + + memDir := filepath.Join(ta.workspace, "memory") + _ = os.MkdirAll(memDir, 0755) + + logPath := filepath.Join(memDir, "trigger-audit.jsonl") + if data, mErr := json.Marshal(e); mErr == nil { + f, oErr := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if oErr == nil { + _, _ = f.Write(append(data, '\n')) + _ = f.Close() + } + } + + statsPath := filepath.Join(memDir, "trigger-stats.json") + stats := TriggerStats{Counts: map[string]int{}} + if raw, rErr := os.ReadFile(statsPath); rErr == nil { + _ = json.Unmarshal(raw, &stats) + if stats.Counts == nil { + stats.Counts = map[string]int{} + } + } + stats.Counts[trigger]++ + stats.UpdatedAt = e.Time + if raw, mErr := json.MarshalIndent(stats, "", " "); mErr == nil { + _ = os.WriteFile(statsPath, raw, 0644) + } +} + +func normalizeTrigger(v string) string { + s := strings.ToLower(strings.TrimSpace(v)) + if s == "" { + return "user" + } + return s +}