{"record":{"id":"92e7fe8c8105069a","repo":"Hmbown/CodeWhale","slug":"persistent-service-id-exited-before-ownership-tr","errorCode":null,"errorMessage":"Persistent service {id} exited before ownership transfer (status {:?}, exit code {:?})","messagePattern":"Persistent service (.+?) exited before ownership transfer \\(status (.+?), exit code (.+?)\\)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/shell.rs","lineNumber":2884,"sourceCode":"    /// 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            }\n        }\n\n        let mut receipts = Vec::with_capacity(ids.len());","sourceCodeStart":2866,"sourceCodeEnd":2902,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/tools/shell.rs#L2866-L2902","documentation":"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.","triggerScenarios":"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`.","commonSituations":"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).","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"],"exampleFix":"// before: blind commit after the turn\nmanager.spawn_background(/* persist: true */ /* ... */)?;\nlet receipts = manager.commit_persistent_services()?;\n\n// after: prove the service is still running before handoff\nfor job in manager.list_jobs() {\n    if job.status != ShellStatus::Running {\n        anyhow::bail!(\"service {} exited early (status {:?}); check its output\", job.id, job.status);\n    }\n}\nlet receipts = manager.commit_persistent_services()?;","handlingStrategy":"validation","validationCode":"for job in manager.list_jobs() {\n    if job.status != ShellStatus::Running {\n        anyhow::bail!(\"service {} exited early (status {:?}); capture its output before commit\", job.id, job.status);\n    }\n}\nlet receipts = manager.commit_persistent_services()?;","typeGuard":"fn all_services_running(manager: &mut ShellManager) -> bool {\n    manager.list_jobs().iter().all(|job| job.status == ShellStatus::Running)\n}","tryCatchPattern":"match manager.commit_persistent_services() {\n    Ok(receipts) => Ok(receipts),\n    Err(err) if err.to_string().contains(\"exited before ownership transfer\") => {\n        // read the embedded status/exit code, surface the service's own failure,\n        // fix the service (port/args/env) and re-spawn — retrying commit alone cannot help\n        Err(anyhow!(\"persistent service failed at runtime: {err}\"))\n    }\n    Err(err) => Err(err),\n}","preventionTips":["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"],"tags":["persistent-services","process-lifecycle","readiness","unix","rust"],"backgroundTag":"service-exited-before-handoff","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}