Repository navigation
Description
1. Problem summary
unvibecode review fails for the entire repository when a single source file has a syntax tree deeper than about 490 levels (Python) or about 985 levels (the tree-sitter languages). Python and tree-sitter both parse these files without trouble. The crash comes from UnvibeCode's own recursive tree walkers, and no per-file handler catches the RecursionError.
The result for the whole repository:
- no Connected Code Map;
- no context ZIP;
- no business review;
- exit code 1.
Neither the terminal nor analysis-status.json names the file responsible.
The real-world trigger is generated code. On SymPy, one generated lookup table (sympy/polys/numberfields/resolvent_lookup.py, 41 KB) is enough to make all 1,612 source files unanalysable. Deleting only that file gives a complete run with 39,577 connections.
2. Test environment
- UnvibeCode version: 0.3.15
- OS / Python: Windows 11 (10.0.26200), Python 3.13.9, default recursion limit (1000)
- Public repository: https://github.com/sympy/sympy at
1cb89981676cc408760aa70fcc2362124f08e96e - Commands:
python -m unvibecode review --repository <path> --no-open-browser, for the default run.python -m unvibecode review --repository <path> --large-repo-token-limit 1 --no-open-browser, a local-only run that skips the hosted stage.- SymPy (9.1M estimated tokens, above the 2M default limit) fails with both commands.
- The minimal repro below was run with the local-only command.
3. Steps to reproduce
A. Minimal, network-free
-
Create the two repos below. The only difference is the size of
table.py:import pathlib def mk(root, n): r = pathlib.Path(root); r.mkdir(exist_ok=True) (r / "app.py").write_text("from table import TOTAL\n\ndef show():\n return TOTAL\n") (r / "table.py").write_text("TOTAL = " + " + ".join(["1"] * n) + "\n") mk("depth490", 490); mk("depth491", 491)
-
Run the local-only command on
depth490, then ondepth491.
B. Real repository (A/B by deleting one file)
git clone https://github.com/sympy/sympy && git -C sympy checkout 1cb89981676cc408760aa70fcc2362124f08e96e- Run
python -m unvibecode review --repository sympy --no-open-browser. - Copy the tree, delete only
sympy/polys/numberfields/resolvent_lookup.pyin the copy, and run again. - Smaller variant: run on the folder
sympy/polys/numberfieldsalone, then on the same folder withoutresolvent_lookup.py.
4. Expected behavior
A file the analyser cannot walk should degrade gracefully, and the rest of the repository should still be analysed. The file should either:
- produce no symbols, or
- appear in
skipped_fileswith a reason.
The engine already does this for other per-file problems:
SyntaxError:PythonAstAdapter.parsereturns an empty record (00a:278-286).- Read
OSError:_chunk_filerecords it inskipped_files(00a:4333-4337). RecursionErroraroundast.literal_eval: it is already caught at 00a:3164 and 01:3121. So the code anticipates deep input in one place, but not in the walkers that actually crash.
5. Actual behavior
| Input | Result |
|---|---|
depth490 |
Code understanding ready, 1 connection (app.py → table.py) |
depth491 |
UNVIBECODE REVIEW FAILED (exit 1); no context ZIP; analysis-status.json → "error_type": "RecursionError" |
| SymPy, full repository, default command | UNVIBECODE REVIEW FAILED (exit 1) after 58 s; no Connected Code Map, no ZIP |
SymPy, same tree without resolvent_lookup.py |
Completes: 20,717 code sections, 39,577 connections |
sympy/polys/numberfields alone |
exit 1, RecursionError |
Same folder without resolvent_lookup.py |
exit 0: 212 sections, 113 connections |
ast.parse()acceptsresolvent_lookup.py; its maximum AST depth is 569.- The 115 KB
analysis-status.jsonof the failing run never mentionsresolvent_lookup.py. The user has no way to find the cause.
Exact thresholds (bisected on Python 3.13.9, one generated file plus one importer, everything else identical). The numbers shift slightly with Python version, because they depend on the recursion limit and on the call depth at which the walk starts.
| Construct in one file | Last passing | First failing |
|---|---|---|
Python 1 + 1 + … |
490 terms | 491 |
Python if/elif chain |
488 branches | 489 |
JavaScript / TypeScript 1 + 1 + … |
984 | 985 |
| Go | 982 | 983 |
| Rust | 986 | 987 |
| PHP | 984 | 985 |
| Ruby | 985 | 986 |
The run is deterministic: identical inputs gave the same result on every rerun.
6. Evidence
Python path. Traceback from technical-logs/connected_code_map.log (SymPy and depth491):
customer_runner.py:1497 run_fast_lane_worker
00a:7174 run_pipeline -> linker.resolve_to(...)
00a:3374 resolve_to -> self._enrich_python_nodes()
00a:3577 _enrich_python_nodes -> PythonSemanticExtractor().extract(source_text)
00a:3062 extract -> self._scan_imports(tree)
00a:3300 _scan_imports -> Visitor().visit(tree) # ast.NodeVisitor: one Python frame pair per AST level
ast.py:422 visit / ast.py:432 generic_visit (repeated)
RecursionError: maximum recursion depth exceeded
- The
tryaround the call at 00a:3575-3578 catches only(OSError, SyntaxError).
Tree-sitter path. JS/TS/Go/Rust/PHP/Ruby, js_985 run:
00a:7164 run_pipeline
00a:2212 build -> self.ts_adapter.parse(language, ...)
00a:440 parse
00a:498 _extract_js_ts -> for node in walk_ts(root)
00a:251 walk_ts -> yield from walk_ts(child) (repeated)
RecursionError: maximum recursion depth exceeded
walk_ts(00a:248-251) is a recursive generator, so each tree level costs one frame.ShipReadyV2DeepGraphBuilder.build()(00a:2188 onward) has no per-filetryaround parsing.
Same code in the second engine. 01_shipreadyv2_merged_graph_pipeline_5chunks.py has the same weaknesses, so the business-analysis graph builder is exposed in the same way:
- a recursive
walk_ts(01:205-208); - an
ast.NodeVisitor(01:3145, visited at 01:3257); - the same
except (OSError, SyntaxError)(01:3535).
7. Impact and scope
- Severity: a single file causes total loss of output for the whole repository (exit 1), with no pointer to the file.
- Real-world case: SymPy, one of the most-used Python libraries, cannot be analysed at all with default settings.
- How often it happens (honest scope):
- I scanned all 47,645
.pyfiles of a full Anaconda distribution.resolvent_lookup.pyis the only one with AST depth ≥ 300. - I scanned 732 real JS/TS/HTML files ≥ 20 KB from the same machine. None reached tree depth 900.
- So the trigger is uncommon, and in practice it is generated code: lookup tables, long
elifdispatchers and generated parsers. - When it does trigger, the result is a total failure, with no workaround short of guessing which file to delete.
- I scanned all 47,645
- Languages: Python, plus the six tree-sitter languages (reproduced synthetically for each).
- Platform: tested on Windows. The cause is Python's recursion limit in pure-Python tree walks, not filesystem behaviour, so it is expected on macOS and Linux too.
8. Suspected cause / suggested fix
The cause is two things together:
- recursive tree walks bounded by
sys.getrecursionlimit(); - no per-file error isolation around parsing and enrichment.
Suggested fix, for both engine files:
- Isolate per file. Wrap the per-file parse in
build()and the per-file extract in_enrich_python_nodeswithexcept (RecursionError, MemoryError, ValueError, SyntaxError). Record{"file": ..., "reason": "parse_depth_exceeded"}inskipped_files, and continue. - Remove the limit. Make
walk_tsiterative with an explicit stack, and replace theast.NodeVisitorin_scan_importswith an iterative walk such asast.walkor an explicit stack. Deep files are then fully analysed rather than skipped. - Name the file. If a fatal error still escapes, include the file path in
analysis-status.jsonand in the terminal message.
To validate:
depth491, the six tree-sitter fixtures and the full SymPy checkout should all complete.resolvent_lookup.pyshould be either analysed or listed inskipped_files.- SymPy's connection count should be ≥ the 39,577 measured without the file.
9. Duplicate check
I searched all 357 open and closed issues through #453 (fetched 2026-11-04) for recursion, RecursionError, nested, depth and SymPy.
- [Bug] Guardrails runner — deeply nested config JSON crashes check with an uncaught RecursionError and exit code 1 instead of the documented exit 2 #400 (and its fix, PR guardrails runner: report deeply nested configs as exit 2 instead of crashing (fixes #400) #410): RecursionError in the guardrails runner's JSON config check. That is a different component and a different input.
- [Bug] Guardrails runner — init on an existing file and questions on an invalid config both print a generic 'Invalid configuration' error with exit 2, hiding the real cause #401, [Bug] Quality CI — the RAG review example's 21 unit tests, the guardrails runner and its shipped profiles are never run or linted in CI #402: other guardrails-runner reports (error messages and CI coverage). They are unrelated to Code Understanding.
- [[Challenge 2026][Cohort 3][Track 1] [Bug] Code Understanding — Windows review crashes on a checked-out symlinked source file #350: a symlinked source file on Windows aborts the stage with
OSErrorfromsafe_read_text. Its proposed fix catches filesystem errors around reads. ARecursionErrorcomes from parsing, is not anOSError, is triggered by file content rather than the filesystem, and also occurs in_enrich_python_nodes, which [[Challenge 2026][Cohort 3][Track 1] [Bug] Code Understanding — Windows review crashes on a checked-out symlinked source file #350 does not touch. A fix for [[Challenge 2026][Cohort 3][Track 1] [Bug] Code Understanding — Windows review crashes on a checked-out symlinked source file #350 does not fix this issue. - [Bug] Fast Lane Python parser — file silently dropped when ast.parse raises SyntaxError, still reported as high confidence #286: a
SyntaxErrorsilently drops a file. That path is handled; this one is not. - [Issue] Fast Lane Symbol Resolver drops module-level object instantiations (FastAPI router, Flask app), breaking Connected Code graph edges #208: quotes the
literal_evalhandler that already catchesRecursionError. It is about module-level instantiations, not this crash.
No issue mentions SymPy, deep expressions, depth thresholds or walk_ts.
Activity