Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions loop/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,15 @@ def _store_path(target: str | Path) -> Path:
return resolve_loop_paths(target).loop_dir / "events.db"


def _readonly_query(path: Path) -> str:
"""Avoid creating sidecars on clean stores while preserving crash-left WAL reads."""
return "mode=ro" if (path.parent / (path.name + "-wal")).exists() else "mode=ro&immutable=1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recheck the WAL before trusting an immutable snapshot

When a writer starts an append after this exists() check returns false but before sqlite3.connect() opens the database, the URI remains immutable=1; SQLite then ignores the newly created WAL and its committed frames. Running status, replay, or doctor concurrently with an active loop can consequently report a stale event count or a false state divergence, so the WAL selection needs an atomic/retry check around opening the immutable connection.

Useful? React with 👍 / 👎.



def _read_events_readonly(path: Path, run_id: str) -> list[dict[str, Any]]:
"""Read the EventStore row shape without invoking its write-capable connector."""
try:
conn = sqlite3.connect(f"{path.absolute().as_uri()}?mode=ro", uri=True)
conn = sqlite3.connect(f"{path.absolute().as_uri()}?{_readonly_query(path)}", uri=True)
try:
rows = conn.execute(
"SELECT run_id, sequence, event_id, type, actor, causation_id, "
Expand Down Expand Up @@ -59,7 +64,7 @@ def _discover_run_id(path: Path) -> str:
if not path.exists():
raise RuntimeStoreError("missing_store", f"event store does not exist: {path}")
try:
conn = sqlite3.connect(f"{path.absolute().as_uri()}?mode=ro", uri=True)
conn = sqlite3.connect(f"{path.absolute().as_uri()}?{_readonly_query(path)}", uri=True)
try:
rows = conn.execute("SELECT DISTINCT run_id FROM events ORDER BY run_id ASC").fetchall()
finally:
Expand Down
11 changes: 11 additions & 0 deletions scripts/test_doctor_eventstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,14 @@ def test_ambiguous_run_id_fails_doctor(tmp_path):
assert report["ok"] is False
assert report["event_store"]["error_code"] == "ambiguous_run_id"
assert "ambiguous_run_id" in _codes(report)


def test_doctor_event_store_reads_do_not_leave_wal_or_shm_sidecars(tmp_path):
target = _fresh_contract(tmp_path)
_sync_active_task(target)
_open(_store(target))
sidecars = (target / ".loop" / "events.db-wal", target / ".loop" / "events.db-shm")
assert all(not path.exists() for path in sidecars)
report = doctor_report(target)
assert report["event_store"]["present"] is True
assert all(not path.exists() for path in sidecars)
13 changes: 10 additions & 3 deletions scripts/test_loop_cli_status_replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,18 @@ def test_events_db_is_opened_strictly_read_only_no_write_side_effects(tmp_path):
workspace, store = _workspace(tmp_path)
_terminal(store)
(workspace / ".loop" / "terminal_state.json").write_text('{"state":"Succeeded"}', encoding="utf-8")
files = sorted((workspace / ".loop").iterdir())
before = {p.name: (p.stat().st_mtime_ns, hashlib.sha256(p.read_bytes()).hexdigest()) for p in files if p.is_file()}
before = {
p.name: (p.stat().st_mtime_ns, hashlib.sha256(p.read_bytes()).hexdigest())
for p in sorted((workspace / ".loop").iterdir())
if p.is_file()
}
status_report(workspace)
replay_report(workspace)
after = {p.name: (p.stat().st_mtime_ns, hashlib.sha256(p.read_bytes()).hexdigest()) for p in files if p.is_file()}
after = {
p.name: (p.stat().st_mtime_ns, hashlib.sha256(p.read_bytes()).hexdigest())
for p in sorted((workspace / ".loop").iterdir())
if p.is_file()
}
assert after == before


Expand Down