From 1d6c1c2f8f4907bc9da8f3dcc5659054228093c2 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:37:02 +0530 Subject: [PATCH] fix(skills): abort blob GC on a non-dict ledger row instead of raising MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tests/tools/test_skill_ledger_delta.py | 4 +++- tools/skill_ledger.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_skill_ledger_delta.py b/tests/tools/test_skill_ledger_delta.py index dd420cd06c..dfcc1cd67e 100644 --- a/tests/tools/test_skill_ledger_delta.py +++ b/tests/tools/test_skill_ledger_delta.py @@ -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) diff --git a/tools/skill_ledger.py b/tools/skill_ledger.py index 160d883b52..ff5154ca82 100644 --- a/tools/skill_ledger.py +++ b/tools/skill_ledger.py @@ -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