chenhg5/cc-connect · warning

session is closed

Error message

session is closed

What it means

This error is returned by appServerSession.Send when the session's alive flag is false, i.e. Close() has already been called or the session terminated. Sending on a closed session is rejected up front so writes never target dead pipes. It is a state error, not a transport error — the caller attempted to use a session past its lifetime.

Source

Thrown at agent/codex/appserver_session.go:446

	defer s.runtimeMu.RUnlock()
	return cloneContextUsage(s.context)
}

func (s *appServerSession) storeUsage(report *core.UsageReport) {
	s.runtimeMu.Lock()
	defer s.runtimeMu.Unlock()
	s.usage = cloneUsageReport(report)
}

func (s *appServerSession) storeContextUsage(usage *core.ContextUsage) {
	s.runtimeMu.Lock()
	defer s.runtimeMu.Unlock()
	s.context = cloneContextUsage(usage)
}

func (s *appServerSession) Send(prompt string, messageID string, images []core.ImageAttachment, files []core.FileAttachment) error {
	if !s.alive.Load() {
		return fmt.Errorf("session is closed")
	}

	if len(files) > 0 {
		filePaths := core.SaveFilesToDisk(s.workDir, messageID, files)
		prompt = core.AppendFileRefs(prompt, filePaths)
	}

	prompt, imagePaths, err := s.stageImages(prompt, images)
	if err != nil {
		return err
	}

	s.stateMu.Lock()
	if !s.preambleSent {
		prompt = prependCodexPromptPreamble(prompt, s.promptPreamble)
		s.preambleSent = true
	}
	s.stateMu.Unlock()

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Check session liveness before sending (or rely on this error to trigger re-creating the session via StartSession).
  2. Serialize session teardown vs. sends with the engine's session lock so in-flight messages are drained before Close.
  3. If the error is from a race in the engine, treat it as benign: drop the message and notify the user the session ended.
  4. Re-establish the session: create a new session (with resumeID of the old thread if history should continue) and retry the prompt.

Example fix

// before
err := session.Send(prompt, messageID, nil, nil)
if err != nil {
    return err
}
// after
err := session.Send(prompt, messageID, nil, nil)
if err != nil {
    if strings.Contains(err.Error(), "session is closed") {
        session, err = agent.StartSession(ctx, resumeOpts) // recreate
        if err == nil {
            err = session.Send(prompt, messageID, nil, nil)
        }
    }
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Track session liveness in the caller before sending
if closed.Load() {
    return errors.New("session already closed; recreate before sending")
}

Try / catch

if err := session.Send(ctx, prompt); err != nil {
    if strings.Contains(err.Error(), "session is closed") {
        // recreate and retry once, or notify the user the session ended
        newSession, serr := agent.StartSession(ctx, opts)
        if serr == nil {
            return newSession.Send(ctx, prompt)
        }
    }
    return err
}

Prevention

When it happens

Trigger: Calling Send on an appServerSession after Close() (or Stop) completed; a queued/async message lands on a session another goroutine just closed; engine retries a prompt after the session was torn down due to a previous error.

Common situations: Concurrent message handling where a user cancels/closes the session while a message is in flight; engine not refreshing session references after session expiry; daemon shutdown racing an inbound platform message.

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