fix(skills): abort blob GC on a non-dict ledger row instead of raising

After json.loads() succeeds, gc_blobs() called row.get(...) unconditionally
(tools/skill_ledger.py:353). A syntactically valid but non-object line such as `[]`
or `"x"` raised AttributeError out of gc_blobs() and up through
`hermes curator ledger --compact`. list_entries() already tolerates such rows with an
isinstance(row, dict) check (~L414); mirror it here and abort the sweep with the same
"malformed ledger line; blob GC skipped" warning — a row we cannot interpret may still
hold blob references, so nothing may be deleted.

Test: `non-dict-row` param on the kept invariant test (ledger + b"[]\n"). RED with the
guard removed (AttributeError: 'list' object has no attribute 'get'), GREEN at head.
This commit is contained in:
kshitijk4poor
2026-09-22 12:37:02 +05:30
committed by kshitij
parent 49aa7215e8
commit 1d6c1c2f8f
2 changed files with 6 additions and 1 deletions

View File

@@ -94,7 +94,7 @@ def test_gc_blobs_removes_only_unreferenced(ledger_home):
assert skill_ledger.gc_blobs() == (0, 0)
@pytest.mark.parametrize("failure", ["unreadable", "missing", "invalid-encoding", "malformed-json"])
@pytest.mark.parametrize("failure", ["unreadable", "missing", "invalid-encoding", "malformed-json", "non-dict-row"])
def test_gc_keeps_rollback_blobs_when_the_ledger_cannot_be_read(ledger_home, monkeypatch, caplog, failure):
from tools import skill_ledger
@@ -121,6 +121,8 @@ def test_gc_keeps_rollback_blobs_when_the_ledger_cannot_be_read(ledger_home, mon
ledger.unlink()
elif failure == "malformed-json":
ledger.write_bytes(saved + b"{broken\n")
elif failure == "non-dict-row":
ledger.write_bytes(saved + b"[]\n")
else:
ledger.write_bytes(b"\xff")
assert skill_ledger.gc_blobs() == (0, 0)

View File

@@ -351,6 +351,9 @@ def gc_blobs() -> Tuple[int, int]:
except json.JSONDecodeError:
logger.warning("skill_ledger: malformed ledger line; blob GC skipped")
return 0, 0
if not isinstance(row, dict):
logger.warning("skill_ledger: malformed ledger line; blob GC skipped")
return 0, 0
for item in (row.get("before") or []) + (row.get("after") or []):
referenced.add(str(item.get("sha256", "")))
deleted = freed = 0