From 0e0547b3e124b76348d157d8f4ef9c2af96a962e Mon Sep 17 00:00:00 2001 From: Artem Bambalov Date: Mon, 26 Jan 2026 04:05:11 +0200 Subject: [PATCH] fix(mail): prevent message type failures (#960) --- internal/cmd/install.go | 9 +++------ internal/mail/mailbox.go | 8 ++++++-- internal/mail/router.go | 30 ++++++++++++++++++++++++++---- 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/internal/cmd/install.go b/internal/cmd/install.go index 252bf823..213128fa 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -11,9 +11,9 @@ import ( "github.com/spf13/cobra" "github.com/steveyegge/gastown/internal/beads" - "github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/claude" "github.com/steveyegge/gastown/internal/config" + "github.com/steveyegge/gastown/internal/constants" "github.com/steveyegge/gastown/internal/deps" "github.com/steveyegge/gastown/internal/formula" "github.com/steveyegge/gastown/internal/shell" @@ -408,11 +408,8 @@ func initTownBeads(townPath string) error { // Configure custom types for Gas Town (agent, role, rig, convoy, slot). // These were extracted from beads core in v0.46.0 and now require explicit config. - configCmd := exec.Command("bd", "config", "set", "types.custom", constants.BeadsCustomTypes) - configCmd.Dir = townPath - if configOutput, configErr := configCmd.CombinedOutput(); configErr != nil { - // Non-fatal: older beads versions don't need this, newer ones do - fmt.Printf(" %s Could not set custom types: %s\n", style.Dim.Render("⚠"), strings.TrimSpace(string(configOutput))) + if err := beads.EnsureCustomTypes(beadsDir); err != nil { + return fmt.Errorf("ensuring custom types: %w", err) } // Configure allowed_prefixes for convoy beads (hq-cv-* IDs). diff --git a/internal/mail/mailbox.go b/internal/mail/mailbox.go index c1f85a55..de5af343 100644 --- a/internal/mail/mailbox.go +++ b/internal/mail/mailbox.go @@ -212,6 +212,10 @@ func (m *Mailbox) identityVariants() []string { // queryMessages runs a bd list query with the given filter flag and value. func (m *Mailbox) queryMessages(beadsDir, filterFlag, filterValue, status string) ([]*Message, error) { + if err := beads.EnsureCustomTypes(beadsDir); err != nil { + return nil, fmt.Errorf("ensuring custom types: %w", err) + } + args := []string{"list", "--type", "message", filterFlag, filterValue, @@ -829,8 +833,8 @@ func (m *Mailbox) rewriteLegacy(messages []*Message) error { for _, msg := range messages { data, err := json.Marshal(msg) if err != nil { - _ = file.Close() // best-effort cleanup - _ = os.Remove(tmpPath) // best-effort cleanup + _ = file.Close() // best-effort cleanup + _ = os.Remove(tmpPath) // best-effort cleanup return err } _, _ = file.WriteString(string(data) + "\n") // non-fatal: partial write is acceptable diff --git a/internal/mail/router.go b/internal/mail/router.go index d2bb790b..b864c069 100644 --- a/internal/mail/router.go +++ b/internal/mail/router.go @@ -189,6 +189,13 @@ func (r *Router) resolveBeadsDir(_ string) string { // address unused: all mail return filepath.Join(r.townRoot, ".beads") } +func (r *Router) ensureCustomTypes(beadsDir string) error { + if err := beads.EnsureCustomTypes(beadsDir); err != nil { + return fmt.Errorf("ensuring custom types: %w", err) + } + return nil +} + // isTownLevelAddress returns true if the address is for a town-level agent or the overseer. func isTownLevelAddress(address string) bool { addr := strings.TrimSuffix(address, "/") @@ -214,10 +221,10 @@ const ( // ParsedGroup represents a parsed @group address. type ParsedGroup struct { - Type GroupType - RoleType string // witness, crew, polecat, dog, etc. - Rig string // rig name for rig-scoped groups - Original string // original @group string + Type GroupType + RoleType string // witness, crew, polecat, dog, etc. + Rig string // rig name for rig-scoped groups + Original string // original @group string } // parseGroupAddress parses a @group address into its components. @@ -697,6 +704,9 @@ func (r *Router) sendToSingle(msg *Message) error { } beadsDir := r.resolveBeadsDir(msg.To) + if err := r.ensureCustomTypes(beadsDir); err != nil { + return err + } _, err := runBdCommand(args, filepath.Dir(beadsDir), beadsDir) if err != nil { return fmt.Errorf("sending message: %w", err) @@ -807,6 +817,9 @@ func (r *Router) sendToQueue(msg *Message) error { // Queue messages go to town-level beads (shared location) beadsDir := r.resolveBeadsDir("") + if err := r.ensureCustomTypes(beadsDir); err != nil { + return err + } _, err = runBdCommand(args, filepath.Dir(beadsDir), beadsDir) if err != nil { return fmt.Errorf("sending to queue %s: %w", queueName, err) @@ -878,6 +891,9 @@ func (r *Router) sendToAnnounce(msg *Message) error { // Announce messages go to town-level beads (shared location) beadsDir := r.resolveBeadsDir("") + if err := r.ensureCustomTypes(beadsDir); err != nil { + return err + } _, err = runBdCommand(args, filepath.Dir(beadsDir), beadsDir) if err != nil { return fmt.Errorf("sending to announce %s: %w", announceName, err) @@ -951,6 +967,9 @@ func (r *Router) sendToChannel(msg *Message) error { // Channel messages go to town-level beads (shared location) beadsDir := r.resolveBeadsDir("") + if err := r.ensureCustomTypes(beadsDir); err != nil { + return err + } _, err = runBdCommand(args, filepath.Dir(beadsDir), beadsDir) if err != nil { return fmt.Errorf("sending to channel %s: %w", channelName, err) @@ -988,6 +1007,9 @@ func (r *Router) pruneAnnounce(announceName string, retainCount int) error { } beadsDir := r.resolveBeadsDir("") + if err := r.ensureCustomTypes(beadsDir); err != nil { + return err + } // Query existing messages in this announce channel // Use bd list with labels filter to find messages with announce: label