diff --git a/loopx/capabilities/explore/result_log.py b/loopx/capabilities/explore/result_log.py index 6f5dfc0c1..433fce8d3 100644 --- a/loopx/capabilities/explore/result_log.py +++ b/loopx/capabilities/explore/result_log.py @@ -506,7 +506,7 @@ def append_explore_result_events( with exclusive_file_lock(log_path): existing_by_id: dict[str, dict[str, Any]] = {} if log_path.exists(): - for line in log_path.read_text(encoding="utf-8").splitlines(): + for line in log_path.read_text(encoding="utf-8").split("\n"): try: current = json.loads(line) except json.JSONDecodeError: @@ -546,7 +546,7 @@ def load_explore_result_events( if not log_path.exists(): return [] events: list[dict[str, Any]] = [] - for line in log_path.read_text(encoding="utf-8").splitlines(): + for line in log_path.read_text(encoding="utf-8").split("\n"): stripped = line.strip() if not stripped: continue @@ -579,7 +579,7 @@ def load_explore_result_events_strict( if not log_path.exists(): return [] events: list[dict[str, Any]] = [] - for line_number, line in enumerate(log_path.read_text(encoding="utf-8").splitlines(), start=1): + for line_number, line in enumerate(log_path.read_text(encoding="utf-8").split("\n"), start=1): stripped = line.strip() if not stripped: continue diff --git a/loopx/chat_store.py b/loopx/chat_store.py index 259aa22fc..a182ed84c 100644 --- a/loopx/chat_store.py +++ b/loopx/chat_store.py @@ -75,7 +75,7 @@ def _read_json(path: Path) -> dict[str, Any]: def _read_jsonl(path: Path) -> list[dict[str, Any]]: try: - lines = path.read_text(encoding="utf-8").splitlines() + lines = path.read_text(encoding="utf-8").split("\n") except OSError: return [] rows: list[dict[str, Any]] = [] diff --git a/loopx/doctor.py b/loopx/doctor.py index bffb9fc22..ab5923544 100644 --- a/loopx/doctor.py +++ b/loopx/doctor.py @@ -625,7 +625,7 @@ def latest_promotion_readiness_event(runtime_root: Path, goal_id: str | None = N ) for index_path, current_goal_id, source in indexes: try: - lines = index_path.read_text(encoding="utf-8").splitlines() + lines = index_path.read_text(encoding="utf-8").split("\n") except OSError: continue for line in lines: diff --git a/loopx/domain_packs/issue_fix.py b/loopx/domain_packs/issue_fix.py index 89e9f9742..2a847f37f 100644 --- a/loopx/domain_packs/issue_fix.py +++ b/loopx/domain_packs/issue_fix.py @@ -170,7 +170,7 @@ def promote_issue_fix_feasibility_ledger_jsonl( canonical_existing: dict[str, Any] | None = None if path.exists(): for index, line in enumerate( - path.read_text(encoding="utf-8").splitlines(), start=1 + path.read_text(encoding="utf-8").split("\n"), start=1 ): if not line.strip(): continue @@ -530,7 +530,7 @@ def retain_issue_fix_repository_snapshot_jsonl( path = Path(ledger_path) existing_rows: list[dict[str, Any]] = [] if path.exists(): - for line in path.read_text(encoding="utf-8").splitlines(): + for line in path.read_text(encoding="utf-8").split("\n"): try: value = json.loads(line) except (TypeError, ValueError): diff --git a/loopx/domain_state.py b/loopx/domain_state.py index a447a2bc9..3590e6739 100644 --- a/loopx/domain_state.py +++ b/loopx/domain_state.py @@ -71,7 +71,7 @@ def upsert_domain_state_jsonl( candidate = {**payload, "domain_state_key": key} if path.exists(): for index, line in enumerate( - path.read_text(encoding="utf-8").splitlines(), start=1 + path.read_text(encoding="utf-8").split("\n"), start=1 ): if not line.strip(): continue diff --git a/loopx/event_sourced_state.py b/loopx/event_sourced_state.py index 82f97dffc..e33293dde 100644 --- a/loopx/event_sourced_state.py +++ b/loopx/event_sourced_state.py @@ -578,7 +578,7 @@ class AppendOnlyStateEventStore: def load(self) -> list[dict[str, Any]]: events: list[dict[str, Any]] = [] if self.path.exists(): - for line_number, line in enumerate(self.path.read_text(encoding="utf-8").splitlines(), start=1): + for line_number, line in enumerate(self.path.read_text(encoding="utf-8").split("\n"), start=1): if not line.strip(): continue try: diff --git a/tests/test_event_sourced_state_store.py b/tests/test_event_sourced_state_store.py index e09bd8c90..86346bdc1 100644 --- a/tests/test_event_sourced_state_store.py +++ b/tests/test_event_sourced_state_store.py @@ -12,6 +12,8 @@ make_state_event, ) +NEL = "\x85" + def test_load_observes_events_appended_by_another_store(tmp_path: Path) -> None: event_log = tmp_path / "events.jsonl" @@ -429,3 +431,30 @@ def test_concurrent_processes_publish_contiguous_batches(tmp_path: Path) -> None if row["event_id"].startswith(f"{worker}-") ] assert positions == list(range(positions[0], positions[0] + 3)) + + +def test_load_reads_a_record_whose_value_carries_u0085(tmp_path: Path) -> None: + """The event log is one JSON document per LF. + + `json.dumps(..., ensure_ascii=False)` keeps U+0085 inside a value verbatim, + and `str.splitlines()` treats it as a line break, so a valid event arrived + as two fragments and `load()` raised `StateEventError`. + """ + + event_log = tmp_path / "events.jsonl" + store = AppendOnlyStateEventStore(event_log) + event = make_state_event( + event_id="evt-raw-separator", + goal_id="goal-a", + event_type=TODO_ADDED, + refs={"todo_id": "todo_raw_separator"}, + payload={"role": "agent", "title": f"Observe the durable event.{NEL}"}, + recorded_at="2026-09-06T00:00:00Z", + ) + event_log.write_text( + json.dumps(event, ensure_ascii=False) + "\n", encoding="utf-8" + ) + + loaded = store.load() + + assert [item["event_id"] for item in loaded] == ["evt-raw-separator"]