runGateway builds the configured adapters and runs the messaging runner until ctx is canceled, backing conversations with the given hub store (nil = no cross-channel continuity).
(ctx context.Context, broker hub.Store)
| 238 | // ctx is canceled, backing conversations with the given hub store (nil = no |
| 239 | // cross-channel continuity). |
| 240 | func (cli *ChatCLI) runGateway(ctx context.Context, broker hub.Store) error { |
| 241 | // Adopt the interactive session's live model before serving (the daemon is a |
| 242 | // separate process that snapshotted .env at boot). Per-message refresh in |
| 243 | // gatewayAgentFunc keeps it current after a /switch while the daemon runs. |
| 244 | cli.refreshGatewayModel() |
| 245 | |
| 246 | adapters, err := gateway.BuildConfigured() |
| 247 | if err != nil { |
| 248 | return err |
| 249 | } |
| 250 | if len(adapters) == 0 { |
| 251 | return fmt.Errorf("%s", i18n.T("gateway.no_platforms")) |
| 252 | } |
| 253 | names := make([]string, 0, len(adapters)) |
| 254 | for _, a := range adapters { |
| 255 | names = append(names, a.Name()) |
| 256 | // Builders create adapters with a no-op logger (they run at import |
| 257 | // time). Inject the daemon's real logger now so adapter events and |
| 258 | // every external API request land in the log. |
| 259 | if la, ok := a.(gateway.LoggerAware); ok { |
| 260 | la.SetLogger(cli.logger) |
| 261 | } |
| 262 | } |
| 263 | // Startup line so gateway.log is never empty while the daemon is live — |
| 264 | // immediate proof the advertised log is the one actually being written. |
| 265 | cli.logger.Info("gateway started", |
| 266 | zap.Strings("platforms", names), zap.Int("adapters", len(adapters))) |
| 267 | |
| 268 | sessions := newHubSessions(broker, cli.logger) |
| 269 | sessions.loadBindings(ctx) |
| 270 | |
| 271 | // Voice support: build the transcription provider once (self-hosted-first, |
| 272 | // per CHATCLI_TRANSCRIPTION_*). Null when nothing is usable — voice |
| 273 | // messages then get a friendly "enable transcription" reply instead of |
| 274 | // being silently dropped. |
| 275 | transcriber := transcription.NewFromEnv(cli.logger) |
| 276 | if !transcription.IsNull(transcriber) { |
| 277 | cli.logger.Info("gateway: voice transcription enabled", zap.String("provider", transcriber.Name())) |
| 278 | logVoicePreflight(cli.logger, transcriber) |
| 279 | } |
| 280 | // Self-provisioning backends (embedded whisper) download their engine and |
| 281 | // model on first use; kick that off now, tied to the daemon lifetime, so |
| 282 | // the one-time fetch races startup instead of the first voice note. |
| 283 | if prov, ok := transcriber.(transcription.Provisioner); ok { |
| 284 | go func() { |
| 285 | if err := prov.EnsureReady(ctx); err != nil { |
| 286 | cli.logger.Warn("gateway: transcription engine provisioning failed", zap.Error(err)) |
| 287 | } |
| 288 | }() |
| 289 | } |
| 290 | |
| 291 | imgOutbox := newGatewayImageOutbox() |
| 292 | runner := gateway.NewRunner(adapters, cli.gatewayAgentFunc(sessions, transcriber, imgOutbox), cli.logger, 0) |
| 293 | runner.SetThinkingNotice(i18n.T("gateway.thinking")) |
| 294 | runner.SetVoicePrefs(gateway.SharedVoicePrefs()) |
| 295 | cli.maybeEnableVoiceReplies(runner) |
| 296 | cli.maybeEnableImageReplies(runner, imgOutbox) |
| 297 | return runner.Run(ctx) |
no test coverage detected