From 50b4b34d33654d84b19b42a2cbdcc6ce6c98fa1f Mon Sep 17 00:00:00 2001 From: TejasLamba2006 Date: Wed, 11 Nov 2026 01:41:37 +0530 Subject: [PATCH] fix(plugin): guard against non-dict ledger records in accounting and repos store.read_lines can return non-dict JSON values (null, arrays, strings) if a ledger file was corrupted or written incorrectly. Code calling .get() on these records crashes with AttributeError. Added isinstance(r, dict) checks before accessing record fields in: - accounting.summarize (filters at entry) - repos._infer_repo_from_state (inline check) - repos._merge_state (filters ledger lists, inline check for events) Same pattern as the distiller fix in #199. --- tokeneconomizer/plugin/accounting.py | 1 + tokeneconomizer/plugin/repos.py | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tokeneconomizer/plugin/accounting.py b/tokeneconomizer/plugin/accounting.py index cab043a..36e7d68 100644 --- a/tokeneconomizer/plugin/accounting.py +++ b/tokeneconomizer/plugin/accounting.py @@ -35,6 +35,7 @@ def summarize(records): A session imported from the agent's history (`imported: true`) ran before the plugin: it is counted on its own (`imported_sessions`) and left out of every other number.""" + records = [r for r in records if isinstance(r, dict)] imported = [r for r in records if r.get("kind") == "session" and r.get("imported")] records = [r for r in records if not r.get("imported")] sessions = [r for r in records if r.get("kind") == "session"] diff --git a/tokeneconomizer/plugin/repos.py b/tokeneconomizer/plugin/repos.py index e6a05ff..aa8b46c 100644 --- a/tokeneconomizer/plugin/repos.py +++ b/tokeneconomizer/plugin/repos.py @@ -235,7 +235,7 @@ def dir_label(repo_dir): if names: return names[0][:-5].replace("__", "/") for rec in reversed(st.read_lines("ledger.jsonl")): - if rec.get("repo"): + if isinstance(rec, dict) and rec.get("repo"): return rec["repo"] return "" @@ -386,7 +386,8 @@ def _merge_host(src, dst, sids, whole, label="", timeout=LOCK_WAIT_S): refold.add(sid) incoming = set(sids) - s_ledger, d_ledger = src.read_lines("ledger.jsonl"), dst.read_lines("ledger.jsonl") + s_ledger = [r for r in src.read_lines("ledger.jsonl") if isinstance(r, dict)] + d_ledger = [r for r in dst.read_lines("ledger.jsonl") if isinstance(r, dict)] # One session record per session: 0.1.0 could fold a session it had split over two keys # under both, and its cost counts once -- the later fold's, which read the longer log. # A refolded session's record is written again by its fold. @@ -421,7 +422,7 @@ def _merge_host(src, dst, sids, whole, label="", timeout=LOCK_WAIT_S): rec["folded"] = False dst.write(os.path.join("sessions", f"{sid}.json"), rec) for r in src.read_lines("events.jsonl"): - if r.get("session_id") in incoming: + if isinstance(r, dict) and r.get("session_id") in incoming: dst.append("events.jsonl", r) if whole: _merge_envelope(src, dst)