xai-org/grok-build · error · io::Error

resources state has no parent directory

Error message

resources state has no parent directory

What it means

publish_durable refuses to write the resources state when the target path has no parent directory (a bare filename or empty parent), returning InvalidInput with "resources state has no parent directory". Without an explicit parent, the temporary file would land in the server's current working directory, shared by every session — a correctness hazard, so bare filenames are rejected outright.

Source

Thrown at crates/codegen/xai-grok-tools/src/persistence.rs:341

    async fn replace_state_path(path: &Path, tmp_path: &Path) -> io::Result<()> {
        if path.is_dir() {
            tracing::warn!(
                "Resources state path {:?} is a directory — removing before write",
                path
            );
            tokio::fs::remove_dir_all(path).await?;
        }
        tokio::fs::rename(tmp_path, path).await
    }

    #[cfg(not(windows))]
    async fn publish_durable(path: &Path, tmp_path: &Path) -> io::Result<()> {
        // A bare filename has an empty parent, so the write would land in the server's own directory, shared by every session.
        let parent = path
            .parent()
            .filter(|p| !p.as_os_str().is_empty())
            .ok_or_else(|| {
                io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "resources state has no parent directory",
                )
            })?;

        Self::replace_state_path(path, tmp_path).await?;
        tokio::fs::File::open(parent).await?.sync_all().await
    }

    #[cfg(windows)]
    async fn publish_durable(path: &Path, tmp_path: &Path) -> io::Result<()> {
        use windows::Win32::Storage::FileSystem::MoveFileExW;
        use windows::core::PCWSTR;
        if path.is_dir() {
            tokio::fs::remove_dir_all(path).await?;
        }
        let from = Self::windows_extended_path(tmp_path)?;
        let to = Self::windows_extended_path(path)?;

View on GitHub (pinned to bc7f02eddd)

Solutions

  1. Always build the state path as an absolute, per-session path (e.g. /sessions/<id>/resources.json).
  2. Join the filename with the session's directory before handing it to the persistence layer.
  3. Validate configured paths at startup (reject bare filenames early with a clear config error).
  4. Use std::fs::canonicalize or path.parent() checks in setup code to assert a parent exists.

Example fix

// before
let path = PathBuf::from("resources.json");
// after
let path = session_dir.join("resources.json");
Defensive patterns

Strategy: validation

Validate before calling

fn state_path_has_parent(p: &Path) -> bool {
    p.parent().map(|p| !p.as_os_str().is_empty()).unwrap_or(false)
}

Try / catch

match persistence.save_and_flush(snapshot).await {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidInput => {
        bail!("misconfiguration: resources state path must include a directory, got {path:?}")
    }
    other => other.map_err(Into::into),
}

Prevention

When it happens

Trigger: Constructing the persistence path from a bare filename like Path::new("state.json") instead of a full per-session path, then triggering a durable publish (save_and_flush) — the tmp file path.with_extension("json.tmp") would resolve against the process cwd.

Common situations: Configuring a session/state path without a directory component; path built by joining an empty string; tests passing a relative bare name; a refactor that dropped the session-directory prefix.

Related errors


AI-assisted analysis of xai-org/grok-build@bc7f02eddd (2026-08-31). Data as JSON: /api/errors/d29f81bfdf5b836e. Report an issue: GitHub.