From 25d7f3bb9b3527785889e6022c447617ef3a92c1 Mon Sep 17 00:00:00 2001 From: Hermes Date: Tue, 23 Jun 2026 01:31:03 -0700 Subject: [PATCH] feat(mutation): UpsertAuthor (humans and agents) --- mutations_upsert_author.go | 56 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 mutations_upsert_author.go diff --git a/mutations_upsert_author.go b/mutations_upsert_author.go new file mode 100644 index 0000000..1c2d14e --- /dev/null +++ b/mutations_upsert_author.go @@ -0,0 +1,56 @@ +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 +}