Hmbown/CodeWhale · error

sub-agent state path must include a parent directory

Error message

sub-agent state path must include a parent directory

What it means

`checked_subagent_state_path` requires a parent directory component after extracting the file name; parent() returning None means the path is degenerate (no directory part above the file). In practice this is nearly unreachable when paths are joined under an absolute state root, and it exists as a completeness guard.

Source

Thrown at crates/tui/src/tools/subagent/mod.rs:7683

        &Path::new(".codewhale")
            .join("state")
            .join(SUBAGENT_STATE_FILE),
    )
}

fn checked_subagent_state_path(state_root: &Path, path: &Path) -> Result<PathBuf> {
    let state_root = normalize_subagent_workspace(state_root);
    let absolute = if path.is_absolute() {
        path.to_path_buf()
    } else {
        state_root.join(path)
    };
    let file_name = absolute
        .file_name()
        .ok_or_else(|| anyhow!("sub-agent state path must include a file name"))?;
    let parent = absolute
        .parent()
        .ok_or_else(|| anyhow!("sub-agent state path must include a parent directory"))?;
    let parent = match parent.canonicalize() {
        Ok(parent) => parent,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => normalize_path_components(parent),
        Err(err) => return Err(err.into()),
    };
    let state_path = parent.join(file_name);
    if !state_path.starts_with(&state_root) {
        return Err(anyhow!(
            "sub-agent state path must stay within state root: {}",
            state_path.display()
        ));
    }
    reject_root_relative_symlinks(&state_root, &state_path)?;
    Ok(state_path)
}

fn normalize_subagent_workspace(workspace: &Path) -> PathBuf {
    if let Ok(canonical) = workspace.canonicalize() {

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Always derive sub-agent state paths via state_root.join(relative) rather than constructing bare Path values.
  2. Reject empty or component-less path strings at the input boundary.
Defensive patterns

Strategy: validation

Validate before calling

fn has_parent(rel: &str) -> bool {
    std::path::Path::new(rel).parent().map_or(false, |p| !p.as_os_str().is_empty())
}

Type guard

fn is_joinable_under_root(rel: &str) -> bool {
    has_parent(rel) && has_file_name(rel)
}

Try / catch

match checked_subagent_state_path(&state_root, Path::new(rel)) {
    Err(e) if e.to_string().contains("parent directory") => { /* rebuild path under an explicit directory */ }
    r => r?,
}

Prevention

When it happens

Trigger: A path object with a file name but no parent component — essentially only constructible from degenerate inputs (empty/odd PathBuf built outside the manager's join logic).

Common situations: Direct API use with hand-constructed Path values bypassing state_root.join; test code assembling unusual paths.

Related errors


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