fix: prevent stale IM adapter sidecars

Telegram long polling can be consumed or disrupted by orphaned adapter sidecar processes left behind after desktop restarts or IM settings refreshes. Kill stale Unix claude-sidecar adapters processes before launching a new managed adapter sidecar so only one Telegram consumer remains active.

Constraint: Telegram Bot API long polling should have a single active consumer for a token
Rejected: Only restart the current tracked child | orphaned sidecars with PPID 1 are outside the tracked Tauri child handle
Confidence: high
Scope-risk: narrow
Directive: Keep adapter sidecar startup single-instance across platforms when adding new IM adapters
Tested: cargo fmt --check
Tested: cargo check
Tested: bun run check:native
Tested: git diff --check
Not-tested: Packaged macOS app relaunch with a real stale sidecar during automated test
This commit is contained in:
程序员阿江(Relakkes) 2026-05-05 22:20:03 +08:00
parent 54c975bc09
commit c4d4568783

View File

@ -1084,6 +1084,9 @@ fn stop_server_sidecar(app: &AppHandle) {
/// 启动 adapter sidecar。返回 Result 主要为了把"无法 spawn"和"spawn 后立刻
/// 退出(凭据缺失)"区分开 —— 后者不算错误,是正常 default 状态。
fn start_adapters_sidecar(app: &AppHandle) -> Result<CommandChild, String> {
#[cfg(unix)]
kill_stale_unix_adapter_sidecars();
let app_root = resolve_app_root(app)?;
let app_root_arg = app_root.to_string_lossy().to_string();
@ -1194,6 +1197,39 @@ fn stop_adapters_sidecar(app: &AppHandle) {
}
}
#[cfg(unix)]
fn kill_stale_unix_adapter_sidecars() {
let current_pid = std::process::id();
let Ok(output) = StdCommand::new("ps")
.args(["-axo", "pid=,command="])
.output()
else {
return;
};
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let mut parts = line.trim_start().splitn(2, char::is_whitespace);
let Some(pid_text) = parts.next() else {
continue;
};
let Some(command) = parts.next() else {
continue;
};
let Ok(pid) = pid_text.parse::<u32>() else {
continue;
};
if pid == current_pid {
continue;
}
if !command.contains("claude-sidecar") || !command.contains(" adapters") {
continue;
}
let _ = StdCommand::new("kill").arg(pid.to_string()).status();
}
}
#[cfg(target_os = "windows")]
fn kill_windows_sidecars() {
for image_name in [