From 04736f5e97ba778c9de9aaa2bfdd0af12f4d8a22 Mon Sep 17 00:00:00 2001 From: Mustapha Date: Wed, 1 Apr 2026 13:53:30 +0100 Subject: [PATCH] fix: restore resume for Gemini (project name dir) and OpenCode (SQLite) Gemini CLI stores sessions under ~/.gemini/tmp//chats/ but CCB was looking for ~/.gemini/tmp//chats/. The hash directory never exists, so has_history was always False and --resume latest was never passed. Fix: try both sha256 and Path(cwd).name to support all versions. OpenCode migrated from JSON session files to an SQLite database (opencode.db). _opencode_resume_allowed() returned False early when storage/session/ didn't exist, before reaching any fallback. Fix: check the SQLite database first (querying sessions by directory), then fall back to the legacy JSON file scan for older installations. Both fixes confirmed working locally against Gemini CLI and OpenCode. --- ccb | 66 +++++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/ccb b/ccb index eddd1fbb..8bbe9ef3 100755 --- a/ccb +++ b/ccb @@ -1827,20 +1827,26 @@ class AILauncher: if not candidate or candidate in seen: continue seen.add(candidate) - project_hash = hashlib.sha256(candidate.encode()).hexdigest() - chats_dir = gemini_root / project_hash / "chats" - if not chats_dir.exists(): - continue - session_files = list(chats_dir.glob("session-*.json")) - if session_files: - resume_dir = None - try: - p = Path(candidate) - if p.is_dir(): - resume_dir = p - except Exception: + # Gemini CLI has used both sha256(cwd) and Path(cwd).name as the directory name. + # Try both to support all versions. + dir_names = [ + hashlib.sha256(candidate.encode()).hexdigest(), + Path(candidate).name, + ] + for dir_name in dir_names: + chats_dir = gemini_root / dir_name / "chats" + if not chats_dir.exists(): + continue + session_files = list(chats_dir.glob("session-*.json")) + if session_files: resume_dir = None - return project_hash, True, resume_dir + try: + p = Path(candidate) + if p.is_dir(): + resume_dir = p + except Exception: + resume_dir = None + return dir_name, True, resume_dir return None, False, None @@ -1923,6 +1929,36 @@ class AILauncher: return cmd def _opencode_resume_allowed(self) -> bool: + target_dir = _normalize_path_for_match(str(self.project_root)) + if not target_dir: + return False + + # Newer OpenCode versions store sessions in an SQLite database. + xdg = (os.environ.get("XDG_DATA_HOME") or "").strip() + db_candidates = [] + if xdg: + db_candidates.append(Path(xdg) / "opencode/opencode.db") + db_candidates.append(Path.home() / ".local/share/opencode/opencode.db") + for db_path in db_candidates: + if not db_path.exists(): + continue + try: + import sqlite3 + conn = sqlite3.connect(str(db_path)) + try: + row = conn.execute( + "SELECT id FROM session WHERE directory = ? AND time_archived IS NULL " + "ORDER BY time_updated DESC LIMIT 1", + (target_dir,), + ).fetchone() + if row: + return True + finally: + conn.close() + except Exception: + pass + + # Older OpenCode versions store sessions as JSON files. try: from opencode_comm import OPENCODE_STORAGE_ROOT except Exception: @@ -1933,10 +1969,6 @@ class AILauncher: if not sessions_root.exists(): return False - target_dir = _normalize_path_for_match(str(self.project_root)) - if not target_dir: - return False - def _load_json(path: Path) -> dict: try: raw = path.read_text(encoding="utf-8")