diff --git a/README.de.md b/README.de.md
index 93abf27..56a9fb9 100644
--- a/README.de.md
+++ b/README.de.md
@@ -378,15 +378,15 @@ Der Bot akzeptiert derzeit:
CODEX_BIN |
- Befehl zum Starten von Codex CLI. Standard: codex. |
+ Befehl zum Starten von Codex CLI. Die Anwendung versucht beim Initialisieren der .env_coding_agent_telegram-Datei, den lokal installierten Codex-Pfad automatisch zu erkennen. Alternativ können Sie mit which codex den Pfad anzeigen. |
COPILOT_BIN |
- Befehl zum Starten von Copilot CLI. Standard: copilot. |
+ Befehl zum Starten von Copilot CLI. Die Anwendung versucht beim Initialisieren der .env_coding_agent_telegram-Datei, den lokal installierten Copilot-Pfad automatisch zu erkennen. Alternativ können Sie mit which copilot den Pfad anzeigen. |
CLAUDE_BIN |
- Befehl zum Starten von Claude Code CLI. Standard: ~/.local/bin/claude. |
+ Befehl zum Starten von Claude Code CLI. Die Anwendung versucht beim Initialisieren der .env_coding_agent_telegram-Datei, den lokal installierten Claude-Pfad automatisch zu erkennen. Alternativ können Sie mit which claude den Pfad anzeigen. |
CODEX_MODEL |
diff --git a/README.fr.md b/README.fr.md
index cb3a5cb..73df0cc 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -378,15 +378,15 @@ Le bot accepte actuellement :
CODEX_BIN |
- Commande utilisée pour lancer Codex CLI. Valeur par défaut : codex. |
+ Commande utilisée pour lancer Codex CLI. L'application essaiera de détecter automatiquement le chemin d'installation local de Codex lors de l'initialisation de .env_coding_agent_telegram. Vous pouvez également utiliser which codex pour afficher ce chemin. |
COPILOT_BIN |
- Commande utilisée pour lancer Copilot CLI. Valeur par défaut : copilot. |
+ Commande utilisée pour lancer Copilot CLI. L'application essaiera de détecter automatiquement le chemin d'installation local de Copilot lors de l'initialisation de .env_coding_agent_telegram. Vous pouvez également utiliser which copilot pour afficher ce chemin. |
CLAUDE_BIN |
- Commande utilisée pour lancer Claude Code CLI. Valeur par défaut : ~/.local/bin/claude. |
+ Commande utilisée pour lancer Claude Code CLI. L'application essaiera de détecter automatiquement le chemin d'installation local de Claude lors de l'initialisation de .env_coding_agent_telegram. Vous pouvez également utiliser which claude pour afficher ce chemin. |
CODEX_MODEL |
diff --git a/README.ja.md b/README.ja.md
index 1e9ad9c..8fbe137 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -378,15 +378,15 @@ https://api.telegram.org/bot/getUpdates
CODEX_BIN |
- Codex CLI を起動するコマンドです。既定値: codex. |
+ Codex CLI を起動するコマンドです。アプリは .env_coding_agent_telegram の初期化時に、ローカルにインストールされている Codex のパスを自動的に検出します。別の方法として、which codex を使用してパスを確認できます。 |
COPILOT_BIN |
- Copilot CLI を起動するコマンドです。既定値: copilot. |
+ Copilot CLI を起動するコマンドです。アプリは .env_coding_agent_telegram の初期化時に、ローカルにインストールされている Copilot のパスを自動的に検出します。別の方法として、which copilot を使用してパスを確認できます。 |
CLAUDE_BIN |
- Claude Code CLI を起動するコマンドです。既定値: ~/.local/bin/claude. |
+ Claude Code CLI を起動するコマンドです。アプリは .env_coding_agent_telegram の初期化時に、ローカルにインストールされている Claude のパスを自動的に検出します。別の方法として、which claude を使用してパスを確認できます。 |
CODEX_MODEL |
diff --git a/README.ko.md b/README.ko.md
index 35cd12a..014cad4 100644
--- a/README.ko.md
+++ b/README.ko.md
@@ -380,15 +380,15 @@ https://api.telegram.org/bot/getUpdates
CODEX_BIN |
- Codex CLI 를 실행할 명령입니다. 기본값: codex. |
+ Codex CLI 를 실행할 명령입니다. 앱은 .env_coding_agent_telegram를 초기화할 때 로컬에 설치된 Codex 경로를 자동으로 감지합니다. 또는 which codex를 사용하여 경로를 확인할 수 있습니다. |
COPILOT_BIN |
- Copilot CLI 를 실행할 명령입니다. 기본값: copilot. |
+ Copilot CLI 를 실행할 명령입니다. 앱은 .env_coding_agent_telegram를 초기화할 때 로컬에 설치된 Copilot 경로를 자동으로 감지합니다. 또는 which copilot를 사용하여 경로를 확인할 수 있습니다. |
CLAUDE_BIN |
- Claude Code CLI 를 실행할 명령입니다. 기본값: ~/.local/bin/claude. |
+ Claude Code CLI 를 실행할 명령입니다. 앱은 .env_coding_agent_telegram를 초기화할 때 로컬에 설치된 Claude 경로를 자동으로 감지합니다. 또는 which claude를 사용하여 경로를 확인할 수 있습니다. |
CODEX_MODEL |
diff --git a/README.md b/README.md
index 08069d7..e9b8097 100644
--- a/README.md
+++ b/README.md
@@ -389,15 +389,15 @@ The bot currently accepts:
CODEX_BIN |
- Command used to launch Codex CLI. Default: codex. |
+ Command used to launch Codex CLI. The app would try to detect the locally installed Codex path when initializing the .env_coding_agent_telegram. Alternatively use which codex to view the path. |
COPILOT_BIN |
- Command used to launch Copilot CLI. Default: copilot. |
+ Command used to launch Copilot CLI. The app would try to detect the locally installed Copilot path when initializing the .env_coding_agent_telegram. Alternatively use which copilot to view the path. |
CLAUDE_BIN |
- Command used to launch Claude Code CLI. Default: ~/.local/bin/claude. |
+ Command used to launch Claude Code CLI. The app would try to detect the locally installed Claude path when initializing the .env_coding_agent_telegram. Alternatively use which claude to view the path. |
CODEX_MODEL |
diff --git a/README.nl.md b/README.nl.md
index 8a155af..74de966 100644
--- a/README.nl.md
+++ b/README.nl.md
@@ -378,15 +378,15 @@ De bot accepteert momenteel:
CODEX_BIN |
- Commando om Codex CLI te starten. Standaard: codex. |
+ Commando om Codex CLI te starten. De applicatie probeert tijdens het initialiseren van .env_coding_agent_telegram automatisch het lokaal geïnstalleerde Codex-pad te detecteren. U kunt ook which codex gebruiken om het pad weer te geven. |
COPILOT_BIN |
- Commando om Copilot CLI te starten. Standaard: copilot. |
+ Commando om Copilot CLI te starten. De applicatie probeert tijdens het initialiseren van .env_coding_agent_telegram automatisch het lokaal geïnstalleerde Copilot-pad te detecteren. U kunt ook which copilot gebruiken om het pad weer te geven. |
CLAUDE_BIN |
- Commando om Claude Code CLI te starten. Standaard: ~/.local/bin/claude. |
+ Commando om Claude Code CLI te starten. De applicatie probeert tijdens het initialiseren van .env_coding_agent_telegram automatisch het lokaal geïnstalleerde Claude-pad te detecteren. U kunt ook which claude gebruiken om het pad weer te geven. |
CODEX_MODEL |
diff --git a/README.th.md b/README.th.md
index 5584b4e..f6d0370 100644
--- a/README.th.md
+++ b/README.th.md
@@ -378,15 +378,15 @@ https://api.telegram.org/bot/getUpdates
CODEX_BIN |
- คำสั่งที่ใช้เรียก Codex CLI ค่าเริ่มต้น: codex |
+ คำสั่งที่ใช้เรียก Codex CLI. แอปจะพยายามตรวจหาเส้นทางของ Codex ที่ติดตั้งอยู่ในเครื่องโดยอัตโนมัติระหว่างการเริ่มต้น .env_coding_agent_telegram หรือสามารถใช้ which codex เพื่อดูเส้นทางได้ |
COPILOT_BIN |
- คำสั่งที่ใช้เรียก Copilot CLI ค่าเริ่มต้น: copilot |
+ คำสั่งที่ใช้เรียก Copilot CLI. แอปจะพยายามตรวจหาเส้นทางของ Copilot ที่ติดตั้งอยู่ในเครื่องโดยอัตโนมัติระหว่างการเริ่มต้น .env_coding_agent_telegram หรือสามารถใช้ which copilot เพื่อดูเส้นทางได้ |
CLAUDE_BIN |
- คำสั่งที่ใช้เรียก Claude Code CLI ค่าเริ่มต้น: ~/.local/bin/claude |
+ คำสั่งที่ใช้เรียก Claude Code CLI. แอปจะพยายามตรวจหาเส้นทางของ Claude ที่ติดตั้งอยู่ในเครื่องโดยอัตโนมัติระหว่างการเริ่มต้น .env_coding_agent_telegram หรือสามารถใช้ which claude เพื่อดูเส้นทางได้ |
CODEX_MODEL |
diff --git a/README.vi.md b/README.vi.md
index feab410..adfbde3 100644
--- a/README.vi.md
+++ b/README.vi.md
@@ -378,15 +378,15 @@ Hiện tại bot chấp nhận:
CODEX_BIN |
- Lệnh dùng để chạy Codex CLI. Mặc định: codex. |
+ Lệnh dùng để chạy Codex CLI. Ứng dụng sẽ cố gắng tự động phát hiện đường dẫn của Codex được cài đặt trên máy khi khởi tạo .env_coding_agent_telegram. Ngoài ra, bạn có thể sử dụng which codex để xem đường dẫn. |
COPILOT_BIN |
- Lệnh dùng để chạy Copilot CLI. Mặc định: copilot. |
+ Lệnh dùng để chạy Copilot CLI. Ứng dụng sẽ cố gắng tự động phát hiện đường dẫn của Copilot được cài đặt trên máy khi khởi tạo .env_coding_agent_telegram. Ngoài ra, bạn có thể sử dụng which copilot để xem đường dẫn. |
CLAUDE_BIN |
- Lệnh dùng để chạy Claude Code CLI. Mặc định: ~/.local/bin/claude. |
+ Lệnh dùng để chạy Claude Code CLI. Ứng dụng sẽ cố gắng tự động phát hiện đường dẫn của Claude được cài đặt trên máy khi khởi tạo .env_coding_agent_telegram. Ngoài ra, bạn có thể sử dụng which claude để xem đường dẫn. |
CODEX_MODEL |
diff --git a/README.zh-CN.md b/README.zh-CN.md
index c533282..2b2c731 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -380,15 +380,15 @@ bot 当前接受:
CODEX_BIN |
- 用于启动 Codex CLI 的命令。默认:codex。 |
+ 用于启动 Codex CLI 的命令。应用程序会在初始化 .env_coding_agent_telegram 时尝试自动检测本地安装的 Codex 路径。您也可以使用 which codex 查看该路径。 |
COPILOT_BIN |
- 用于启动 Copilot CLI 的命令。默认:copilot。 |
+ 用于启动 Copilot CLI 的命令。应用程序会在初始化 .env_coding_agent_telegram 时尝试自动检测本地安装的 Copilot 路径。您也可以使用 which copilot 查看该路径。 |
CLAUDE_BIN |
- 用于启动 Claude Code CLI 的命令。默认:~/.local/bin/claude。 |
+ 用于启动 Claude Code CLI 的命令。应用程序会在初始化 .env_coding_agent_telegram 时尝试自动检测本地安装的 Claude 路径。您也可以使用 which claude 查看该路径。 |
CODEX_MODEL |
diff --git a/README.zh-HK.md b/README.zh-HK.md
index 280a28e..73c9c2a 100644
--- a/README.zh-HK.md
+++ b/README.zh-HK.md
@@ -378,15 +378,15 @@ bot 目前接受:
CODEX_BIN |
- 用來啟動 Codex CLI 的指令。預設:codex。 |
+ 用來啟動 Codex CLI 的指令。應用程式會在初始化 .env_coding_agent_telegram 時嘗試自動偵測本機已安裝的 Codex 路徑。您亦可使用 which codex 查看該路徑。 |
COPILOT_BIN |
- 用來啟動 Copilot CLI 的指令。預設:copilot。 |
+ 用來啟動 Copilot CLI 的指令。應用程式會在初始化 .env_coding_agent_telegram 時嘗試自動偵測本機已安裝的 Copilot 路徑。您亦可使用 which copilot 查看該路徑。 |
CLAUDE_BIN |
- 用來啟動 Claude Code CLI 的指令。預設:~/.local/bin/claude。 |
+ 用來啟動 Claude Code CLI 的指令。應用程式會在初始化 .env_coding_agent_telegram 時嘗試自動偵測本機已安裝的 Claude 路徑。您亦可使用 which claude 查看該路徑。 |
CODEX_MODEL |
diff --git a/README.zh-TW.md b/README.zh-TW.md
index 4e083e8..4ca3122 100644
--- a/README.zh-TW.md
+++ b/README.zh-TW.md
@@ -380,15 +380,15 @@ bot 目前接受:
CODEX_BIN |
- 用來啟動 Codex CLI 的指令。預設:codex。 |
+ 用來啟動 Codex CLI 的指令。應用程式會在初始化 .env_coding_agent_telegram 時嘗試自動偵測本機安裝的 Codex 路徑。您也可以使用 which codex 查看該路徑。 |
COPILOT_BIN |
- 用來啟動 Copilot CLI 的指令。預設:copilot。 |
+ 用來啟動 Copilot CLI 的指令。應用程式會在初始化 .env_coding_agent_telegram 時嘗試自動偵測本機安裝的 Copilot 路徑。您也可以使用 which copilot 查看該路徑。 |
CLAUDE_BIN |
- 用來啟動 Claude Code CLI 的指令。預設:~/.local/bin/claude。 |
+ 用來啟動 Claude Code CLI 的指令。應用程式會在初始化 .env_coding_agent_telegram 時嘗試自動偵測本機安裝的 Claude 路徑。您也可以使用 which claude 查看該路徑。 |
CODEX_MODEL |
diff --git a/src/coding_agent_telegram/cli.py b/src/coding_agent_telegram/cli.py
index d293a72..232ce31 100644
--- a/src/coding_agent_telegram/cli.py
+++ b/src/coding_agent_telegram/cli.py
@@ -153,6 +153,14 @@ def main() -> None:
raise
store = SessionStore(cfg.state_file, cfg.state_backup_file)
+ cleared_pending_actions = store.clear_all_pending_actions()
+ if cleared_pending_actions:
+ logger.warning(
+ "Cleared %d stale pending_action entr%s left over from a previous run "
+ "(likely an unclean shutdown mid-message); affected chats can send messages again.",
+ cleared_pending_actions,
+ "y" if cleared_pending_actions == 1 else "ies",
+ )
runner = MultiAgentRunner(
codex_bin=cfg.codex_bin,
copilot_bin=cfg.copilot_bin,
diff --git a/src/coding_agent_telegram/config.py b/src/coding_agent_telegram/config.py
index 06ac0a8..aee9bf8 100644
--- a/src/coding_agent_telegram/config.py
+++ b/src/coding_agent_telegram/config.py
@@ -6,6 +6,7 @@
import os
import pwd
import re
+import shutil
import locale as system_locale
from dataclasses import dataclass
from pathlib import Path
@@ -134,6 +135,59 @@ def _apply_initial_app_locale(template_text: str, app_locale: str) -> str:
return f"{replacement}\n{template_text}"
+# Fallback install locations checked when a CLI isn't on PATH, e.g. because the
+# process runs under a service manager or a shell profile that startup.sh
+# doesn't source. Order matters: earlier entries win when a bin exists in
+# more than one of them.
+_BIN_CANDIDATE_DIRS = (
+ "~/.local/bin",
+ "~/bin",
+ "/opt/homebrew/bin",
+ "/usr/local/bin",
+ "/usr/bin",
+ "~/.npm-global/bin",
+ "~/.volta/bin",
+)
+
+
+def detect_installed_bin(bin_name: str) -> Optional[str]:
+ """Return the absolute path of a locally installed CLI, or None if not found.
+
+ Deliberately does not resolve symlinks to their final target (e.g. a
+ version manager's ``.local/bin/claude`` -> ``.local/share/claude/versions/x.y.z``):
+ keeping the symlink path lets in-place CLI upgrades keep working without
+ editing the env file again.
+ """
+ found = shutil.which(bin_name)
+ if found:
+ return str(Path(found).absolute())
+ for candidate_dir in _BIN_CANDIDATE_DIRS:
+ candidate = Path(candidate_dir).expanduser() / bin_name
+ if candidate.is_file() and os.access(candidate, os.X_OK):
+ return str(candidate.absolute())
+ return None
+
+
+# Maps each provider's env var name to the CLI executable it looks for.
+_PROVIDER_BIN_ENV_VARS = (
+ ("CODEX_BIN", "codex"),
+ ("COPILOT_BIN", "copilot"),
+ ("CLAUDE_BIN", "claude"),
+)
+
+
+def _apply_detected_provider_bins(template_text: str) -> str:
+ text = template_text
+ for env_var, bin_name in _PROVIDER_BIN_ENV_VARS:
+ detected_path = detect_installed_bin(bin_name)
+ if not detected_path:
+ continue
+ pattern = rf"(?m)^{env_var}=.*$"
+ if re.search(pattern, text):
+ text = re.sub(pattern, f"{env_var}={detected_path}", text, count=1)
+ return text
+
+
def create_initial_env_file(env_path: Path, template_path: Optional[Path] = None) -> str:
if template_path is None:
template_text = importlib.resources.files("coding_agent_telegram").joinpath("resources/.env.example").read_text(
@@ -143,7 +197,9 @@ def create_initial_env_file(env_path: Path, template_path: Optional[Path] = None
template_text = template_path.read_text(encoding="utf-8")
app_locale = detect_system_locale()
env_path.parent.mkdir(parents=True, exist_ok=True)
- env_path.write_text(_apply_initial_app_locale(template_text, app_locale), encoding="utf-8")
+ template_text = _apply_initial_app_locale(template_text, app_locale)
+ template_text = _apply_detected_provider_bins(template_text)
+ env_path.write_text(template_text, encoding="utf-8")
return app_locale
diff --git a/src/coding_agent_telegram/resources/.env.example b/src/coding_agent_telegram/resources/.env.example
index 168c3ab..408409c 100644
--- a/src/coding_agent_telegram/resources/.env.example
+++ b/src/coding_agent_telegram/resources/.env.example
@@ -19,14 +19,14 @@ TELEGRAM_BOT_TOKENS=
# Comma-separated allowed private chat IDs.
ALLOWED_CHAT_IDS=
-# Codex CLI executable name or path.
+# Codex CLI executable name or path, Use `which codex` to view the path
CODEX_BIN=codex
-# Copilot CLI executable name or path.
+# Copilot CLI executable name or path. Use `which copilot` to view the path
COPILOT_BIN=copilot
-# Claude Code CLI executable name or path.
-CLAUDE_BIN=~/.local/bin/claude
+# Claude Code CLI executable name or path. Use `which claude` to view the path
+CLAUDE_BIN=claude
# Optional Codex model override.
CODEX_MODEL=
diff --git a/src/coding_agent_telegram/router/git_commands.py b/src/coding_agent_telegram/router/git_commands.py
index 5009102..9fa626b 100644
--- a/src/coding_agent_telegram/router/git_commands.py
+++ b/src/coding_agent_telegram/router/git_commands.py
@@ -21,7 +21,10 @@ class GitCommandMixin:
'Only include files you intentionally modified for this task. '
'Do not include unrelated changed files. '
'Do not include untracked files unless they were created for this task and are clearly required. '
- 'Output only a single executable command in this format with \\ if there is line break: git add && git commit -m "".'
+ 'Write the message as plain text: use one -m "" flag per summary or bullet line (git joins multiple -m values into separate paragraphs automatically) instead of embedding literal newlines inside a single -m value. '
+ 'Do not use $(...), backticks, heredocs (< && git commit -m "" -m "" -m "" ... '
+ 'If the file list is long, you may wrap it across lines with a trailing \\ for readability, but keep every -m value on its own single line.'
)
@staticmethod
diff --git a/src/coding_agent_telegram/session_store.py b/src/coding_agent_telegram/session_store.py
index 58fd867..e32ba9b 100644
--- a/src/coding_agent_telegram/session_store.py
+++ b/src/coding_agent_telegram/session_store.py
@@ -206,6 +206,28 @@ def mutate(chat_data: dict[str, Any]) -> None:
self._mutate_chat_data(bot_id, chat_id, mutate, create=True)
+ def clear_all_pending_actions(self) -> int:
+ """Drop any persisted `pending_action` left over from a previous process.
+
+ `pending_action` marks a message as "currently being run through an agent
+ subprocess", which only ever makes sense within the process that started
+ it — in-memory locks and running-process handles never survive a restart.
+ If the process is killed or crashes mid-run, the flag stays set in the
+ state file forever, and every future incoming message gets silently
+ queued (never drained) because `_should_queue_incoming_message` treats a
+ stale pending_action the same as a real in-flight run. Call this once at
+ startup so a crash never permanently wedges a chat.
+ """
+
+ def mutate(state: dict[str, Any]) -> int:
+ cleared = 0
+ for chat_data in state.get("chats", {}).values():
+ if isinstance(chat_data, dict) and chat_data.pop("pending_action", None) is not None:
+ cleared += 1
+ return cleared
+
+ return self._mutate_state(mutate)
+
def create_session(
self,
bot_id: str,
diff --git a/tests/test_config.py b/tests/test_config.py
index a6985a4..ce2e822 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -477,6 +477,69 @@ def test_create_initial_env_file_initializes_app_locale_from_system_language(tmp
assert "APP_LOCALE=ja" in env_path.read_text(encoding="utf-8")
+def test_create_initial_env_file_fills_in_detected_provider_bin_paths(tmp_path, monkeypatch):
+ env_path = tmp_path / ".env_coding_agent_telegram"
+ template_path = tmp_path / ".env.example"
+ template_path.write_text(
+ "APP_LOCALE=en\nCODEX_BIN=codex\nCOPILOT_BIN=copilot\nCLAUDE_BIN=~/.local/bin/claude\n",
+ encoding="utf-8",
+ )
+
+ detected = {
+ "codex": "/opt/homebrew/bin/codex",
+ "claude": "/usr/local/bin/claude",
+ }
+ monkeypatch.setattr(config_module, "detect_installed_bin", lambda bin_name: detected.get(bin_name))
+
+ create_initial_env_file(env_path, template_path)
+
+ written = env_path.read_text(encoding="utf-8")
+ assert "CODEX_BIN=/opt/homebrew/bin/codex" in written
+ assert "CLAUDE_BIN=/usr/local/bin/claude" in written
+ # copilot wasn't detected, so the template value is left untouched.
+ assert "COPILOT_BIN=copilot" in written
+
+
+def test_detect_installed_bin_prefers_path_then_falls_back_to_candidate_dirs(tmp_path, monkeypatch):
+ from coding_agent_telegram.config import detect_installed_bin
+
+ monkeypatch.setattr(config_module.shutil, "which", lambda name: None)
+ fake_home = tmp_path / "home"
+ local_bin = fake_home / ".local" / "bin"
+ local_bin.mkdir(parents=True)
+ claude_bin = local_bin / "claude"
+ claude_bin.write_text("#!/bin/sh\n", encoding="utf-8")
+ claude_bin.chmod(0o755)
+ monkeypatch.setenv("HOME", str(fake_home))
+
+ assert detect_installed_bin("claude") == str(claude_bin.absolute())
+ assert detect_installed_bin("nonexistent-bin-xyz") is None
+
+
+def test_detect_installed_bin_preserves_symlink_instead_of_resolving_target(tmp_path, monkeypatch):
+ from coding_agent_telegram.config import detect_installed_bin
+
+ # Mirrors real installs where a version manager symlinks a stable path
+ # (e.g. ~/.local/bin/claude) to a versioned target that changes on upgrade.
+ versions_dir = tmp_path / "versions"
+ versions_dir.mkdir()
+ real_bin = versions_dir / "2.1.214"
+ real_bin.write_text("#!/bin/sh\n", encoding="utf-8")
+ real_bin.chmod(0o755)
+
+ path_dir = tmp_path / "path_bin"
+ path_dir.mkdir()
+ symlink_bin = path_dir / "claude"
+ symlink_bin.symlink_to(real_bin)
+
+ monkeypatch.setattr(config_module.shutil, "which", lambda name: str(symlink_bin))
+
+ detected = detect_installed_bin("claude")
+
+ assert detected == str(symlink_bin.absolute())
+ assert detected != str(real_bin)
+
+
# ---------------------------------------------------------------------------
# _parse_allowed_chat_ids: malformed value raises clear ValueError
# ---------------------------------------------------------------------------
diff --git a/tests/test_session_store.py b/tests/test_session_store.py
index bb33d92..9f26d9b 100644
--- a/tests/test_session_store.py
+++ b/tests/test_session_store.py
@@ -51,6 +51,32 @@ def test_set_pending_action_persists_and_clears(tmp_path: Path):
assert "pending_action" not in store.get_chat_state("bot-a", 123)
+def test_clear_all_pending_actions_removes_stale_entries_across_chats(tmp_path: Path):
+ state = tmp_path / "state.json"
+ backup = tmp_path / "state.json.bak"
+ store = SessionStore(state, backup)
+
+ store.set_pending_action("bot-a", 123, {"kind": "message", "user_message": "stuck"})
+ store.set_pending_action("bot-a", 456, {"kind": "message", "user_message": "also stuck"})
+ store.set_current_provider("bot-a", 789, "codex") # chat with no pending_action
+
+ cleared = store.clear_all_pending_actions()
+
+ assert cleared == 2
+ assert "pending_action" not in store.get_chat_state("bot-a", 123)
+ assert "pending_action" not in store.get_chat_state("bot-a", 456)
+
+
+def test_clear_all_pending_actions_returns_zero_when_none_present(tmp_path: Path):
+ state = tmp_path / "state.json"
+ backup = tmp_path / "state.json.bak"
+ store = SessionStore(state, backup)
+
+ store.set_current_provider("bot-a", 123, "codex")
+
+ assert store.clear_all_pending_actions() == 0
+
+
def test_load_empty_state_file_returns_default_state(tmp_path: Path):
state = tmp_path / "state.json"
backup = tmp_path / "state.json.bak"