Hmbown/CodeWhale · critical

restoring retired xAI OAuth file

Error message

restoring retired xAI OAuth file {original}

What it means

anyhow error raised by `XaiOAuthRevocation::rollback` (crates/config/src/xai_credentials.rs:522) when restoring a retired xAI OAuth file from its tombstone name back to the original name fails during `store.rename_raw(&tombstone, &original)`. Rollback is fail-closed: it never overwrites a file that unexpectedly appeared at the original name, and it keeps the first error while attempting every restoration. Called by `stage_revocation` and `mark_running_if_pending_with` when a config mutation fails and the old epoch must come back.

Solutions

  1. Inspect the credentials directory (`store.directory`) for stray files at both the original and tombstone names; resolve the collision manually before retrying (move the unexpected file aside).
  2. Close other Codewhale/xAI sessions and file-locking sync clients, then retry the operation.
  3. Fix directory permissions (ensure the store directory is writable by the current user) and free disk space if exhausted.
  4. After manual cleanup, re-run the credential revocation flow; rollback is idempotent per remaining tombstone and reports only the first failure.

Example fix

// before (failed)
rollback(store)?; // error: restoring retired xAI OAuth file /home/me/.codewhale/xai.oauth
// after: clear the collision first
// mv /home/me/.codewhale/xai.oauth /home/me/.codewhale/xai.oauth.unexpected
rollback(store)?;
Defensive patterns

Strategy: try-catch

Validate before calling

// before staging revocation
let dir = &store.directory;
for original in originals {
    ensure!(!dir.join(original).exists(), "unexpected file at {} blocks rollback", dir.join(original).display());
}

Try / catch

if let Err(e) = revocation.rollback(&store) {
    log::error!("xAI OAuth rollback failed: {e:#}; manual cleanup of {} required", store.directory.display());
    // surface to user; do not silently proceed with half-restored credentials
}

Prevention

When it happens

Trigger: The tombstone file was deleted or locked between retirement and rollback; another process created a file at the original path so the rename is refused (fail-closed); the credentials directory lost write permission mid-operation; on Windows the rollback path is a no-op, so this only fires on non-Windows stores.

Common situations: Two Codewhale/xAI CLI sessions racing on the same credentials directory; AV or sync tools (Dropbox, OneDrive) holding locks on the OAuth files; disk-full or permission changes between revocation staging and failure-driven rollback; stale tombstones left by a crashed prior run.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/4abaf5c13d4fe98f. Report an issue: GitHub.

Appendix: source

Thrown at crates/config/src/xai_credentials.rs:522

}

impl XaiOAuthRevocation {
    /// Restore the old epoch after a config mutation fails. Restoration is
    /// fail-closed: an unexpected replacement at an original name is never
    /// overwritten.
    pub fn rollback(self, store: &XaiOAuthCredentialStore) -> Result<()> {
        #[cfg(windows)]
        {
            let _ = store;
            Ok(())
        }
        #[cfg(not(windows))]
        {
            let mut first_error = None;
            for (original, tombstone) in self.retired.into_iter().rev() {
                let result = store.rename_raw(&tombstone, &original).with_context(|| {
                    format!(
                        "restoring retired xAI OAuth file {}",
                        crate::quote_os_path(&store.directory.join(original))
                    )
                });
                if result.is_err() && first_error.is_none() {
                    first_error = result.err();
                }
            }
            if let Some(error) = first_error {
                return Err(error);
            }
            Ok(())
        }
    }

    /// Permanently remove retired bytes after the replacement config commits.
    pub fn commit(self, store: &XaiOAuthCredentialStore) -> Result<usize> {
        let mut removed = 0;
        for (_original, _tombstone) in self.retired {

View on GitHub (pinned to 73e0f67d83)