chenhg5/cc-connect · error

tmux: session closed

Error message

tmux: session closed

What it means

tmuxSession.Send first checks the atomic alive flag; if the session has been closed (or was never fully started), it returns 'tmux: session closed' without touching tmux. The tmux agent models a persistent terminal, so a closed session is terminal — messages cannot be delivered to it.

Source

Thrown at agent/tmux/session.go:77

	s := &tmuxSession{
		target:          target,
		sessionID:       sessionID,
		workDir:         workDir,
		promptPat:       pat,
		pollInt:         pollInt,
		stripInputBlock: stripInputBlock,
		stripPatterns:   stripPats,
		events:          make(chan core.Event, 128),
		ctx:             sessCtx,
		cancel:          cancel,
	}
	s.alive.Store(true)
	return s, nil
}

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

	// Save attached files and append their paths to the prompt
	if len(files) > 0 {
		paths := core.SaveFilesToDisk(s.workDir, messageID, files)
		if len(paths) > 0 {
			prompt = prompt + "\n# files: " + strings.Join(paths, ", ")
		}
	}

	// Cancel any running poll from a previous Send
	s.mu.Lock()
	if s.pollCancel != nil {
		s.pollCancel()
		s.pollCancel = nil
	}

	// Snapshot the full scrollback (history + visible pane) before sending.

View on GitHub (pinned to 4000b2338a)

Solutions

  1. Verify session lifecycle: only send to the session returned by the current StartSession/lookup, not a stale reference
  2. Handle the error by prompting the user to start a new session (or auto-start one) instead of retrying Send on the closed object
  3. If the race is between teardown and in-flight messages, synchronize with the engine so messages are rejected/redirected before Stop() completes

Example fix

// caller side
if err := sess.Send(prompt, msgID, nil, files); err != nil {
    if strings.Contains(err.Error(), "session closed") {
        sess, err = agent.StartSession(ctx, opts) // recreate
    }
}
Defensive patterns

Strategy: type-guard

Validate before calling

// before sending, ensure the session is current
if sess == nil || sess.ID() != engine.CurrentSessionID(chatID) {
    return fmt.Errorf("stale session reference")
}

Type guard

func isSessionClosed(err error) bool {
    return err != nil && strings.Contains(err.Error(), "tmux: session closed")
}

Try / catch

// Go
if err := sess.Send(prompt, id, nil, files); isSessionClosed(err) {
    newSess, serr := agent.StartSession(ctx, opts)
    if serr == nil {
        err = newSess.Send(prompt, id, nil, files)
    }
}

Prevention

When it happens

Trigger: Calling Send on a tmuxSession after Stop() was called, after the underlying context was canceled (agent shutdown, engine teardown, /new or /switch closing the session), or on a session whose creation path failed to set alive.

Common situations: Engine still routing a queued user message to a session the user just closed; a race between the platform handler and session teardown; reusing a stored session reference after a reconnect created a new session object.

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