Hmbown/CodeWhale · error · anyhow::Error
Persistent service {id} disappeared before commit
Error message
Persistent service {id} disappeared before commit What it means
`commit_persistent_services` (unix-only) hands still-running `persist:true` jobs to external ownership after a successful headless turn. It collects the PersistPending IDs, then re-fetches each; this error fires when a collected ID is no longer in `processes` between those two steps. With a single-threaded manager the two passes are adjacent, so this is an invariant guard: something removed the entry mid-commit (a re-entrant kill/cleanup path), not a caller input mistake.
Source
Thrown at crates/tui/src/tools/shell.rs:2881
/// Transfer every still-running `persist:true` process out of Codewhale's
/// ownership. This is called only by the real headless exec host after the
/// 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"
));
}View on GitHub (pinned to 0c42157ee5)
Solutions
- Treat as a bug in calling code: guarantee nothing calls kill/cleanup/spawn on the same ShellManager while `commit_persistent_services` runs
- Keep commit as the sole owner of the PersistPending set at the turn boundary (serialize manager access)
- If hit, capture the task_id and call stack and file it against the shell tool
- Re-derive the pending set afterwards and retry the commit if the services are still running
Defensive patterns
Strategy: try-catch
Try / catch
match manager.commit_persistent_services() {
Ok(receipts) => receipts,
Err(err) if err.to_string().contains("disappeared before commit") => {
// registry mutated mid-commit: log the invariant breach, re-derive pending set
}
Err(err) => return Err(err),
} Prevention
- Never call kill/cleanup/spawn on the same ShellManager while commit_persistent_services runs
- Serialize all manager access at the turn boundary
- File invariant hits with the task_id and stack trace
- Re-derive the PersistPending set after the failure instead of assuming state
When it happens
Trigger: Any mutation of the `processes` map (kill, kill_all, cleanup, spawn eviction, another commit) interleaving between the id-collection pass and the validation pass inside `commit_persistent_services`.
Common situations: Essentially unreachable through the public API in normal use; appears when new code paths drive the same ShellManager during commit, or tests that mutate the manager from multiple threads.
Related errors
- Persistent service {id} exited before ownership transfer (st
- Persistent service {id} has no releasable process id
- Persistent service {id} disappeared during commit
- Persistent service {id} lost its process id
- snapshot dir has no parent
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/9f0c0415f67f2ffc.
Report an issue: GitHub.