Skip to content

Bug: ensure_pane() trusts stale pane_id without title verification — messages sent to wrong process #93

@bookandlover

Description

@bookandlover

Summary

After CCB restarts, ensure_pane() in gaskd_session.py can route messages to the wrong tmux pane because it trusts cached pane_id values without verifying ownership.

Root Cause

ensure_pane() L85-93 has a fast path:

pane_id = self.pane_id
if pane_id and backend.is_alive(pane_id):
    self._attach_pane_log(backend, pane_id)
    return True, pane_id  # ← returns immediately, no ownership check

This skips pane_title_marker verification entirely. When tmux recycles a pane ID (e.g., %20) to a different process (like Claude itself), is_alive() returns True, and messages are delivered to the wrong target.

Three-layer failure chain

  1. Stale pane_id: CCB restart creates new panes with new IDs, but .gemini-session / .codex-session still cache old pane_id
  2. title_marker bypass: The fast path at L91 returns before reaching the find_pane_by_title_marker fallback at L95-104
  3. tmux ID recycling: Old pane IDs get reassigned to other processes. is_alive() returns True because the pane exists — just not for the intended provider

Observed Behavior

  • ask gemini "..." silently delivered messages to Claude's own pane
  • Gemini never received the message, no response came back
  • User had to manually discover the mismatch via tmux list-panes and update session files
  • This happened repeatedly during a full work session, severely disrupting productivity

Real-world pane state example

# Session file says pane_id=%20 for Gemini
# But after CCB restart:
%20 ◇  Ready (BitXiongServer)    ← Claude's pane (recycled ID)
%56 ◇  Ready (BitXiongServer)    ← actual Gemini pane (new ID)
%57 CCB-Codex                    ← Codex pane (title marker present)

Suggested Fix

Add title marker verification to the fast path. When pane_id is alive but find_pane_by_title_marker resolves to a different pane, update the cached pane_id:

pane_id = self.pane_id
marker = self.pane_title_marker
resolver = getattr(backend, "find_pane_by_title_marker", None)

if pane_id and backend.is_alive(pane_id):
    if marker and callable(resolver):
        resolved = resolver(marker)
        if resolved and str(resolved) != str(pane_id):
            # Stale: pane alive but belongs to another process
            if backend.is_alive(str(resolved)):
                self.data["pane_id"] = str(resolved)
                self.data["updated_at"] = _now_str()
                self._write_back()
                self._attach_pane_log(backend, str(resolved))
                return True, str(resolved)
        else:
            self._attach_pane_log(backend, pane_id)
            return True, pane_id
    else:
        self._attach_pane_log(backend, pane_id)
        return True, pane_id

Additional Note

Gemini's pane title often shows "Ready (ProjectName)" instead of "CCB-Gemini", which also causes find_pane_by_title_marker to fail as a fallback. This is a secondary issue — the Gemini CLI overrides the title set by CCB's set_pane_title().

Environment

  • CCB version: v5.2.4 (ce20d5c)
  • Platform: macOS (Darwin 25.3.0)
  • Terminal: tmux 3.x
  • Providers affected: Gemini (primarily), potentially Codex

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions