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
- Run the same command once in the foreground to see its real exit code and stderr before marking it persist:true
- Add a readiness check during the turn (poll status/output) so a failing service surfaces before commit, not at handoff
- If the service daemonizes, run it in foreground mode under the manager so the tracked child stays alive
- 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
- Smoke-test persist:true commands in the foreground before persisting them
- Add a readiness probe during the turn so failures surface before handoff
- Never mark forking/daemonizing commands persist:true — run them foreground
- A clean exit code 0 in the message means the command was never actually long-running
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
- Persistent service {id} disappeared before commit
- Persistent service {id} has no releasable process id
- Persistent service {id} disappeared during commit
- Persistent service {id} lost its process id
- ${name} is unavailable in Workflow scripts: runs must be det
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/92e7fe8c8105069a.
Report an issue: GitHub.