chenhg5/cc-connect · error

discord: platform stopped

Error message

discord: platform stopped

What it means

Platform.Start checks a p.stopping flag under mutex; if a Stop() was already issued, Start refuses to boot the bot and returns "discord: platform stopped". This guard exists so a platform that was permanently stopped (or is shutting down) is never resurrected with a stale handler — a fix for a past release-gate bug where Discord stayed offline until manual restart.

Source

Thrown at platform/discord/discord.go:550

func (p *Platform) SetLifecycleHandler(h core.PlatformLifecycleHandler) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.lifecycleHandler = h
}

// Start launches the gateway connection in the background. It returns nil as
// soon as the recovery loop is running; the caller should treat
// OnPlatformReady as the signal that the platform is actually usable.
//
// Before this change Start returned the first session.Open() error directly,
// which meant a transient proxy/network blip during cc-connect startup
// permanently took Discord offline until manual restart (release-gate
// 2026-06-14: "discord: open gateway: ... EOF" with no retry).
func (p *Platform) Start(handler core.MessageHandler) error {
	p.mu.Lock()
	if p.stopping {
		p.mu.Unlock()
		return fmt.Errorf("discord: platform stopped")
	}
	p.handler = handler
	ctx, cancel := context.WithCancel(context.Background())
	p.cancel = cancel
	p.mu.Unlock()

	go p.connectLoop(ctx)
	return nil
}

// buildSession creates a fresh discordgo session with proxy + handlers wired up.
// It is called once per connect attempt so a session that failed mid-handshake
// is fully discarded before the next retry.
func (p *Platform) buildSession() (*discordgo.Session, error) {
	session, err := discordgo.New("Bot " + p.token)
	if err != nil {
		return nil, fmt.Errorf("discord: create session: %w", err)
	}

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Construct a fresh Platform instance (NewPlatform with config) instead of calling Start on a previously stopped one.
  2. Audit the lifecycle: never call Start concurrently with or after Stop; use a restart method that resets the stopping flag if restarts are intended.
  3. Serialize lifecycle calls with a mutex/sync.Once in the caller (daemon/supervisor layer).
  4. If restart support is needed, add an explicit Reset/restart API rather than flipping p.stopping back.

Example fix

// before
p.Stop()
err := p.Start(handler) // error: discord: platform stopped
// after
p.Stop()
p = discord.NewPlatform(cfg) // fresh instance
err := p.Start(handler)
Defensive patterns

Strategy: type-guard

Validate before calling

p.mu.Lock()
stopping := p.stopping
p.mu.Unlock()
if stopping {
    p = discord.NewPlatform(cfg) // rebuild instead of restarting
}

Type guard

func isStopped(p *Platform) bool { p.mu.Lock(); defer p.mu.Unlock(); return p.stopping }

Try / catch

if err := p.Start(handler); err != nil {
    if strings.Contains(err.Error(), "platform stopped") {
        p = discord.NewPlatform(cfg)
        return p.Start(handler)
    }
    return err
}

Prevention

When it happens

Trigger: Calling Start after Stop() on the same Platform instance, a lifecycle race where the daemon's shutdown path ran before (or concurrently with) a restart attempt, or reusing a stopped Platform object instead of constructing a new one.

Common situations: Config-reload/restart logic calling Stop then Start on the same instance where Stop set stopping=true permanently, tests reusing a stopped platform, shutdown signal handlers racing with reconnection logic.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/da01f4810e6944e1. Report an issue: GitHub.