{"record":{"id":"9f0c0415f67f2ffc","repo":"Hmbown/CodeWhale","slug":"persistent-service-id-disappeared-before-commit","errorCode":null,"errorMessage":"Persistent service {id} disappeared before commit","messagePattern":"Persistent service (.+?) disappeared before commit","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/shell.rs","lineNumber":2881,"sourceCode":"\n    /// Transfer every still-running `persist:true` process out of Codewhale's\n    /// ownership. This is called only by the real headless exec host after the\n    /// enclosing turn has completed successfully.\n    #[cfg(unix)]\n    pub fn commit_persistent_services(&mut self) -> Result<Vec<PersistentServiceReceipt>> {\n        let mut ids = self\n            .processes\n            .iter()\n            .filter(|(_, shell)| shell.ownership == ShellOwnership::PersistPending)\n            .map(|(id, _)| id.clone())\n            .collect::<Vec<_>>();\n        ids.sort();\n\n        for id in &ids {\n            let shell = self\n                .processes\n                .get_mut(id)\n                .ok_or_else(|| anyhow!(\"Persistent service {id} disappeared before commit\"))?;\n            shell.poll();\n            if shell.status != ShellStatus::Running {\n                return Err(anyhow!(\n                    \"Persistent service {id} exited before ownership transfer (status {:?}, exit code {:?})\",\n                    shell.status,\n                    shell.exit_code\n                ));\n            }\n            if shell\n                .child\n                .as_ref()\n                .and_then(ShellChild::process_id)\n                .is_none()\n            {\n                return Err(anyhow!(\n                    \"Persistent service {id} has no releasable process id\"\n                ));\n            }","sourceCodeStart":2863,"sourceCodeEnd":2899,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/tools/shell.rs#L2863-L2899","documentation":"`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.","triggerScenarios":"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`.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match manager.commit_persistent_services() {\n    Ok(receipts) => receipts,\n    Err(err) if err.to_string().contains(\"disappeared before commit\") => {\n        // registry mutated mid-commit: log the invariant breach, re-derive pending set\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["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"],"tags":["persistent-services","process-lifecycle","invariant","unix","rust"],"backgroundTag":"background-job-registry-race","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}