From 0e7cc8d0970c24cf24c0e3be221427981a799efb Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Fri, 26 Dec 2025 10:57:03 -0500 Subject: fix UTF-8 encoding in deployed services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Systemd.hs: add LANG and LC_ALL defaults (en_US.utf8) to all generated unit files to ensure proper UTF-8 handling - Systemd.hs: add generateUnitWithLocale that reads LOCALE_ARCHIVE from the deployer's environment and injects it into generated units - Telegram.hs: add safePutText wrapper that catches encoding errors in logging to prevent them from killing message sends The root cause was NixOS systemd services not inheriting locale settings from the system, causing emoji characters to fail encoding. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Omni/Agent/Telegram.hs | 11 +++++++++-- Omni/Deploy/Systemd.hs | 23 +++++++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/Omni/Agent/Telegram.hs b/Omni/Agent/Telegram.hs index 2bf7aed..57420a4 100644 --- a/Omni/Agent/Telegram.hs +++ b/Omni/Agent/Telegram.hs @@ -116,6 +116,13 @@ import qualified Omni.Test as Test import System.Environment (lookupEnv) import Text.Printf (printf) +safePutText :: Text -> IO () +safePutText msg = do + result <- try @SomeException (putText msg) + case result of + Left _ -> putText "[log encoding error - message contained unrepresentable characters]" + Right () -> pure () + defaultTelegramConfig :: Text -> [Int] -> Maybe Text -> Text -> Types.TelegramConfig defaultTelegramConfig = Types.defaultTelegramConfig @@ -623,7 +630,7 @@ runTelegramBot tgConfig provider = do handleBotAddedToGroup tgConfig addedEvent Nothing -> case Types.parseUpdate rawUpdate of Just msg -> do - putText <| "Received message from " <> Types.tmUserFirstName msg <> " in chat " <> tshow (Types.tmChatId msg) <> " (type: " <> tshow (Types.tmChatType msg) <> "): " <> Text.take 50 (Types.tmText msg) + safePutText <| "Received message from " <> Types.tmUserFirstName msg <> " in chat " <> tshow (Types.tmChatId msg) <> " (type: " <> tshow (Types.tmChatType msg) <> "): " <> Text.take 50 (Types.tmText msg) atomically (writeTVar offsetVar (Types.tmUpdateId msg + 1)) IncomingQueue.enqueueIncoming incomingQueues IncomingQueue.defaultBatchWindowSeconds msg Nothing -> do @@ -1329,7 +1336,7 @@ processEngagedMessage tgConfig provider engineCfg msg uid userName chatId userMe let baseResponse = Engine.resultFinalMessage agentResult response = baseResponse <> traceLink threadId = Types.tmThreadId msg - putText <| "Response text: " <> Text.take 200 response + safePutText <| "Response text: " <> Text.take 200 response if isGroup then void <| Memory.saveGroupMessage chatId threadId Memory.AssistantRole "Ava" response diff --git a/Omni/Deploy/Systemd.hs b/Omni/Deploy/Systemd.hs index 8c6d416..99d4820 100644 --- a/Omni/Deploy/Systemd.hs +++ b/Omni/Deploy/Systemd.hs @@ -8,6 +8,7 @@ -- : dep directory module Omni.Deploy.Systemd ( generateUnit, + generateUnitWithLocale, writeUnit, createSymlink, reloadAndRestart, @@ -27,12 +28,24 @@ import qualified Data.Text.IO as Text.IO import Omni.Deploy.Manifest (Artifact (..), Exec (..), Hardening (..), Service (..), Systemd (..)) import qualified Omni.Test as Test import qualified System.Directory as Dir +import System.Environment (lookupEnv) import System.FilePath (()) import qualified System.Process as Process servicesDir :: FilePath servicesDir = "/var/lib/deployer/services" +-- | Generate unit with locale settings from the current environment. +-- This reads LOCALE_ARCHIVE at generation time and injects it into the unit. +generateUnitWithLocale :: Service -> IO Text +generateUnitWithLocale svc = do + maybeLocaleArchive <- lookupEnv "LOCALE_ARCHIVE" + let localeEnv = case maybeLocaleArchive of + Just path -> Map.singleton "LOCALE_ARCHIVE" (Text.pack path) + Nothing -> mempty + svcWithLocale = svc {serviceEnv = Map.union (serviceEnv svc) localeEnv} + pure (generateUnit svcWithLocale) + generateUnit :: Service -> Text generateUnit Service {..} = Text.unlines <| unitSection ++ serviceSection ++ hardeningSection ++ installSection @@ -69,9 +82,15 @@ generateUnit Service {..} = Just dir -> ["WorkingDirectory=" <> dir] envLines = - Map.toList serviceEnv + Map.toList envWithLocale |> map (\(k, v) -> "Environment=\"" <> k <> "=" <> v <> "\"") + -- Add locale settings for NixOS if not already set + envWithLocale = + Map.insertWith (\_ old -> old) "LANG" "en_US.utf8" + <| Map.insertWith (\_ old -> old) "LC_ALL" "en_US.utf8" + <| serviceEnv + envFileLine = case serviceEnvFile of Nothing -> [] Just path -> ["EnvironmentFile=" <> path] @@ -104,7 +123,7 @@ writeUnit :: FilePath -> Service -> IO FilePath writeUnit baseDir svc = do Dir.createDirectoryIfMissing True baseDir let path = baseDir Text.unpack (serviceName svc) <> ".service" - content = generateUnit svc + content <- generateUnitWithLocale svc Text.IO.writeFile path content pure path -- cgit v1.2.3