fix(tools,tui_gateway,cli,plugins): parseable non-dict JSON no longer crashes the remaining file scans

Same class as the Bot Chat drain wedge already on this branch: every JSON-file
scan guarded "did it parse?" and then assumed the value was a dict. A file
holding `42`, `"oops"` or `[1,2,3]` (corruption, truncated write, foreign tool)
passed the guard and raised AttributeError/TypeError at the first `.get()`,
usually before a single healthy sibling was processed. Each site now treats a
non-object payload like a corrupt file under that subsystem's existing policy:

- tools/bot_relay.py::_expire_if_stale / claim_pending_envelopes — the
  envelope is skipped by the sweep and not claimed (same as unparseable).
- tools/browser_lightpanda.py::reap_orphaned_lightpanda — record unlinked,
  scan continues.
- tools/write_approval.py::list_pending / get_pending — record skipped with
  the existing "unreadable pending record" warning / None.
- tui_gateway/methods_session.py::_legacy_spawn_tree_entry / spawn_tree.load —
  scalar snapshot reads as empty / returns the existing 5000 error instead of
  violating the SpawnTreeLoadResult contract.
- hermes_cli/local_runtime/binaries.py::manifest_verified — False.
- plugins/platforms/a2a/protocol.py::load_conversation — non-dict lines are
  dropped, keeping the declared list[dict] return.
- batch_runner.py::_load_dataset / _scan_completed_prompts_by_content /
  _combine_batch_files — line skipped and counted as filtered.
- trajectory_compressor.py::process_entry_async — scalar entry passed through
  unchanged.

Ported from the source hunks of PR #114241; its gateway/shutdown_flush.py
drain_transcript_spool hunk is left to open PR #84785, and its
recover_pending_to_db / cron / bot_live_delivery / bot_mode_dm hunks are
already on this branch or on main.

(cherry picked from commit d4b54568887e69b3ee3d363ebe4dcd657ccf64f9)
This commit is contained in:
beardthelion
2026-09-18 01:35:01 -07:00
committed by Teknium
parent 1f4ab2cc78
commit 9ac4e76e0f
8 changed files with 33 additions and 12 deletions

View File

@@ -2160,6 +2160,8 @@ def _legacy_spawn_tree_entry(p, session_dir_name: str) -> dict | None:
raw = {}
with contextlib.suppress(Exception):
raw = json.loads(p.read_text(encoding="utf-8"))
if not isinstance(raw, dict):
raw = {}
subagents = raw.get("subagents") or []
return {"path": str(p), "session_id": raw.get("session_id") or session_dir_name,
"finished_at": raw.get("finished_at") or stat.st_mtime, "started_at": raw.get("started_at"),
@@ -2198,6 +2200,8 @@ def _(rid, params: dict) -> dict:
payload = json.loads(resolved.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
return _err(rid, 5000, f"spawn_tree.load failed: {exc}")
if not isinstance(payload, dict):
return _err(rid, 5000, "spawn_tree.load failed: snapshot is not a JSON object")
return _ok(rid, payload)