package main import ( "context" "fmt" "path/filepath" "strings" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/iot" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "git.yeij.top/AskaEth/Cyrene_v2/internal/platform" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/exec" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/task" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/automations" "git.yeij.top/AskaEth/Cyrene_v2/internal/services/backthink" ) // wireAuxiliary builds the execution provider, task manager, IoT manager, // channel session manager, automations and background thinking services. func (a *app) wireAuxiliary() error { provider, err := exec.New(a.cfg.ExecSandbox, a.log) if err != nil { return fmt.Errorf("exec provider: %w", err) } a.execProv = provider tmgr, err := task.New(a.cfg, provider, a.log, task.WithBus(a.bus)) if err != nil { return fmt.Errorf("task manager: %w", err) } a.tasks = tmgr iotMgr, err := iot.New(a.cfg.IoT, a.log, iot.WithBus(a.bus)) if err != nil { return fmt.Errorf("iot: %w", err) } a.iot = iotMgr sess, err := session.New(context.Background(), a.db, a.log) if err != nil { return fmt.Errorf("session manager: %w", err) } a.sessions = sess hist, herr := history.NewStore(context.Background(), a.db, a.log) if herr != nil { return fmt.Errorf("history store: %w", herr) } a.history = hist automation, err := automations.NewService(automations.Config{}, automations.Deps{ Bus: a.bus, Loop: a.loop, Log: logger.WithSource(a.log, "services/automations"), }) if err != nil { return fmt.Errorf("automations: %w", err) } a.automation = automation bt, err := backthink.New(backthink.ConfigFromBackground(a.cfg.Background), backthink.Deps{ Bus: a.bus, Thinker: backthink.NewLoopThinker(a.loop, a.history), Recent: a.history, State: backthink.NewIoTStateProvider(iotMgr, a.log), Exporter: backthink.NewPushOutlet(a.pushOutletConfig()), DB: a.db, Log: logger.WithSource(a.log, "services/backthink"), }) if err != nil { return fmt.Errorf("backthink: %w", err) } a.backthink = bt a.wireVoice() a.wireMCP() a.wireSkills() a.wireTerminalTool() a.wireSubagentTool() a.wireSessionReadTool() return nil } // pushOutletConfig maps the environment push-outlet block into the backthink // exporter config. The Getui credentials are read by config but not consumed: // the fallback SDK is not integrated yet, so the outlet logs the pending tier // instead of faking a push. The token is passed through and never logged. func (a *app) pushOutletConfig() backthink.PushOutletConfig { po := a.cfg.PushOutlet return backthink.PushOutletConfig{ Enabled: po.Enabled, URL: po.URL, Token: po.Token, Timeout: time.Duration(po.TimeoutSec) * time.Second, Log: a.log, } } // close shuts down every subsystem in dependency order and releases resources. func (a *app) close(ctx context.Context) { if a.backthink != nil { _ = a.backthink.Stop(ctx) } if a.automation != nil { _ = a.automation.Stop(ctx) } if a.channelBridge != nil { a.channelBridge.Stop(ctx) } if a.scheduler != nil { _ = a.scheduler.CancelAll(ctx) } if a.gw != nil { a.gw.Close() } for _, reg := range a.mcpRegs { _ = reg.Unregister(ctx) } if a.plugins != nil { a.plugins.Close(ctx) } if a.tasks != nil { _ = a.tasks.Close() } if a.vindex != nil { _ = a.vindex.Close() } if a.bus != nil { _ = a.bus.Close() } if a.db != nil { _ = a.db.Close() } a.log.Info("cyrene shutdown complete") } // resolveDataDir returns the configured data directory (from .env / env, via // cfg.DataDir), falling back to the OS-appropriate default only when no // directory is configured. func resolveDataDir(cfg *config.Config) string { if cfg.DataDir != "" { return cfg.DataDir } return platform.DefaultDataDir() } // resolveSQLitePath returns the SQLite path to use, deriving it under the data // directory when the configured default is left untouched. func resolveSQLitePath(cfg *config.Config, dataDir string) string { p := cfg.DB.SQLitePath if strings.TrimSpace(p) == "" || p == defaultSQLitePath { return filepath.Join(dataDir, "cyrene.db") } return p }