session-catchup.py OpenCode adapter raises on malformed part rows instead of skipping them

Author: OthmanAdiCreated Sep 17, 2026Updated Sep 17, 2026
Labelsbughelp wantedgood first issue

Problem

The OpenCode adapter in session-catchup.py assumes well-formed rows in OpenCode's SQLite store. Two malformed shapes make the whole catchup run raise instead of skipping the row:

  1. _format_opencode_part does state = data.get('state') or {} and then state.get('input'). When a part row carries state as a string or list (not a dict), this raises AttributeError.
  2. The SQL query uses json_extract(data, '$.type') on every part row. When a row's data column is not valid JSON, SQLite raises malformed JSON for the whole statement, so nothing from that session is summarised.

Scope: the explicit session-catchup.py command on projects that use OpenCode, and only with corrupted or hand-edited OpenCode rows. Normal OpenCode stores are well-formed and are not affected. The Claude Code, Codex, Hermes and Pi catchup paths are separate code and are not affected.

Copies: the same code exists in the root scripts/session-catchup.py (Claude Code plugin path), skills/planning-with-files/scripts/session-catchup.py, the five skills/i18n/*/scripts/ copies, and every host bundle synced from the canonical file (python scripts/sync-ide-folders.py --verify lists them). The fix lands in the canonical copy and is synced from there; the root copy and the i18n copies are not covered by the sync tool and are updated by hand.

How to reproduce

Point the adapter at a copy of an OpenCode store, then:

sql
UPDATE part SET data = '{"type":"tool","tool":"write","state":"broken"}' WHERE id = '<some part id>';

and run python scripts/session-catchup.py <project>: AttributeError: 'str' object has no attribute 'get'.

Then:

sql
UPDATE part SET data = 'not json' WHERE id = '<some part id>';

and run it again: sqlite3.OperationalError: malformed JSON.

Suggested fix

  • Guard the shape: input_ = state.get('input') if isinstance(state, dict) else None (the same guard the read path a few lines below already uses), and treat a non-dict input_ as empty.
  • Filter non-JSON rows in SQL with json_valid(data) in the WHERE clause (available in SQLite 3.9+, which every supported Python ships), or catch sqlite3.OperationalError per session and skip that session with a one-line notice on stderr.
  • Add two rows of this shape to the existing OpenCode fixture in tests/ and assert the run completes and reports the healthy rows.

Origin

Found by the v3.19.0 release review (PRs #247, #248, #249). Pre-existing behaviour. Tracked as item 6 of #250 before that issue was split into one issue per item.

Source: OthmanAdi/planning-with-files