From f9e820985d9166ff9935ba784e241f4ea79e4e58 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Mon, 29 Dec 2025 23:36:29 -0800 Subject: [PATCH] feat: Filter agent session molecule noise from activity feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent session molecules (gt-gastown-crew-joe, gt-gastown-witness, etc.) update frequently for status tracking, creating noisy entries in the activity feed. This change: - Adds IsAgentSessionBead() to identify agent session beads - Filters out "update" events for agent sessions from the event feed - Still updates the agent tree so status is visible there - Still shows create/complete/fail/delete events for agents The filtering happens in addEvent() in the TUI feed model. Agent session updates are identified by parsing the bead ID pattern and checking for known agent roles (mayor, deacon, witness, refinery, crew, polecat). Resolves: gt-sb6m4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/beads/beads.go | 17 ++++++++ internal/beads/beads_test.go | 78 ++++++++++++++++++++++++++++++++++++ internal/tui/feed/model.go | 30 ++++++++++---- 3 files changed, 117 insertions(+), 8 deletions(-) diff --git a/internal/beads/beads.go b/internal/beads/beads.go index fa3b2105..4555c98e 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -822,3 +822,20 @@ func ParseAgentBeadID(id string) (rig, role, name string, ok bool) { return "", "", "", false } } + +// IsAgentSessionBead returns true if the bead ID represents an agent session molecule. +// Agent session beads follow patterns like gt-mayor, gt-gastown-witness, gt-gastown-crew-joe. +// These are used to track agent state and update frequently, which can create noise. +func IsAgentSessionBead(beadID string) bool { + _, role, _, ok := ParseAgentBeadID(beadID) + if !ok { + return false + } + // Known agent roles + switch role { + case "mayor", "deacon", "witness", "refinery", "crew", "polecat": + return true + default: + return false + } +} diff --git a/internal/beads/beads_test.go b/internal/beads/beads_test.go index 9ec8c7ca..ad2d1871 100644 --- a/internal/beads/beads_test.go +++ b/internal/beads/beads_test.go @@ -967,3 +967,81 @@ func TestResolveBeadsDir(t *testing.T) { } }) } + +func TestParseAgentBeadID(t *testing.T) { + tests := []struct { + input string + wantRig string + wantRole string + wantName string + wantOK bool + }{ + // Town-level agents + {"gt-mayor", "", "mayor", "", true}, + {"gt-deacon", "", "deacon", "", true}, + // Rig-level singletons + {"gt-gastown-witness", "gastown", "witness", "", true}, + {"gt-gastown-refinery", "gastown", "refinery", "", true}, + // Rig-level named agents + {"gt-gastown-crew-joe", "gastown", "crew", "joe", true}, + {"gt-gastown-crew-max", "gastown", "crew", "max", true}, + {"gt-gastown-polecat-capable", "gastown", "polecat", "capable", true}, + // Names with hyphens + {"gt-gastown-polecat-my-agent", "gastown", "polecat", "my-agent", true}, + // Parseable but not valid agent roles (IsAgentSessionBead will reject) + {"gt-abc123", "", "abc123", "", true}, // Parses as town-level but not valid role + // Truly invalid patterns + {"bd-gastown-crew-joe", "", "", "", false}, // Wrong prefix + {"", "", "", "", false}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + rig, role, name, ok := ParseAgentBeadID(tt.input) + if ok != tt.wantOK { + t.Errorf("ParseAgentBeadID(%q) ok = %v, want %v", tt.input, ok, tt.wantOK) + return + } + if rig != tt.wantRig { + t.Errorf("ParseAgentBeadID(%q) rig = %q, want %q", tt.input, rig, tt.wantRig) + } + if role != tt.wantRole { + t.Errorf("ParseAgentBeadID(%q) role = %q, want %q", tt.input, role, tt.wantRole) + } + if name != tt.wantName { + t.Errorf("ParseAgentBeadID(%q) name = %q, want %q", tt.input, name, tt.wantName) + } + }) + } +} + +func TestIsAgentSessionBead(t *testing.T) { + tests := []struct { + beadID string + want bool + }{ + // Agent session beads (should return true) + {"gt-mayor", true}, + {"gt-deacon", true}, + {"gt-gastown-witness", true}, + {"gt-gastown-refinery", true}, + {"gt-gastown-crew-joe", true}, + {"gt-gastown-polecat-capable", true}, + // Regular work beads (should return false) + {"gt-abc123", false}, + {"gt-sb6m4", false}, + {"gt-u7dxq", false}, + // Invalid beads + {"bd-abc123", false}, + {"", false}, + } + + for _, tt := range tests { + t.Run(tt.beadID, func(t *testing.T) { + got := IsAgentSessionBead(tt.beadID) + if got != tt.want { + t.Errorf("IsAgentSessionBead(%q) = %v, want %v", tt.beadID, got, tt.want) + } + }) + } +} diff --git a/internal/tui/feed/model.go b/internal/tui/feed/model.go index d0e43278..24bb737e 100644 --- a/internal/tui/feed/model.go +++ b/internal/tui/feed/model.go @@ -8,6 +8,7 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" + "github.com/steveyegge/gastown/internal/beads" ) // Panel represents which panel has focus @@ -252,14 +253,7 @@ func (m *Model) updateViewContent() { // addEvent adds an event and updates the agent tree func (m *Model) addEvent(e Event) { - m.events = append(m.events, e) - - // Keep max 1000 events - if len(m.events) > 1000 { - m.events = m.events[len(m.events)-1000:] - } - - // Update agent tree + // Update agent tree first (always do this for status tracking) if e.Rig != "" { rig, ok := m.rigs[e.Rig] if !ok { @@ -287,6 +281,26 @@ func (m *Model) addEvent(e Event) { } } + // Filter out noisy agent session updates from the event feed. + // Agent session molecules (like gt-gastown-crew-joe) update frequently + // for status tracking. These updates are visible in the agent tree, + // so we don't need to clutter the event feed with them. + // We still show create/complete/fail/delete events for agent sessions. + if e.Type == "update" && beads.IsAgentSessionBead(e.Target) { + // Skip adding to event feed, but still refresh the view + // (agent tree was updated above) + m.updateViewContent() + return + } + + // Add to event feed + m.events = append(m.events, e) + + // Keep max 1000 events + if len(m.events) > 1000 { + m.events = m.events[len(m.events)-1000:] + } + m.updateViewContent() }