Hmbown/CodeWhale · error

Session goal must be a regular file

Error message

Session goal {path} must be a regular file

What it means

The goal-file reader validates each session goal file with symlink_metadata and requires a regular, non-symlink file; anything else is InvalidData 'Session goal ... must be a regular file'. NotFound maps to Ok(false) (no goal), but symlinks and directories at the goal path are refused. Like the directory check, this blocks reading goal contents through a linked path.

Solutions

  1. Replace the symlink with a real file (copy the target's contents to the goal path)
  2. Remove the directory or special file at that path and let the manager recreate it
  3. Avoid syncing goal files via tools that create symlinks; copy instead

Example fix

// before
ln -s ~/shared/goal.json <data>/goals/<session>.json
// after
cp ~/shared/goal.json <data>/goals/<session>.json
Defensive patterns

Strategy: validation

Validate before calling

let md = std::fs::symlink_metadata(&goal_path)?;
if md.file_type().is_symlink() || !md.is_file() { return Err(anyhow!("goal must be a regular file")); }

Type guard

fn is_regular_file(p: &std::path::Path) -> bool {
    std::fs::symlink_metadata(p).map(|m| !m.file_type().is_symlink() && m.is_file()).unwrap_or(false)
}

Try / catch

match manager.read_session_goal(id) {
    Ok(Some(goal)) => use(goal),
    Ok(None) => {}, // no goal set
    Err(e) if e.to_string().contains("must be a regular file") => eprintln!("Replace the symlink/dir at the goal path with a real file"),
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: Calling the goal existence/read check when the path holds a symlink (even to a valid file), a directory, or another non-regular file type (fifo, device).

Common situations: A user symlinks a goal file into a shared location; the goal path was replaced by a directory during a failed cleanup; sync tools substitute links for real files; tests creating directories at the goal path.

Related errors


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

Appendix: source

Thrown at crates/tui/src/session_manager.rs:1353

                io::ErrorKind::NotFound,
                format!("Session goal store {} was not created", dir.display()),
            )
        })
    }

    fn validated_session_goal_path(&self, session_id: &str) -> std::io::Result<PathBuf> {
        let id = self.validated_session_id(session_id)?;
        Ok(self.session_goals_dir().join(format!("{id}.json")))
    }

    fn checked_existing_session_goal_file(path: &Path) -> std::io::Result<bool> {
        let metadata = match fs::symlink_metadata(path) {
            Ok(metadata) => metadata,
            Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
            Err(error) => return Err(error),
        };
        if metadata.file_type().is_symlink() || !metadata.is_file() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!("Session goal {} must be a regular file", path.display()),
            ));
        }
        Ok(true)
    }

    fn validated_checkpoint_path(&self, session_id: &str) -> std::io::Result<PathBuf> {
        let trimmed = self.validated_session_id(session_id)?;
        // Reserved file names inside `checkpoints/` must never collide with a
        // per-session checkpoint file.
        if format!("{trimmed}.json") == LEGACY_CHECKPOINT_FILE
            || format!("{trimmed}.json") == OFFLINE_QUEUE_FILE
        {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                format!("Session id '{trimmed}' collides with a reserved checkpoint file"),
            ));

View on GitHub (pinned to 73e0f67d83)