MusePi

Filesystem Scan Cache Architecture Contract

English 中文

This document defines the current contract for the shared filesystem scan cache implemented in Rust (crates/pi-natives/src/fs_cache.rs) and consumed by native discovery/search APIs exposed to packages/coding-agent.

What this cache is

The cache stores full directory-scan entry lists (GlobMatch[]) keyed by scan scope, traversal policy, and requested metadata detail. Higher-level operations (glob filtering, fuzzyFind scoring, and cached grep candidate selection) run against those cached entries.

Primary goals:

Ownership and public surface

Cache key partitioning (hard contract)

Each entry is keyed by:

Implications:

Consumers must pass stable semantics for hidden/gitignore/node_modules/detail behavior; changing any keyed flag creates a different cache partition.

Scan collection behavior

Cache population uses ignore::WalkBuilder configured by include_hidden, use_gitignore, skip_node_modules, and follow_links:

Search roots for cache scans are resolved by fs_cache::resolve_search_path:

Freshness and eviction policy

Global policy (environment-overridable):

Behavior:

Empty-result fast recheck (separate from normal hits)

Normal cache hit:

Empty-result fast recheck:

Current consumers:

Consumer defaults and cache usage

Cache is opt-in on glob/fuzzyFind/grep (cache?: boolean, default false). astGrep/astEdit file discovery always uses the cache (there is no opt-in flag).

Current defaults in native APIs:

Current callers:

Invalidation contract

Native invalidation entrypoint:

Path handling details:

Coding-agent mutation flow responsibilities

Coding-agent code must invalidate after successful filesystem mutations.

Central helpers:

Current mutation callsites include:

Rule: if a flow mutates filesystem content or location and bypasses these helpers, cache staleness bugs are expected.

Adding a new cache consumer safely

When introducing cache use in a new scanner/search path:

  1. Use stable scan policy inputs
    • decide hidden/gitignore/node_modules/detail semantics first
    • pass them consistently to get_or_scan/force_rescan so cache partitions are intentional
  2. Treat cache data as pre-filtered only by traversal policy
    • apply tool-specific filtering (glob patterns, type filters, scoring) after retrieval
    • never assume cached entries already reflect your higher-level filters
  3. Implement empty-result fast recheck only for stale-negative risk
    • use scan.cache_age_ms >= empty_recheck_ms()
    • retry once with force_rescan(..., store=true, ...)
    • keep this path separate from normal cache-hit logic
  4. Respect no-cache mode explicitly
    • when caller disables cache, call force_rescan(..., store=false, ...) or use an uncached streaming walker
    • do not populate shared cache in a no-cache request path
  5. Wire mutation invalidation for any new write path
    • after successful write/edit/delete/rename, call the coding-agent invalidation helper
    • for rename/move, invalidate both old and new paths
  6. Do not add per-call TTL knobs
    • current contract is global policy only (env-configured), no per-request TTL override

Known boundaries