{"record":{"id":"3fb48dd2504df282","repo":"zeroclaw-labs/zeroclaw","slug":"search-timed-out-after-timeout-secs-seconds","errorCode":null,"errorMessage":"Search timed out after {TIMEOUT_SECS} seconds.","messagePattern":"Search timed out after (.+?) seconds\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/content_search.rs","lineNumber":486,"sourceCode":"        context_before,\n        context_after,\n        max_results,\n        deadline,\n        &mut raw_lines,\n        &mut results_seen,\n    )?;\n\n    Ok(format_line_output(\n        &raw_lines.join(\"\\n\"),\n        workspace_canon,\n        output_mode,\n        max_results,\n    ))\n}\n\nfn check_internal_deadline(deadline: Instant) -> anyhow::Result<()> {\n    if Instant::now() >= deadline {\n        anyhow::bail!(\"Search timed out after {TIMEOUT_SECS} seconds.\");\n    }\n    Ok(())\n}\n\n#[allow(clippy::too_many_arguments)]\nfn visit_internal_search_path(\n    path: &Path,\n    workspace_canon: &Path,\n    include: Option<&glob::Pattern>,\n    security: &SecurityPolicy,\n    regex: &regex::Regex,\n    output_mode: &str,\n    context_before: usize,\n    context_after: usize,\n    max_results: usize,\n    deadline: Instant,\n    raw_lines: &mut Vec<String>,\n    results_seen: &mut usize,","sourceCodeStart":468,"sourceCodeEnd":504,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/content_search.rs#L468-L504","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Narrow the search path: search a subdirectory of the workspace instead of the workspace root.","Pass an `include` glob (e.g. \"*.rs\") so internal_include_matches skips non-matching files before search_internal_file reads them.","Split the search into multiple invocations, one per top-level directory, so each walk stays under 30 seconds.","Reduce per-match work: lower max_results and avoid context lines on huge trees."],"exampleFix":"// before\nsearch(path: \"/workspace\", pattern: \"TODO\", include: null)\n// -> internal walker walks every file, hits the 30s deadline, bails\n\n// after\nsearch(path: \"/workspace/crates\", pattern: \"TODO\", include: \"*.rs\")\n// plus install ripgrep so the rg backend handles the request","handlingStrategy":"retry","validationCode":"// Estimate workload before searching: if ripgrep is absent and the tree\n// is large, expect the 30s internal-walker deadline.\nfn rg_available() -> bool {\n    std::process::Command::new(\"rg\")\n        .arg(\"--version\")\n        .stdout(std::process::Stdio::null())\n        .stderr(std::process::Stdio::null())\n        .status()\n        .map(|s| s.success())\n        .unwrap_or(false)\n}\nfn count_files(dir: &std::path::Path, cap: usize) -> usize {\n    let mut n = 0;\n    if let Ok(rd) = std::fs::read_dir(dir) {\n        for e in rd.flatten() {\n            n += 1;\n            if n > cap { break; }\n            let p = e.path();\n            if p.is_dir() { n += count_files(&p, cap.saturating_sub(n)); }\n        }\n    }\n    n\n}\n// before invoking: require rg, or a small tree, or an include glob\nassert!(rg_available() || count_files(root, 20_000) < 20_000 || include_glob.is_some());","typeGuard":null,"tryCatchPattern":"match tool.execute(params).await {\n    Err(e) if e.to_string().contains(\"Search timed out after\") => {\n        // deadline fired: retry once per top-level subdir, or with a\n        // tighter include glob; do not retry the same broad request\n        for sub in top_level_dirs(root) {\n            let _ = tool.execute(with_path(params, sub)).await;\n        }\n    }\n    other => other,\n}","preventionTips":["Install ripgrep in every image/sandbox that runs the search tool","Always pass an include glob for large workspaces","Search subdirectories instead of the workspace root","Avoid pathological regexes (nested quantifiers, huge alternations)"],"tags":["search","timeout","ripgrep","fallback","deadline"],"backgroundTag":"search-operation-timeout","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}