Compare commits

...
Author SHA1 Message Date
praisonai-triage-agent[bot]andMervin Praison a1b6c89c8f fix(session): log skipped unreadable transcripts during search
Surface invalid-UTF-8/corrupt transcript skips via a debug log in both
the JSON scan and indexed candidate loader so a corrupt session is
observable instead of an indistinguishable silent miss. Search stays
read-only (no quarantine/rewrite), preserving corrupt bytes.

Co-authored-by: Mervin Praison <[email protected]>
2026-10-02 01:47:39 +00:00
Wu Shuwen aff24a9104 fix(session): skip invalid UTF-8 transcript search candidates 2026-10-02 09:41:47 +08:00
3 changed files with 53 additions and 2 deletions

No files matched your search

@@ -560,7 +560,16 @@ class SqliteSessionStore(DefaultSessionStore):
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, IOError, OSError):
except (UnicodeDecodeError, json.JSONDecodeError, IOError, OSError) as exc:
# Read-only path: never quarantine or rewrite during search.
# Log so an unreadable indexed candidate is observable rather
# than a silent miss, then keep scoring the remaining valid
# sessions.
logger.debug(
"Skipping unreadable session file %s during search: %s",
filepath,
exc,
)
continue
messages = self._searchable_messages(data)
@@ -2075,7 +2075,15 @@ class DefaultSessionStore:
try:
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, IOError):
except (UnicodeDecodeError, json.JSONDecodeError, IOError) as exc:
# Read-only path: never quarantine or rewrite during search.
# Log so an unreadable transcript is observable rather than a
# silent miss, then keep scoring the remaining valid sessions.
logger.debug(
"Skipping unreadable session file %s during search: %s",
filepath,
exc,
)
continue
messages = self._searchable_messages(data)
@@ -0,0 +1,34 @@
"""Unreadable transcript bytes must not interrupt recall of valid sessions."""
import pytest
from praisonaiagents.session import SqliteSessionStore
from praisonaiagents.session.store import DefaultSessionStore
@pytest.mark.parametrize("backend", ["json", "fts", "like"])
def test_search_skips_invalid_utf8_candidate(tmp_path, backend):
store = (
DefaultSessionStore(session_dir=str(tmp_path))
if backend == "json"
else SqliteSessionStore(session_dir=str(tmp_path), db_path=":memory:")
)
try:
assert store.add_message("bad", "user", "migration needle")
assert store.add_message("valid", "user", "migration needle")
if backend == "fts" and not store._fts_available:
pytest.skip("SQLite build lacks FTS5")
if backend == "like":
store._fts_available = False
assert {hit.session_id for hit in store.search("needle")} == {"bad", "valid"}
bad = tmp_path / "bad.json"
payload = b"\xff\xfe"
bad.write_bytes(payload)
hits = store.search("needle")
assert [hit.session_id for hit in hits] == ["valid"]
assert hits[0].messages[0]["content"] == "migration needle"
assert bad.read_bytes() == payload
finally:
conn = getattr(store, "_conn", None)
if conn is not None:
conn.close()