From 987bd7587ccab273f06c3f97ea9a25ef7e9c04d1 Mon Sep 17 00:00:00 2001 From: jenkins Date: Thu, 13 Aug 2026 13:05:17 -0300 Subject: [PATCH] hermes: deploy Telegram readiness diagnostics --- services/hermes/chat-router.yaml | 4 +-- services/hermes/router/telegram.go | 45 ++++++++++++++++++++----- services/hermes/router/telegram_test.go | 19 +++++++++++ 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/services/hermes/chat-router.yaml b/services/hermes/chat-router.yaml index c27284e7e..6f813de1a 100644 --- a/services/hermes/chat-router.yaml +++ b/services/hermes/chat-router.yaml @@ -20,7 +20,7 @@ spec: app: hermes-chat-router annotations: ai.bstein.dev/role: privacy-preserving-chat-tenant-router - ai.bstein.dev/config-rev: "20260813-telegram-readiness-v1" + ai.bstein.dev/config-rev: "20260813-telegram-readiness-v2" vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/agent-pre-populate-only: "true" vault.hashicorp.com/agent-init-first: "true" @@ -62,7 +62,7 @@ spec: values: [rpi5] containers: - name: router - image: registry.bstein.dev/bstein/hermes-chat-router@sha256:d639e78934d36384eaebc631a95c7e151492c59af2d483c994f14ac585590eeb + image: registry.bstein.dev/bstein/hermes-chat-router@sha256:e3794fb8b9ee76d699893ba303d7080f54698c0a06064dc32293e99e988cabff imagePullPolicy: IfNotPresent ports: - {name: http, containerPort: 8080, protocol: TCP} diff --git a/services/hermes/router/telegram.go b/services/hermes/router/telegram.go index cb5f2e76e..02a4e5197 100644 --- a/services/hermes/router/telegram.go +++ b/services/hermes/router/telegram.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "log" "net/http" "net/url" "os" @@ -100,14 +101,18 @@ func (bot *telegramBot) status() (string, bool, string) { return bot.botUsername, bot.ready, bot.lastError } -func (bot *telegramBot) setStatus(username string, ready bool, lastError string) { +func (bot *telegramBot) setStatus(username string, ready bool, lastError string) bool { bot.mu.Lock() defer bot.mu.Unlock() + previousUsername := bot.botUsername + previousReady := bot.ready + previousError := bot.lastError if username != "" { bot.botUsername = strings.TrimPrefix(strings.TrimSpace(username), "@") } bot.ready = ready bot.lastError = strings.TrimSpace(lastError) + return previousUsername != bot.botUsername || previousReady != bot.ready || previousError != bot.lastError } func (bot *telegramBot) slotLock(slot int) *sync.Mutex { @@ -140,11 +145,20 @@ func (bot *telegramBot) call(ctx context.Context, method string, values url.Valu return errors.New("read Telegram response") } var envelope struct { - OK bool `json:"ok"` - Result json.RawMessage `json:"result"` + OK bool `json:"ok"` + Result json.RawMessage `json:"result"` + Description string `json:"description"` + ErrorCode int `json:"error_code"` } - if response.StatusCode != http.StatusOK || json.Unmarshal(body, &envelope) != nil || !envelope.OK { - return fmt.Errorf("Telegram API returned status %d", response.StatusCode) + if err := json.Unmarshal(body, &envelope); err != nil { + return fmt.Errorf("Telegram API returned undecodable status %d", response.StatusCode) + } + if response.StatusCode != http.StatusOK || !envelope.OK { + description := strings.TrimSpace(envelope.Description) + if description == "" { + description = http.StatusText(response.StatusCode) + } + return fmt.Errorf("Telegram API error %d: %s", envelope.ErrorCode, description) } if result != nil && len(envelope.Result) > 0 { if err := json.Unmarshal(envelope.Result, result); err != nil { @@ -164,10 +178,21 @@ func (bot *telegramBot) run() { err := bot.call(ctx, "getMe", url.Values{}, &me) cancel() if err == nil { - bot.setStatus(me.Username, true, "") - break + ctx, cancel = context.WithTimeout(context.Background(), 15*time.Second) + err = bot.call(ctx, "deleteWebhook", url.Values{ + "drop_pending_updates": {"false"}, + }, nil) + cancel() + if err == nil { + if bot.setStatus(me.Username, true, "") { + log.Printf("Telegram bot ready as @%s", strings.TrimPrefix(me.Username, "@")) + } + break + } + } + if bot.setStatus("", false, "Telegram rejected the configured bot token or is temporarily unavailable.") { + log.Printf("Telegram bot readiness check failed: %v", err) } - bot.setStatus("", false, "Telegram rejected the configured bot token or is temporarily unavailable.") time.Sleep(10 * time.Second) } for { @@ -182,7 +207,9 @@ func (bot *telegramBot) run() { err := bot.call(ctx, "getUpdates", values, &updates) cancel() if err != nil { - bot.setStatus("", false, "Hermes lost contact with the configured Telegram bot.") + if bot.setStatus("", false, "Hermes lost contact with the configured Telegram bot.") { + log.Printf("Telegram update polling failed: %v", err) + } time.Sleep(3 * time.Second) break } diff --git a/services/hermes/router/telegram_test.go b/services/hermes/router/telegram_test.go index 6762ab33d..81cbaf4c1 100644 --- a/services/hermes/router/telegram_test.go +++ b/services/hermes/router/telegram_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "testing" + "time" ) func TestReadTelegramConfig(t *testing.T) { @@ -72,3 +73,21 @@ func TestSplitTelegramTextStaysUnderTelegramLimit(t *testing.T) { } } } + +func TestTelegramCallIncludesAPIDescription(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusConflict) + _, _ = writer.Write([]byte(`{"ok":false,"error_code":409,"description":"Conflict: terminated by other getUpdates request"}`)) + })) + defer server.Close() + + bot := &telegramBot{ + apiBase: server.URL, + client: &http.Client{Timeout: time.Second}, + } + err := bot.call(t.Context(), "getUpdates", nil, nil) + if err == nil || !strings.Contains(err.Error(), "terminated by other getUpdates request") { + t.Fatalf("expected Telegram API description, got %v", err) + } +}