Repository navigation
[fsconnect] - Bound the indexer walk depth and clean up the skip-cache temp file on failure - #1648
Conversation
…e temp file on failure FsIndexer._walk recursed once per directory level with no ceiling (fs_glob got _MAX_GLOB_DEPTH, the indexer never did). A tree deeper than the frame limit raised RecursionError -- outside the CLI's 0/2/3/4 contract -- and in apply() it fires after files are staged but before the ownership cache is saved, so those staged copies could never be pruned. The walk now stops at 64 levels, records the un-walked subtree as a `skipped: too_deep` manifest row, and keeps enumerating siblings. _save_cache now writes a per-PID temp file and unlinks it on OSError, matching harness_optimizer's _atomic_json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJXTdWRbDpkynPYbpL2pxd
|
PR is a complete fill of |
|
@codex review (for bugs to fix and performance/security/other regressions potentially introduced in this pr) |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Proposed changes
FsIndexer._walk(agentic/fsconnect/indexer.py) recursed once per directory level with no ceiling.fs_globgot_MAX_GLOB_DEPTH = 64for exactly this bug (tests/test_fsconnect_glob_depth.py), but the indexer's walk never did.Reproduced on
main: a 1100-level tree makesFsIndexer.scan()raiseRecursionError. I built it with relativemkdir/chdir; its paths are about 2.2k characters, under LinuxPATH_MAX.RecursionErroris not anFsConnectError, so the fsconnect CLI exits outside its 0/2/3/4 contract. Inapply()the same crash fires mid-walk: some files are already staged, but_save_cachehas not run yet. Those staged copies then have no ownership record, and ownership-bounded pruning can never remove them.Changes:
_walktakes adepthargument and stops at_MAX_INDEX_DEPTH = 64, the same value as fs_glob's cap. Each un-walked subtree becomes a{"path": …, "size": 0, "skipped": "too_deep"}manifest row, so it is reported rather than hidden. Siblings at that level are still enumerated._save_cachenow writes to a per-PID temp name (.fsindex_cache.json.<pid>.tmp) and unlinks it onOSError. Before, a disk-full or permission error left.fsindex_cache.json.tmpin the corpus staging dir, and two concurrent applies shared one temp name. The new code mirrorsharness_optimizer.patching._atomic_json.sync/filters.pycomment now names the new temp form. The.fsindex_cache.json*rule already covers it, so the filter itself does not change.Invariant / Governance Impact: none of the six invariants is touched.
indexer.pystill imports nothing from retrieval, gate, graph or mcp.check_invariants.py: 46 passed, 0 failed.split_componentsand the pathsafe guard._CACHE_NAMEandos.getpid(), never from share input.Types of changes
Scope: Out-of-band agentic/fsconnect layer.
Benefits / why
index-scanandindex-applyon a pathologically deep share now finish with a typed, reported result instead of a traceback. Such a share can be hostile or accidental, and building one needs no symlink.apply()always reaches_save_cache, so pruning ownership stays consistent..fsindex_cache.json*rule at all.Risks to monitor
applyprunes it. This matches howtoo_largeandread_errorrows already drop out ("staging mirrors the ELIGIBLE share"). Shares that deep are not realistic for a corpus.skipped: too_deeprows inindex-scanoutput, plus three new tests intests/test_fsconnect_indexer.py:Checklist
docs/THREAT_MODEL.md,INVARIANTS.mdandSECURITY.md(and any relevant Phase docs)GROK_API_KEY=dummy pytest tests/ -q --tb=short, andbash .claude/skills/CyClaw-Sandbox/verify.shfor core RAG/agentic paths) and passes with no regressions. Not run locally, per the CLAUDE.md §5 verification policy; CI runs the suites.split_components.Further comments
Verification I ran directly, following CLAUDE.md §5:
Code probe (a scratch script driving the real
scan()andapply()):too_deeprow, siblings found, cache saved, no.tmpleftmainit raisesRecursionErrorOne targeted test file:
pytest tests/test_fsconnect_indexer.pygives 31 passed. Withindexer.pyreverted tomain, the 3 new tests fail.Lint:
ruff check --select E,F,I,B,C4,UP,S --ignore E501is clean.ruff formatdrift in these files predates this PR; this diff adds none (the drift count matchesmain).git diff --numstatshows the same counts with and without--ignore-cr-at-eol, so there is no line-ending churn.Invariants:
check_invariants.pygives 46/0.Not run locally: the full suite and the coverage gate. GitHub Actions runs those.
Not fixed here:
write_filter_fileinsync/filters.pyuses the same fixed-name temp pattern. It runs under the sync lock and is regenerated on every run, so I judged it not worth the extra diff.Suggested merge order of open PRs
Trial merges verified (
git merge --no-ff, all three branches in sequence onto a throwaway checkout) against main @ 6396152 on 2026-10-13. No file is touched by more than one of the three PRs.#1647 · Blocked ⛔ (draft, CI pending)
Impact:
agentic/harness_optimizer/only; independent.Action: Merge first (lowest number). Trial merge clean vs main.
#1648 (this PR) · Blocked ⛔ (draft, CI pending)
Impact:
agentic/fsconnect/indexer.py+ async/filters.pycomment; independent.Action: Merge second; no shared files.
#1649 · Blocked ⛔ (draft, CI pending)
Impact:
memory/store.py,memory/policy.py+ tests; independent.Action: Merge third; no shared files.
ELI5
The file-share indexer walks folders like nested boxes: open a box, and if there's a box inside, open that one too. Python can keep only about a thousand boxes open at once, so a share nested deeper than that crashed the whole walk with
RecursionError. Worse, inapply()the crash came after some files were already copied into the corpus but before the indexer wrote down which ones it had copied. It could never clean those copies up later.The walk now stops at 64 levels, the same limit the
fs_globsearch tool already uses. A subtree past the limit shows up as askipped: too_deeprow instead of being silently ignored, and the other folders at that level are still read. Separately, the small cache file is now written through a temp file with a unique name, and that temp file is deleted if the write fails. No leftovers end up in the synced folder.What could still go wrong: the next apply will prune any file deeper than 64 levels that an earlier run copied. That is intended, and the
too_deeprows show it. I did not run the full suite locally; CI will run it.🤖 Generated with Claude Code
https://claude.ai/code/session_01BJXTdWRbDpkynPYbpL2pxd
Generated by Claude Code
Last updated: 2026-10-13 09:43 ET