Hmbown/CodeWhale · error · anyhow::Error

Persistent service {id} exited before ownership transfer (st

Error message

Persistent service {id} exited before ownership transfer (status {:?}, exit code {:?})

What it means

During the ownership-transfer commit, each `persist:true` service is polled and must still be `ShellStatus::Running`; one that exited between spawn and commit aborts the handoff with its observed status and exit code. The service was declared persistent — expected to outlive the turn — so dying early means the command failed at runtime (crash, bad flag, port in use) or exited immediately by design.

Source

Thrown at crates/tui/src/tools/shell.rs:2884

    /// enclosing turn has completed successfully.
    #[cfg(unix)]
    pub fn commit_persistent_services(&mut self) -> Result<Vec<PersistentServiceReceipt>> {
        let mut ids = self
            .processes
            .iter()
            .filter(|(_, shell)| shell.ownership == ShellOwnership::PersistPending)
            .map(|(id, _)| id.clone())
            .collect::<Vec<_>>();
        ids.sort();

        for id in &ids {
            let shell = self
                .processes
                .get_mut(id)
                .ok_or_else(|| anyhow!("Persistent service {id} disappeared before commit"))?;
            shell.poll();
            if shell.status != ShellStatus::Running {
                return Err(anyhow!(
                    "Persistent service {id} exited before ownership transfer (status {:?}, exit code {:?})",
                    shell.status,
                    shell.exit_code
                ));
            }
            if shell
                .child
                .as_ref()
                .and_then(ShellChild::process_id)
                .is_none()
            {
                return Err(anyhow!(
                    "Persistent service {id} has no releasable process id"
                ));
            }
        }

        let mut receipts = Vec::with_capacity(ids.len());

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Run the same command once in the foreground to see its real exit code and stderr before marking it persist:true
  2. Add a readiness check during the turn (poll status/output) so a failing service surfaces before commit, not at handoff
  3. If the service daemonizes, run it in foreground mode under the manager so the tracked child stays alive
  4. Read the status/exit code embedded in the message: a non-zero code means crash/misconfig, code 0 means the command completed by design and should not be persistent

Example fix

// before: blind commit after the turn
manager.spawn_background(/* persist: true */ /* ... */)?;
let receipts = manager.commit_persistent_services()?;

// after: prove the service is still running before handoff
for job in manager.list_jobs() {
    if job.status != ShellStatus::Running {
        anyhow::bail!("service {} exited early (status {:?}); check its output", job.id, job.status);
    }
}
let receipts = manager.commit_persistent_services()?;
Defensive patterns

Strategy: validation

Validate before calling

for job in manager.list_jobs() {
    if job.status != ShellStatus::Running {
        anyhow::bail!("service {} exited early (status {:?}); capture its output before commit", job.id, job.status);
    }
}
let receipts = manager.commit_persistent_services()?;

Type guard

fn all_services_running(manager: &mut ShellManager) -> bool {
    manager.list_jobs().iter().all(|job| job.status == ShellStatus::Running)
}

Try / catch

match manager.commit_persistent_services() {
    Ok(receipts) => Ok(receipts),
    Err(err) if err.to_string().contains("exited before ownership transfer") => {
        // read the embedded status/exit code, surface the service's own failure,
        // fix the service (port/args/env) and re-spawn — retrying commit alone cannot help
        Err(anyhow!("persistent service failed at runtime: {err}"))
    }
    Err(err) => Err(err),
}

Prevention

When it happens

Trigger: Spawning a `persist:true` background command whose process exits quickly — missing binary, invalid arguments, port already bound, missing env — then the turn completing successfully so the exec host calls `commit_persistent_services`.

Common situations: Dev servers marked persist:true in an environment where the port is taken or dependencies are missing; daemons that fork and exit the tracked child while the real service detaches; short-lived commands mistakenly flagged persistent (clean exit code 0).

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/92e7fe8c8105069a. Report an issue: GitHub.