diff --git a/docs/beads-native-messaging.md b/docs/beads-native-messaging.md new file mode 100644 index 00000000..eb3629e1 --- /dev/null +++ b/docs/beads-native-messaging.md @@ -0,0 +1,226 @@ +# Beads-Native Messaging + +This document describes the beads-native messaging system, which extends Gas Town's mail system with first-class support for groups, queues, and channels backed by beads (Git-native storage). + +## Overview + +Beads-native messaging introduces three new bead types that integrate with the existing mail system: + +- **Groups** (`gt:group`) - Named distribution lists for multi-recipient delivery +- **Queues** (`gt:queue`) - Work queues where workers claim items +- **Channels** (`gt:channel`) - Pub/sub broadcast streams with retention policies + +All three are stored as beads, providing Git-native storage, audit trails, and replication. + +## Bead Types + +### Groups (`gt:group`) + +Groups are named collections of addresses used for mail distribution. Members can be: +- Direct agent addresses (`gastown/crew/max`) +- Wildcard patterns (`*/witness`, `gastown/*`) +- Nested group names (groups can contain other groups) + +**ID Format:** `hq-group-` (e.g., `hq-group-ops-team`) + +**Fields:** +- `name` - Unique group name +- `members` - Comma-separated list of addresses/patterns +- `created_by` - Who created the group +- `created_at` - ISO 8601 timestamp + +**Source:** `internal/beads/beads_group.go` + +### Queues (`gt:queue`) + +Queues are work queues where multiple workers can claim items. Messages sent to a queue are delivered once to a claiming worker. + +**ID Format:** `gt-q-` or `hq-q-` (town-level) + +**Fields:** +- `name` - Queue name +- `status` - `active`, `paused`, or `closed` +- `max_concurrency` - Maximum concurrent workers (0 = unlimited) +- `processing_order` - `fifo` or `priority` +- `available_count`, `processing_count`, `completed_count`, `failed_count` - Queue statistics + +**Source:** `internal/beads/beads_queue.go` + +### Channels (`gt:channel`) + +Channels are pub/sub broadcast streams. Messages sent to a channel are retained according to the channel's retention policy and can be viewed by any subscriber. + +**ID Format:** `hq-channel-` (e.g., `hq-channel-alerts`) + +**Fields:** +- `name` - Unique channel name +- `subscribers` - Comma-separated list of subscribed addresses +- `status` - `active` or `closed` +- `retention_count` - Number of messages to retain (0 = unlimited) +- `retention_hours` - Hours to retain messages (0 = forever) +- `created_by` - Who created the channel +- `created_at` - ISO 8601 timestamp + +**Retention Enforcement:** +- On-write cleanup: When a message is posted, old messages are pruned if over the limit +- Patrol cleanup: Deacon patrol runs periodic cleanup with 10% buffer to avoid thrashing + +**Source:** `internal/beads/beads_channel.go` + +## CLI Commands + +### Group Commands + +```bash +# List all groups +gt mail group list [--json] + +# Show group details +gt mail group show [--json] + +# Create a group with members +gt mail group create [members...] +gt mail group create ops-team gastown/witness gastown/crew/max +gt mail group create ops-team --member gastown/witness --member gastown/crew/max + +# Add member to existing group +gt mail group add + +# Remove member from group +gt mail group remove + +# Delete a group +gt mail group delete +``` + +**Source:** `internal/cmd/mail_group.go` + +### Channel Commands + +```bash +# List all channels +gt mail channel [--json] +gt mail channel list [--json] + +# View channel messages +gt mail channel [--json] +gt mail channel show [--json] + +# Create a channel with retention policy +gt mail channel create [--retain-count=N] [--retain-hours=N] +gt mail channel create alerts --retain-count=100 + +# Delete a channel +gt mail channel delete +``` + +**Source:** `internal/cmd/mail_channel.go` + +### Sending Messages + +The `gt mail send` command supports all address types through the address resolver: + +```bash +# Send to agent (direct) +gt mail send gastown/crew/max -s "Hello" -m "Message body" + +# Send to group (expands to all members) +gt mail send ops-team -s "Alert" -m "Important message" +gt mail send group:ops-team -s "Alert" -m "Explicit group syntax" + +# Send to queue (delivered to one claiming worker) +gt mail send queue:build-queue -s "Job" -m "Build request" + +# Send to channel (broadcast, retained) +gt mail send channel:alerts -s "Alert" -m "System alert" + +# Send to pattern (wildcards) +gt mail send "*/witness" -s "Witness alert" -m "All witnesses" +``` + +**Source:** `internal/cmd/mail_send.go` + +## Address Resolution + +The address resolver (`internal/mail/resolve.go`) determines how to route messages based on the address format: + +### Resolution Order + +1. **Explicit prefix** - If address starts with `group:`, `queue:`, or `channel:`, use that type directly +2. **Contains `/`** - Treat as agent address or pattern (direct delivery) +3. **Starts with `@`** - Check for beads group, then fall back to built-in patterns +4. **Name lookup** - Search in order: group → queue → channel + +### Address Formats + +| Format | Type | Example | +|--------|------|---------| +| `group:` | Group | `group:ops-team` | +| `queue:` | Queue | `queue:build-queue` | +| `channel:` | Channel | `channel:alerts` | +| `//` | Agent | `gastown/crew/max` | +| `/` | Agent | `gastown/witness` | +| `*/` | Pattern | `*/witness` (all witnesses) | +| `@` | Group/Pattern | `@ops-team` | + +### Conflict Handling + +If a name matches multiple types (e.g., both a group and a channel named "alerts"), the resolver returns an error requiring an explicit prefix: + +``` +ambiguous address "alerts": matches multiple types. Use explicit prefix: group:alerts, channel:alerts +``` + +## Key Files + +| File | Description | +|------|-------------| +| `internal/beads/beads_group.go` | Group bead CRUD operations | +| `internal/beads/beads_queue.go` | Queue bead CRUD operations | +| `internal/beads/beads_channel.go` | Channel bead CRUD + retention | +| `internal/mail/resolve.go` | Address resolution logic | +| `internal/cmd/mail_group.go` | Group CLI commands | +| `internal/cmd/mail_channel.go` | Channel CLI commands | +| `internal/cmd/mail_send.go` | Send command with resolver | + +## Examples + +### Create a Team Distribution Group + +```bash +# Create group +gt mail group create dev-team gastown/crew/max gastown/crew/dennis + +# Add another member +gt mail group add dev-team gastown/crew/george + +# Send to entire team +gt mail send dev-team -s "Standup" -m "Daily standup in 5 minutes" +``` + +### Set Up a Build Alert Channel + +```bash +# Create channel with retention +gt mail channel create build-alerts --retain-count=50 + +# Send build notifications +gt mail send channel:build-alerts -s "Build #123 passed" -m "All tests green" + +# View channel history +gt mail channel build-alerts +``` + +### Nested Groups + +```bash +# Create base groups +gt mail group create witnesses gastown/witness ranchero/witness +gt mail group create crew gastown/crew/max gastown/crew/dennis + +# Create umbrella group that includes other groups +gt mail group create all-agents witnesses crew deacon/ + +# Send to everyone +gt mail send all-agents -s "Town meeting" -m "All hands meeting at noon" +``` diff --git a/internal/beads/beads_channel.go b/internal/beads/beads_channel.go index 55a369b2..a7c56175 100644 --- a/internal/beads/beads_channel.go +++ b/internal/beads/beads_channel.go @@ -127,15 +127,15 @@ func ParseChannelFields(description string) *ChannelFields { return fields } -// ChannelBeadID returns the channel bead ID for a given channel name. -// Format: hq-channel- for town-level channels (default). -// Town-level channels are stored in the shared beads database. +// ChannelBeadID returns the bead ID for a channel name. +// Format: hq-channel- (town-level, channels span rigs) func ChannelBeadID(name string) string { return "hq-channel-" + name } // CreateChannelBead creates a channel bead for pub/sub messaging. -// The ID format is: hq-channel- (e.g., hq-channel-alerts) for town-level. +// The ID format is: hq-channel- (e.g., hq-channel-alerts) +// Channels are town-level entities (hq- prefix) because they span rigs. // The created_by field is populated from BD_ACTOR env var for provenance tracking. func (b *Beads) CreateChannelBead(name string, subscribers []string, createdBy string) (*Issue, error) { id := ChannelBeadID(name) @@ -156,6 +156,7 @@ func (b *Beads) CreateChannelBead(name string, subscribers []string, createdBy s "--description=" + description, "--type=task", // Channels use task type with gt:channel label "--labels=gt:channel", + "--force", // Override prefix check (town beads may have mixed prefixes) } // Default actor from BD_ACTOR env var for provenance tracking diff --git a/internal/beads/beads_group.go b/internal/beads/beads_group.go index 24b07004..5c2213ff 100644 --- a/internal/beads/beads_group.go +++ b/internal/beads/beads_group.go @@ -97,13 +97,14 @@ func ParseGroupFields(description string) *GroupFields { } // GroupBeadID returns the bead ID for a group name. -// Format: gt-group- +// Format: hq-group- (town-level, groups span rigs) func GroupBeadID(name string) string { - return "gt-group-" + name + return "hq-group-" + name } // CreateGroupBead creates a group bead for mail distribution. -// The ID format is: gt-group- (e.g., gt-group-ops-team) +// The ID format is: hq-group- (e.g., hq-group-ops-team) +// Groups are town-level entities (hq- prefix) because they span rigs. // The created_by field is populated from BD_ACTOR env var for provenance tracking. func (b *Beads) CreateGroupBead(name string, members []string, createdBy string) (*Issue, error) { id := GroupBeadID(name) @@ -123,6 +124,7 @@ func (b *Beads) CreateGroupBead(name string, members []string, createdBy string) "--description=" + description, "--type=task", // Groups use task type with gt:group label "--labels=gt:group", + "--force", // Override prefix check (town beads may have mixed prefixes) } // Default actor from BD_ACTOR env var for provenance tracking diff --git a/internal/beads/beads_group_test.go b/internal/beads/beads_group_test.go index 2fbec1b7..fd7fc97d 100644 --- a/internal/beads/beads_group_test.go +++ b/internal/beads/beads_group_test.go @@ -163,9 +163,9 @@ func TestGroupBeadID(t *testing.T) { name string want string }{ - {"ops-team", "gt-group-ops-team"}, - {"all", "gt-group-all"}, - {"crew-leads", "gt-group-crew-leads"}, + {"ops-team", "hq-group-ops-team"}, + {"all", "hq-group-all"}, + {"crew-leads", "hq-group-crew-leads"}, } for _, tt := range tests {