From 10ecfbb376e63edd20f9811fe6f4b0b2f3907482 Mon Sep 17 00:00:00 2001 From: TARS Date: Fri, 3 Jul 2026 20:04:41 +0200 Subject: [PATCH] fix: encode read-only session DB URI --- api/agent_sessions.py | 10 ++++- ...t_issue5455_listing_readonly_connection.py | 39 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/api/agent_sessions.py b/api/agent_sessions.py index 9203dc2a..e1555270 100644 --- a/api/agent_sessions.py +++ b/api/agent_sessions.py @@ -458,9 +458,15 @@ def read_importable_agent_session_rows( # the agent streams into it adds needless checkpoint/lock surface (#5455). # The defensive index self-heal below still runs, but through a separate # short-lived writable connection on the rare missing-index path only. + read_only_uri = f"{db_path.resolve().as_uri()}?mode=ro" try: - conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) - except sqlite3.Error: + conn = sqlite3.connect(read_only_uri, uri=True) + except sqlite3.Error as exc: + log.warning( + "agent session listing read-only open failed for %s; falling back to writable connection: %s", + db_path, + exc, + ) conn = sqlite3.connect(str(db_path)) with closing(conn): conn.row_factory = sqlite3.Row diff --git a/tests/test_issue5455_listing_readonly_connection.py b/tests/test_issue5455_listing_readonly_connection.py index b3b2858d..fc2cb263 100644 --- a/tests/test_issue5455_listing_readonly_connection.py +++ b/tests/test_issue5455_listing_readonly_connection.py @@ -12,6 +12,7 @@ writable connection. With the index present (the normal case) the read path performs zero writes; when the index is missing the self-heal still runs and rows still come back. """ +import logging import sqlite3 import api.agent_sessions as agent_sessions @@ -73,6 +74,44 @@ def test_listing_opens_read_only_and_returns_rows(tmp_path, monkeypatch): assert "mode=ro" in calls[0]["target"] +def test_listing_read_only_uri_encodes_special_path_chars(tmp_path, monkeypatch): + db_dir = tmp_path / "state dir #1" + db_dir.mkdir() + db = db_dir / "state?.db" + _make_db(db, with_index=True) + calls = _record_connects(monkeypatch) + + out = read_importable_agent_session_rows(db, exclude_sources=None) + + assert "cli-1" in {r["id"] for r in out} + assert calls[0]["uri"] is True + assert calls[0]["target"].startswith("file://") + assert "%20" in calls[0]["target"] + assert "%23" in calls[0]["target"] + assert "%3F" in calls[0]["target"] + assert calls[0]["target"].endswith("?mode=ro") + + +def test_read_only_open_fallback_is_logged(tmp_path, monkeypatch, caplog): + db = tmp_path / "state.db" + _make_db(db, with_index=True) + real_connect = sqlite3.connect + + def fail_read_only(target, *args, **kwargs): + if kwargs.get("uri"): + raise sqlite3.OperationalError("synthetic read-only URI failure") + return real_connect(target, *args, **kwargs) + + monkeypatch.setattr(agent_sessions.sqlite3, "connect", fail_read_only) + + with caplog.at_level(logging.WARNING, logger="api.agent_sessions"): + out = read_importable_agent_session_rows(db, exclude_sources=None) + + assert "cli-1" in {r["id"] for r in out} + assert "read-only open failed" in caplog.text + assert "synthetic read-only URI failure" in caplog.text + + def test_index_present_performs_no_writable_connection(tmp_path, monkeypatch): db = tmp_path / "state.db" _make_db(db, with_index=True)