{"record":{"id":"da01f4810e6944e1","repo":"chenhg5/cc-connect","slug":"discord-platform-stopped","errorCode":null,"errorMessage":"discord: platform stopped","messagePattern":"discord: platform stopped","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/discord/discord.go","lineNumber":550,"sourceCode":"func (p *Platform) SetLifecycleHandler(h core.PlatformLifecycleHandler) {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\tp.lifecycleHandler = h\n}\n\n// Start launches the gateway connection in the background. It returns nil as\n// soon as the recovery loop is running; the caller should treat\n// OnPlatformReady as the signal that the platform is actually usable.\n//\n// Before this change Start returned the first session.Open() error directly,\n// which meant a transient proxy/network blip during cc-connect startup\n// permanently took Discord offline until manual restart (release-gate\n// 2026-06-14: \"discord: open gateway: ... EOF\" with no retry).\nfunc (p *Platform) Start(handler core.MessageHandler) error {\n\tp.mu.Lock()\n\tif p.stopping {\n\t\tp.mu.Unlock()\n\t\treturn fmt.Errorf(\"discord: platform stopped\")\n\t}\n\tp.handler = handler\n\tctx, cancel := context.WithCancel(context.Background())\n\tp.cancel = cancel\n\tp.mu.Unlock()\n\n\tgo p.connectLoop(ctx)\n\treturn nil\n}\n\n// buildSession creates a fresh discordgo session with proxy + handlers wired up.\n// It is called once per connect attempt so a session that failed mid-handshake\n// is fully discarded before the next retry.\nfunc (p *Platform) buildSession() (*discordgo.Session, error) {\n\tsession, err := discordgo.New(\"Bot \" + p.token)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"discord: create session: %w\", err)\n\t}","sourceCodeStart":532,"sourceCodeEnd":568,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/discord/discord.go#L532-L568","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Construct a fresh Platform instance (NewPlatform with config) instead of calling Start on a previously stopped one.","Audit the lifecycle: never call Start concurrently with or after Stop; use a restart method that resets the stopping flag if restarts are intended.","Serialize lifecycle calls with a mutex/sync.Once in the caller (daemon/supervisor layer).","If restart support is needed, add an explicit Reset/restart API rather than flipping p.stopping back."],"exampleFix":"// before\np.Stop()\nerr := p.Start(handler) // error: discord: platform stopped\n// after\np.Stop()\np = discord.NewPlatform(cfg) // fresh instance\nerr := p.Start(handler)","handlingStrategy":"type-guard","validationCode":"p.mu.Lock()\nstopping := p.stopping\np.mu.Unlock()\nif stopping {\n    p = discord.NewPlatform(cfg) // rebuild instead of restarting\n}","typeGuard":"func isStopped(p *Platform) bool { p.mu.Lock(); defer p.mu.Unlock(); return p.stopping }","tryCatchPattern":"if err := p.Start(handler); err != nil {\n    if strings.Contains(err.Error(), \"platform stopped\") {\n        p = discord.NewPlatform(cfg)\n        return p.Start(handler)\n    }\n    return err\n}","preventionTips":["Treat Platform as single-use: one Start, one Stop.","Serialize lifecycle transitions in the daemon layer to avoid stop/start races.","For restarts, build a new instance from config rather than reusing the stopped one."],"tags":["discord","lifecycle","invalid-state"],"backgroundTag":"invalid-state-transition","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}