zeroclaw-labs/zeroclaw · error

Search timed out after {TIMEOUT_SECS} seconds.

Error message

Search timed out after {TIMEOUT_SECS} seconds.

What it means

The content_search tool fell back to its internal pure-Rust walker (used when ripgrep `rg` is not available) and the recursive directory walk exceeded the fixed 30-second budget (TIMEOUT_SECS at crates/zeroclaw-tools/src/content_search.rs:14). check_internal_deadline is a cooperative cancellation check invoked before the search starts, at every directory visit, and per file; once Instant::now() passes the deadline computed at content_search.rs:292, the walk aborts with this error and partial results are discarded.

Source

Thrown at crates/zeroclaw-tools/src/content_search.rs:486

        context_before,
        context_after,
        max_results,
        deadline,
        &mut raw_lines,
        &mut results_seen,
    )?;

    Ok(format_line_output(
        &raw_lines.join("\n"),
        workspace_canon,
        output_mode,
        max_results,
    ))
}

fn check_internal_deadline(deadline: Instant) -> anyhow::Result<()> {
    if Instant::now() >= deadline {
        anyhow::bail!("Search timed out after {TIMEOUT_SECS} seconds.");
    }
    Ok(())
}

#[allow(clippy::too_many_arguments)]
fn visit_internal_search_path(
    path: &Path,
    workspace_canon: &Path,
    include: Option<&glob::Pattern>,
    security: &SecurityPolicy,
    regex: &regex::Regex,
    output_mode: &str,
    context_before: usize,
    context_after: usize,
    max_results: usize,
    deadline: Instant,
    raw_lines: &mut Vec<String>,
    results_seen: &mut usize,

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Install ripgrep (`rg`) on the host/container so the fast external backend is used instead of the internal walker — the same 30s timeout applies but rg finishes orders of magnitude faster.
  2. Narrow the search path: search a subdirectory of the workspace instead of the workspace root.
  3. Pass an `include` glob (e.g. "*.rs") so internal_include_matches skips non-matching files before search_internal_file reads them.
  4. Split the search into multiple invocations, one per top-level directory, so each walk stays under 30 seconds.
  5. Reduce per-match work: lower max_results and avoid context lines on huge trees.

Example fix

// before
search(path: "/workspace", pattern: "TODO", include: null)
// -> internal walker walks every file, hits the 30s deadline, bails

// after
search(path: "/workspace/crates", pattern: "TODO", include: "*.rs")
// plus install ripgrep so the rg backend handles the request
Defensive patterns

Strategy: retry

Validate before calling

// Estimate workload before searching: if ripgrep is absent and the tree
// is large, expect the 30s internal-walker deadline.
fn rg_available() -> bool {
    std::process::Command::new("rg")
        .arg("--version")
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}
fn count_files(dir: &std::path::Path, cap: usize) -> usize {
    let mut n = 0;
    if let Ok(rd) = std::fs::read_dir(dir) {
        for e in rd.flatten() {
            n += 1;
            if n > cap { break; }
            let p = e.path();
            if p.is_dir() { n += count_files(&p, cap.saturating_sub(n)); }
        }
    }
    n
}
// before invoking: require rg, or a small tree, or an include glob
assert!(rg_available() || count_files(root, 20_000) < 20_000 || include_glob.is_some());

Try / catch

match tool.execute(params).await {
    Err(e) if e.to_string().contains("Search timed out after") => {
        // deadline fired: retry once per top-level subdir, or with a
        // tighter include glob; do not retry the same broad request
        for sub in top_level_dirs(root) {
            let _ = tool.execute(with_path(params, sub)).await;
        }
    }
    other => other,
}

Prevention

When it happens

Trigger: Invoking the content search tool with a broad search path (the whole workspace) and either no include glob or one that matches many files, on a machine where `rg` is not on PATH so the internal backend runs (content_search.rs:233). Large trees, network/slow filesystems, or expensive regexes push the visit_internal_search_path/search_internal_file steps past the 30s deadline and the next check_internal_deadline call bails.

Common situations: Agent sandboxes and minimal Docker images that omit ripgrep; searching a monorepo with node_modules/target/vendor directories included; case-insensitive or complex regexes over thousands of files; a workspace on NFS or a slow bind mount where canonicalize plus reads dominate the budget.

Understand the failure class

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/3fb48dd2504df282. Report an issue: GitHub.