fix(beads): use hq- prefix for group/channel beads (town-level entities)
Groups and channels are town-level entities that span rigs, so they should use the hq- prefix rather than gt- (rig-level). Changes: - GroupBeadID: gt-group- → hq-group- - ChannelBeadID: gt-channel- → hq-channel- - Add --force flag to bypass prefix validation (town beads may have mixed prefixes from test runs) - Update tests and documentation Also adds docs/beads-native-messaging.md documenting: - New bead types (gt:group, gt:queue, gt:channel) - CLI commands (gt mail group, gt mail channel) - Address resolution logic - Usage examples Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
e30e46a87a
commit
cbbf566f06
226
docs/beads-native-messaging.md
Normal file
226
docs/beads-native-messaging.md
Normal file
@@ -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-<name>` (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-<name>` or `hq-q-<name>` (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-<name>` (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 <name> [--json]
|
||||||
|
|
||||||
|
# Create a group with members
|
||||||
|
gt mail group create <name> [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 <name> <member>
|
||||||
|
|
||||||
|
# Remove member from group
|
||||||
|
gt mail group remove <name> <member>
|
||||||
|
|
||||||
|
# Delete a group
|
||||||
|
gt mail group delete <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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 <name> [--json]
|
||||||
|
gt mail channel show <name> [--json]
|
||||||
|
|
||||||
|
# Create a channel with retention policy
|
||||||
|
gt mail channel create <name> [--retain-count=N] [--retain-hours=N]
|
||||||
|
gt mail channel create alerts --retain-count=100
|
||||||
|
|
||||||
|
# Delete a channel
|
||||||
|
gt mail channel delete <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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:<name>` | Group | `group:ops-team` |
|
||||||
|
| `queue:<name>` | Queue | `queue:build-queue` |
|
||||||
|
| `channel:<name>` | Channel | `channel:alerts` |
|
||||||
|
| `<town>/<role>/<name>` | Agent | `gastown/crew/max` |
|
||||||
|
| `<town>/<role>` | Agent | `gastown/witness` |
|
||||||
|
| `*/<role>` | Pattern | `*/witness` (all witnesses) |
|
||||||
|
| `@<name>` | 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"
|
||||||
|
```
|
||||||
@@ -127,15 +127,15 @@ func ParseChannelFields(description string) *ChannelFields {
|
|||||||
return fields
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChannelBeadID returns the channel bead ID for a given channel name.
|
// ChannelBeadID returns the bead ID for a channel name.
|
||||||
// Format: hq-channel-<name> for town-level channels (default).
|
// Format: hq-channel-<name> (town-level, channels span rigs)
|
||||||
// Town-level channels are stored in the shared beads database.
|
|
||||||
func ChannelBeadID(name string) string {
|
func ChannelBeadID(name string) string {
|
||||||
return "hq-channel-" + name
|
return "hq-channel-" + name
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateChannelBead creates a channel bead for pub/sub messaging.
|
// CreateChannelBead creates a channel bead for pub/sub messaging.
|
||||||
// The ID format is: hq-channel-<name> (e.g., hq-channel-alerts) for town-level.
|
// The ID format is: hq-channel-<name> (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.
|
// 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) {
|
func (b *Beads) CreateChannelBead(name string, subscribers []string, createdBy string) (*Issue, error) {
|
||||||
id := ChannelBeadID(name)
|
id := ChannelBeadID(name)
|
||||||
@@ -156,6 +156,7 @@ func (b *Beads) CreateChannelBead(name string, subscribers []string, createdBy s
|
|||||||
"--description=" + description,
|
"--description=" + description,
|
||||||
"--type=task", // Channels use task type with gt:channel label
|
"--type=task", // Channels use task type with gt:channel label
|
||||||
"--labels=gt:channel",
|
"--labels=gt:channel",
|
||||||
|
"--force", // Override prefix check (town beads may have mixed prefixes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default actor from BD_ACTOR env var for provenance tracking
|
// Default actor from BD_ACTOR env var for provenance tracking
|
||||||
|
|||||||
@@ -97,13 +97,14 @@ func ParseGroupFields(description string) *GroupFields {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GroupBeadID returns the bead ID for a group name.
|
// GroupBeadID returns the bead ID for a group name.
|
||||||
// Format: gt-group-<name>
|
// Format: hq-group-<name> (town-level, groups span rigs)
|
||||||
func GroupBeadID(name string) string {
|
func GroupBeadID(name string) string {
|
||||||
return "gt-group-" + name
|
return "hq-group-" + name
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateGroupBead creates a group bead for mail distribution.
|
// CreateGroupBead creates a group bead for mail distribution.
|
||||||
// The ID format is: gt-group-<name> (e.g., gt-group-ops-team)
|
// The ID format is: hq-group-<name> (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.
|
// 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) {
|
func (b *Beads) CreateGroupBead(name string, members []string, createdBy string) (*Issue, error) {
|
||||||
id := GroupBeadID(name)
|
id := GroupBeadID(name)
|
||||||
@@ -123,6 +124,7 @@ func (b *Beads) CreateGroupBead(name string, members []string, createdBy string)
|
|||||||
"--description=" + description,
|
"--description=" + description,
|
||||||
"--type=task", // Groups use task type with gt:group label
|
"--type=task", // Groups use task type with gt:group label
|
||||||
"--labels=gt:group",
|
"--labels=gt:group",
|
||||||
|
"--force", // Override prefix check (town beads may have mixed prefixes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default actor from BD_ACTOR env var for provenance tracking
|
// Default actor from BD_ACTOR env var for provenance tracking
|
||||||
|
|||||||
@@ -163,9 +163,9 @@ func TestGroupBeadID(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{"ops-team", "gt-group-ops-team"},
|
{"ops-team", "hq-group-ops-team"},
|
||||||
{"all", "gt-group-all"},
|
{"all", "hq-group-all"},
|
||||||
{"crew-leads", "gt-group-crew-leads"},
|
{"crew-leads", "hq-group-crew-leads"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
Reference in New Issue
Block a user