chenhg5/cc-connect · warning

agent session became nil

Error message

agent session became nil

What it means

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.

Source

Thrown at core/engine.go:3856

	}

	promptContent := e.buildSenderPrompt(msg.Content, msg.UserID, msg.UserName, msg.Platform, msg.SessionKey, msg.ChannelKey)

	sendStart := time.Now()
	state.mu.Lock()
	state.currentMessageID = msg.MessageID
	state.fromVoice = msg.FromVoice
	state.sideText = ""
	as := state.agentSession // capture under lock to avoid race with cleanup
	state.mu.Unlock()

	// Run Send concurrently with processInteractiveEvents. Some agents block inside
	// Send until the prompt turn finishes (e.g. ACP session/prompt); they may emit
	// EventPermissionRequest while blocked — the event loop must run in parallel.
	sendDone := make(chan error, 1)
	go func() {
		if as == nil {
			sendDone <- fmt.Errorf("agent session became nil")
			return
		}
		sendDone <- as.Send(promptContent, msg.MessageID, msg.Images, msg.Files)
	}()

	e.processInteractiveEvents(state, session, sessions, interactiveKey, msg.MessageID, turnStart, stopTyping, sendDone, msg.ReplyCtx)
	if elapsed := time.Since(sendStart); elapsed >= slowAgentSend {
		slog.Warn("slow agent send", "elapsed", elapsed, "session", msg.SessionKey, "content_len", len(msg.Content))
	}
	stopTyping = nil // ownership transferred; prevent defer from double-stopping

	// Start unsolicited reader and arm the idle close timer BEFORE draining
	// queued messages. drainPendingMessages releases the session lock, and
	// without this ordering the next user message can race in, call
	// cancelAgentSessionIdleClose (a no-op since nothing was scheduled yet),
	// and then the late schedule below arms a timer that no subsequent cancel
	// will catch — closing the live session mid-turn. See #1686 P1-C P1-2.
	// The schedule's own state checks (agentSession nil, stopped, etc.) and

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Retry the message after the session resets — a new session will be created
  2. Check ordering of /stop or /reset relative to message sends
  3. Add a nil check before scheduling sends

Example fix

// before
nextSend <- as.Send(queuedPrompt, ...)
// after
if as == nil { nextSend <- fmt.Errorf("agent session became nil"); return }
Defensive patterns

Strategy: try-catch

Type guard

if as == nil { return fmt.Errorf("agent session became nil") }

Try / catch

if err := <-sendDone; err != nil && strings.Contains(err.Error(), "became nil") { recreateSessionAndResend(prompt) }

Prevention

When it happens

Trigger: state.agentSession captured under lock was already nil (cleanup ran first) while a prompt send goroutine starts.

Common situations: User sends a message at the same moment the session is being stopped/reset; race between cleanup and the send path.

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/e2e907688c4d2200. Report an issue: GitHub.