{"record":{"id":"e2e907688c4d2200","repo":"chenhg5/cc-connect","slug":"agent-session-became-nil","errorCode":null,"errorMessage":"agent session became nil","messagePattern":"agent session became nil","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"core/engine.go","lineNumber":3856,"sourceCode":"\t}\n\n\tpromptContent := e.buildSenderPrompt(msg.Content, msg.UserID, msg.UserName, msg.Platform, msg.SessionKey, msg.ChannelKey)\n\n\tsendStart := time.Now()\n\tstate.mu.Lock()\n\tstate.currentMessageID = msg.MessageID\n\tstate.fromVoice = msg.FromVoice\n\tstate.sideText = \"\"\n\tas := state.agentSession // capture under lock to avoid race with cleanup\n\tstate.mu.Unlock()\n\n\t// Run Send concurrently with processInteractiveEvents. Some agents block inside\n\t// Send until the prompt turn finishes (e.g. ACP session/prompt); they may emit\n\t// EventPermissionRequest while blocked — the event loop must run in parallel.\n\tsendDone := make(chan error, 1)\n\tgo func() {\n\t\tif as == nil {\n\t\t\tsendDone <- fmt.Errorf(\"agent session became nil\")\n\t\t\treturn\n\t\t}\n\t\tsendDone <- as.Send(promptContent, msg.MessageID, msg.Images, msg.Files)\n\t}()\n\n\te.processInteractiveEvents(state, session, sessions, interactiveKey, msg.MessageID, turnStart, stopTyping, sendDone, msg.ReplyCtx)\n\tif elapsed := time.Since(sendStart); elapsed >= slowAgentSend {\n\t\tslog.Warn(\"slow agent send\", \"elapsed\", elapsed, \"session\", msg.SessionKey, \"content_len\", len(msg.Content))\n\t}\n\tstopTyping = nil // ownership transferred; prevent defer from double-stopping\n\n\t// Start unsolicited reader and arm the idle close timer BEFORE draining\n\t// queued messages. drainPendingMessages releases the session lock, and\n\t// without this ordering the next user message can race in, call\n\t// cancelAgentSessionIdleClose (a no-op since nothing was scheduled yet),\n\t// and then the late schedule below arms a timer that no subsequent cancel\n\t// will catch — closing the live session mid-turn. See #1686 P1-C P1-2.\n\t// The schedule's own state checks (agentSession nil, stopped, etc.) and","sourceCodeStart":3838,"sourceCodeEnd":3874,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/core/engine.go#L3838-L3874","documentation":"Sent through the sendDone channel when a captured agentSession reference is nil at the moment a queued prompt is about to be sent. Guards a race where cleanup nils the session between capture and use.","triggerScenarios":"state.agentSession captured under lock was already nil (cleanup ran first) while a prompt send goroutine starts.","commonSituations":"User sends a message at the same moment the session is being stopped/reset; race between cleanup and the send path.","solutions":["Retry the message after the session resets — a new session will be created","Check ordering of /stop or /reset relative to message sends","Add a nil check before scheduling sends"],"exampleFix":"// before\nnextSend <- as.Send(queuedPrompt, ...)\n// after\nif as == nil { nextSend <- fmt.Errorf(\"agent session became nil\"); return }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"if as == nil { return fmt.Errorf(\"agent session became nil\") }","tryCatchPattern":"if err := <-sendDone; err != nil && strings.Contains(err.Error(), \"became nil\") { recreateSessionAndResend(prompt) }","preventionTips":["Serialize sends with session lifecycle transitions under the state mutex","Always capture agentSession under lock and nil-check before use","Test message-send vs /stop races in CI"],"tags":["go","race","session"],"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"}