Skip to content

fix(cache): surface corrupt semantic cache entries instead of swallowing them (#2405) - #2683

Closed
ousamabenyounes wants to merge 1 commit into
Graphify-Labs:v8from
ousamabenyounes:fix/issue-2405
Closed

fix(cache): surface corrupt semantic cache entries instead of swallowing them (#2405)#2683
ousamabenyounes wants to merge 1 commit into
Graphify-Labs:v8from
ousamabenyounes:fix/issue-2405

Conversation

@ousamabenyounes

Copy link
Copy Markdown
Contributor

Summary

Fix #2405

load_cached() caught json.JSONDecodeError in the same except as OSError and returned None, so a corrupt (invalid-JSON) cache entry was indistinguishable from a cache miss. A corrupt entry stays on disk and fails to parse on every future run, so the file is silently re-extracted each time — and for semantic kinds that means re-billing the LLM — with nothing in the output hinting why. The reporter found 138 of 249 entries dead in a real vault, paying that extraction cost on every incremental run.

Fix

  • load_cached() now counts JSONDecodeError failures in a process-global _corrupt_cache_entries, mirroring the existing _legacy_semantic_hits counter, and still returns None (miss → re-extract, self-healing).
  • check_semantic_cache() emits one aggregate RuntimeWarning per run naming how many entries could not be parsed and how to clear them (--force / clear cache), matching the existing legacy-vintage warning it already raises.
  • OSError stays a silent miss — a transient read failure is not corruption.

Scope is the consumer side only, as the issue requests; producers that write bad entries are tracked separately (#2380 / #2197).

Test verification (RED → GREEN)

New test test_corrupt_semantic_entry_warns_and_is_a_miss saves a semantic entry, corrupts it on disk, then asserts check_semantic_cache both treats it as a miss and warns.

RED (unmodified v8, test only):

tests/test_cache.py:1483: in test_corrupt_semantic_entry_warns_and_is_a_miss
    with pytest.warns(RuntimeWarning, match="corrupt"):
E   Failed: DID NOT WARN. No warnings of type (<class 'RuntimeWarning'>,) were emitted.
1 failed

GREEN (with fix):

1 passed, 1 warning in 0.26s

No regression

  • uv run --frozen pytest tests/ -q4329 passed, 3 skipped, plus the 3 pre-existing test_ollama.py backend-detection failures that also fail on a clean v8 checkout (unrelated to this change).
  • python -m tools.skillgen --checkcheck OK.

…ing them (Graphify-Labs#2405)

load_cached() caught json.JSONDecodeError together with OSError and returned
None, so a corrupt (invalid-JSON) cache entry was indistinguishable from a
miss. Left in place it fails to parse on every future run, silently
re-extracting — and, for semantic kinds, re-billing the LLM — the file
forever, with no diagnostic (a real vault had 138/249 entries dead).

Count JSONDecodeError failures in a process-global (mirroring the existing
_legacy_semantic_hits counter) and emit one aggregate RuntimeWarning from
check_semantic_cache naming how many entries could not be parsed and how to
clear them. OSError stays a silent miss (transient read failure, not
corruption).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 4 advisory finding(s) below merit a look before merge.


Graphify review — findings

This PR changes how the semantic cache handles cache entries that fail to parse as JSON. Previously invalid-JSON entries were caught alongside OSError and returned as a silent miss; now load_cached distinguishes json.JSONDecodeError, increments a module-level _corrupt_cache_entries counter, and check_semantic_cache emits one aggregate RuntimeWarning naming how many entries failed to parse. It touches graphify/cache.py (new counter, changed exception handling, new warning block), adds a corresponding test in tests/test_cache.py, and adds a changelog entry. The test the diff references in the changed-symbols list is broader than the single new test shown in the diff, but the visible surface area is the cache corruption-handling path, the new warning, and its test plus changelog note.

Worth a look

  • Module-level corrupt-entry counter is not thread-safe; concurrent semantic cache scans corrupt the deltagraphify/cache.py:924 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Corrupt-cache warning message never matches test regex 'corrupt'graphify/cache.py:1197 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Corrupt-entry count delta is process-global, not per-call, giving cross-contaminated warnings under concurrencygraphify/cache.py:1197 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Warning message does not contain the word 'corrupt' the test matches ongraphify/cache.py:1199 · 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 — 1378 functions depend on the 393 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: save_semantic_cache() — 51 callers, 9 callees
  • worse: file_hash() — 40 callers, 6 callees

Verification — 1378 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: 943 function(s) in the blast radius were not formally verified this run

· 2 more finding(s) on lines outside this diff (see the check run).

safishamsi added a commit that referenced this pull request Aug 13, 2026
Correctness (#2685 loop-binding shadow, #2699 provenance, #2707 affected
seed, #2688 py subpackage imports, #2602 sql grammar, #2683 cache
integrity, #2682 report basename) + Windows portability/docs batch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@safishamsi

Copy link
Copy Markdown
Collaborator

Shipped in v0.9.42 (graphifyy==0.9.42 on PyPI). Corrupt semantic-cache entries are surfaced and re-extracted. Credited in the release notes. Thanks @ousamabenyounes!

@safishamsi safishamsi closed this Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

load_cached() silently swallows corrupt cache entries (55% of a real-world cache was dead, no diagnostics)

2 participants