fix(cache): recover semantic cache groups with malformed source_file by basename - #3553
ayushcodes10 wants to merge 7 commits into
Conversation
save_semantic_cache groups nodes and edges by source_file, then skips any group whose path does not resolve to a real file. A weak or local backend's adaptive retry split path sometimes reports a source_file for a group that never resolves at all, silently dropping that group's nodes and edges from the cache on every incremental run. This adds a helper that recovers such a group by an unambiguous basename match against the known good, already dispatched allowlist, with no behavior change yet since nothing calls it. The write loop is switched over in the next commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Fixes issue Graphify-Labs#2973. group_skipped and the save_semantic_cache write loop now call _recover_group_path instead of resolving fpath directly, so a group whose reported source_file never resolves to a real file gets one more chance: an unambiguous basename match against the already dispatched allowlist. An ambiguous or genuinely bogus basename still falls through to the previous skip behavior unchanged, and unscoped callers (no allowed_source_files) are byte identical to before since recovery only runs when an allowlist is available. This stops an incremental run from silently redoing already processed chunks every time the adaptive retry split path reports a source_file that a weak or local backend lost track of. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
A recovered group's write KEY pointed at the right file, but every node, edge, and hyperedge inside it still carried the original unresolvable source_file string. load_cached rejects an entry on read if any item's source_file does not match the path it was loaded under, so the previous commit alone wrote a group that reported as saved but could never actually be read back, silently reproducing the exact loss this fix exists for. This corrects every item's source_file to the recovered path's portable form before the group is written, only when recovery actually redirected the key, so an unrecovered group stays byte identical to before. Includes the regression test proving the full round trip: a group recovered by basename now survives a subsequent load_cached call. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Two allowed files sharing a basename must not let recovery guess between them: an unresolvable source_file with an ambiguous basename still falls through to the previous skip behavior unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Confirms recovery never runs when allowed_source_files is omitted: a genuinely bogus path is skipped exactly as before this fix, and a normal well formed path in the same batch still saves and loads back. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
group_skipped feeds the dangling reference pruning pass and the write loop must agree with it on whether a recovered group is written or skipped. This proves an edge between two nodes inside the same recovered group survives instead of being pruned as referencing a supposedly skipped id. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Recovers a semantic-cache group whose reported source_file never resolves to a real file by matching its basename against the already-dispatched allowlist, so nodes/edges from the adaptive-retry split path reach the cache instead of being silently dropped and re-processed on every incremental run. _recover_group_path performs the match only when the basename is unambiguous in allowed_paths, and group_skipped uses it too so a recovered group's intra-group edges aren't pruned as dangling; when recovery redirects the write key, every item's source_file is rewritten to the corrected path so the entry can actually be read back. An ambiguous or genuinely bogus basename, or any call without an allowlist, still falls through to the existing skip-and-warn behaviour.
Worth a look
- Recovered write key can collide when two unresolvable groups share a basename —
graphify/cache.py:1593· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1773 functions depend on the 296 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 589 callers, 44 callees - new:
_rebuild_code()— 116 callers, 51 callees - new:
detect()— 112 callers, 15 callees - new:
save_semantic_cache()— 63 callers, 9 callees - new:
load_cached()— 53 callers, 7 callees - new:
file_hash()— 51 callers, 6 callees - new:
extract_corpus_parallel()— 26 callers, 11 callees - new:
dispatch_command()— 2 callers, 124 callees - …and 19 more — each is listed as a finding
Verification — 1773 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 1061 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
276 of 276 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/test_affected_cli.py— full-run-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— full-run-safetytests/test_astro_import_ids.py— full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_env_isolation.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_build.py— full-run-safetytests/test_build_merge_dedup_scope.py— full-run-safetytests/test_build_merge_hyperedges_and_prune.py— full-run-safetytests/test_build_merge_shrink_guard.py— full-run-safetytests/test_builtin_global_type_refs.py— full-run-safetytests/test_cache.py— impact, full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— full-run-safetytests/test_case_sensitive_resolution.py— full-run-safetytests/test_charmap_encoding.py— impact, full-run-safetytests/test_chunking.py— impact, full-run-safetytests/test_cjs_module_extension.py— full-run-safetytests/test_claude_cli_backend.py— full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— full-run-safetytests/test_codebuddy.py— full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— full-run-safetytests/test_corrupt_graph_json.py— full-run-safetytests/test_cpp_nested_and_cli.py— full-run-safetytests/test_cpp_objc_cross_file_calls.py— full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— full-run-safetytests/test_cross_language_call_resolution.py— full-run-safetytests/test_cross_repo_member_calls.py— full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— full-run-safetytests/test_csharp_enum_members.py— full-run-safety- … and 226 more
non-code file(s) changed (
CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)
changed code file(s) with no mapped test (
CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
Could not verify: Could not verify save\_semantic\_cache.
The verifier did not have enough to check save\_semantic\_cache, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `root` is annotated `Path` — outside the synthesizable primitive/collection set
· 27 more finding(s) on lines outside this diff (see the check run).
Fixes #2973.
Summary
save_semantic_cachegroups nodes/edges/hyperedges bysource_file, then silently skips any group whose path does not resolve to a real file. The reporter found that during a long incremental run against a weak or local backend, the adaptive-retry split path (llm.pybisecting a chunk that overflowed the model's context) sometimes reports asource_filefor a sub-chunk that never resolves at all, so that group's nodes and edges vanish from the cache on every run — an "incremental" extraction kept re-processing the same already-extracted chunks.This recovers such a group by an unambiguous basename match against
allowed_source_files(the known-good, already-dispatched file list already threaded through from_checkpoint_chunk), matching the fix the reporter tested and verified locally. Two things had to change for it to actually work end to end:_recover_group_path, used consistently by both the write loop andgroup_skipped's dangling-reference pruning, so the two never disagree on whether a group was written).source_filefield corrected to match. Without this,load_cached's_semantic_entry_matches_pathintegrity check rejects the entry on the very next read (it verifies every item'ssource_filematches the path being loaded), so a naive fix would report the group as "saved" while producing an entry that can never actually be read back — silently reproducing the same loss it's meant to fix. This was caught by writing a direct save-then-load round-trip test rather than only asserting thesavedcount.An ambiguous basename (two allowed files sharing a name) or a call with no
allowed_source_filesat all still falls through to the exact previous skip behavior, so unscoped callers are byte-identical.Changes
graphify/cache.py: add_recover_group_path, wire it intogroup_skippedand the write loop, and correct a recovered group's ownsource_filefields before writing.tests/test_semantic_cache_basename_recovery.py(new): basename recovery + save/load round trip, ambiguous basename stays skipped, unscoped calls unaffected, dangling-edge pruning stays consistent for a recovered group.CHANGELOG.md: entry under 0.9.61 (unreleased).Testing
python -m pytest tests/test_semantic_cache_basename_recovery.py -q— 5 passedpython -m pytest -q— 5489 passed, 68 skipped (pre-existing unrelated failures excluded:tests/test_ollama_retry_cap.pymissing the optionalopenaimodule, andtest_ts_normalizer_scales_linearly_on_large_files/test_hyperedge_convex_hull_js_is_geometrically_sound, both known environment-specific flakes unrelated to this change)python -m tools.skillgen --check— OK🤖 Generated with Claude Code
https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh