chenhg5/cc-connect · error

session is closed

Error message

session is closed

What it means

geminiSession.Send checks the session's atomic `alive` flag before doing anything; if the session has been closed or its process exited, it rejects the prompt with `session is closed`. This prevents sending into a dead process and surfaces stale-session usage early.

Source

Thrown at agent/gemini/session.go:71

		mode:      mode,
		timeout:   timeout,
		extraEnv:  extraEnv,
		events:    make(chan core.Event, 64),
		ctx:       sessionCtx,
		cancel:    cancel,
	}
	gs.alive.Store(true)

	if resumeID != "" && resumeID != core.ContinueSession {
		gs.chatID.Store(resumeID)
	}

	return gs, nil
}

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

	// Save images and files into the workspace so Gemini CLI tools can access them.
	attachDir := filepath.Join(gs.workDir, ".cc-connect", "attachments")
	if (len(images) > 0 || len(files) > 0) && os.MkdirAll(attachDir, 0o755) != nil {
		attachDir = os.TempDir()
	}

	var imageRefs []string
	for i, img := range images {
		ext := ".png"
		switch img.MimeType {
		case "image/jpeg":
			ext = ".jpg"
		case "image/gif":
			ext = ".gif"
		case "image/webp":
			ext = ".webp"

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Check session liveness/events before sending, or catch this error and start a new session
  2. Inspect logs for why the previous process exited (timeout, crash, stderr output)
  3. Increase timeoutMins in the gemini agent config if sessions expire too early
  4. Implement automatic session recreation on this error in the calling layer

Example fix

// before
err := sess.Send(prompt, msgID, nil, nil) // fails: session is closed
// after
if err := sess.Send(prompt, msgID, nil, nil); err != nil && strings.Contains(err.Error(), "session is closed") {
    sess, err = agent.StartSession(ctx, workDir, model)
    if err == nil { err = sess.Send(prompt, msgID, nil, nil) }
}
Defensive patterns

Strategy: validation

Validate before calling

// track liveness via events before sending
closed := false
go func() { for evt := range sess.Events() { if evt.Type == core.EventError || evt.Type == core.EventExit { closed = true } } }()
if closed { sess, _ = agent.StartSession(ctx, workDir, model) }

Try / catch

if err := sess.Send(prompt, id, nil, nil); err != nil && err.Error() == "session is closed" {
  sess, serr := agent.StartSession(ctx, workDir, model)
  if serr != nil { return serr }
  return sess.Send(prompt, id, nil, nil)
}

Prevention

When it happens

Trigger: Sending a prompt on a geminiSession after the CLI process exited (crash, timeout, natural completion), after an explicit close/Stop, or after the context was cancelled — any state where alive.Load() is false.

Common situations: Engine keeps a session reference after the gemini CLI timed out; user resumes a session that hit the configured timeout; crash of the CLI marked the session dead but the UI still routes messages to it.

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