Hmbown/CodeWhale · error

tracked SSH Fleet worker {worker_id} has no host adapter

Error message

tracked SSH Fleet worker {worker_id} has no host adapter

What it means

`stop_worker` checks how a worker runs (fleet/executor.rs:646): the streams map says SSH (a host key is present) but `ssh_adapters` no longer holds an adapter for that key. The executor still tracks the worker yet has lost the handle needed to stop it, so cancellation fails.

Source

Thrown at crates/tui/src/fleet/executor.rs:646

            .and_then(|stream| stream.attempt.clone())
    }

    /// Stop a tracked worker at the host boundary.
    ///
    /// Operator controls run in a separate process from the foreground Fleet
    /// manager, so they communicate cancellation through the durable ledger.
    /// The manager calls this method after observing that terminal state; the
    /// executor is the only owner that can reliably reach the live local/SSH
    /// adapter handle.
    pub fn stop_worker(&mut self, worker_id: &str) -> Result<()> {
        let ssh_key = match self.streams.get(worker_id).map(|stream| &stream.host) {
            Some(WorkerStreamHost::Local) => None,
            Some(WorkerStreamHost::Ssh(key)) => Some(key.clone()),
            None => return Ok(()),
        };
        if let Some(key) = ssh_key {
            let adapter = self.ssh_adapters.get_mut(&key).ok_or_else(|| {
                anyhow::anyhow!("tracked SSH Fleet worker {worker_id} has no host adapter")
            })?;
            adapter.stop_worker(worker_id)?;
        } else {
            self.adapter.stop_worker(worker_id)?;
        }
        Ok(())
    }

    /// Stop tracking a terminal worker so the scheduler can reuse the same
    /// logical worker id for the next queued task.
    pub fn forget_worker(&mut self, worker_id: &str) {
        let Some(stream) = self.streams.remove(worker_id) else {
            return;
        };
        match stream.host {
            WorkerStreamHost::Local => {
                let _ = self.adapter.cleanup_worker(worker_id);
            }

View on GitHub (pinned to 8880682c63)

Solutions

  1. Keep each SSH host adapter registered for the lifetime of its workers; remove it only after `forget_worker`
  2. Re-create the adapter for that host key, then retry stop
  3. If the worker already terminated, stop tracking it with `forget_worker` instead
Defensive patterns

Strategy: validation

Validate before calling

// before calling stop_worker
if let Some(stream) = streams.get(worker_id) {
    if let WorkerStreamHost::Ssh(key) = &stream.host {
        anyhow::ensure!(
            ssh_adapters.contains_key(key),
            "cannot stop {worker_id}: host adapter for {key} is gone"
        );
    }
}

Try / catch

match executor.stop_worker(worker_id) {
    Err(err) if err.to_string().contains("no host adapter") => {
        // adapter handle is gone; drop tracking so the id can be reused
        executor.forget_worker(worker_id);
    }
    other => other?,
}

Prevention

When it happens

Trigger: The adapters map was cleared or pruned while the worker was still tracked; the adapter entry was removed after a reconnect or partial reset; executor state was rebuilt keeping streams but dropping adapters.

Common situations: Lifecycle bugs in custom executors; cleanup paths that drop adapters before forgetting workers; test harnesses constructing partial executor state.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/c7e20f88c07062f7. Report an issue: GitHub.