package main import ( "fmt" "strings" ) // UpsertAuthor — create or update an author. Used for the bootstrap path // (registering `hermes`, `cos-dispatcher`, `scott` at startup) and for the // agent onboarding flow. type UpsertAuthor struct { ID string AuthorKind string // "human" | "agent" | "system" DisplayName string AvatarURL string } func (m *UpsertAuthor) Kind() string { return "upsert_author" } func (m *UpsertAuthor) AffectedAuthors() []string { return []string{m.ID} } func (m *UpsertAuthor) Validate(s *Store) error { if strings.TrimSpace(m.ID) == "" { return &ValidationError{Field: "id", Message: "required"} } switch m.AuthorKind { case "human", "agent", "system": default: return &ValidationError{Field: "kind", Message: fmt.Sprintf("must be one of human/agent/system, got %q", m.AuthorKind)} } if strings.TrimSpace(m.DisplayName) == "" { return &ValidationError{Field: "display_name", Message: "required"} } return nil } func (m *UpsertAuthor) Apply(s *Store, lock LockRegion) error { now := s.Now() a := &Author{ ID: m.ID, Kind: m.AuthorKind, DisplayName: m.DisplayName, AvatarURL: m.AvatarURL, CreatedAt: now, UpdatedAt: now, } s.mu.Lock() defer s.mu.Unlock() if existing, ok := s.authors[m.ID]; ok { a.CreatedAt = existing.CreatedAt } s.authors[m.ID] = a return nil }