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

  1. Treat as a bug in calling code: guarantee nothing calls kill/cleanup/spawn on the same ShellManager while `commit_persistent_services` runs
  2. Keep commit as the sole owner of the PersistPending set at the turn boundary (serialize manager access)
  3. If hit, capture the task_id and call stack and file it against the shell tool
  4. 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

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


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