Skip to content

Commit 56e04a3

Browse files
kokokoXUYhuangruiteng
authored andcommitted
fix: frame the remaining JSONL readers on LF
#5117 fixed four readers of the per-Goal run index. The same framing defect exists in nine more places that parse one JSON document per line after `read_text(...).splitlines()`: - `chat_store.py` (stored chat rows) - `doctor.py` (installation index) - `domain_state.py` (domain state rows) - `event_sourced_state.py` (the append-only event log) - `domain_packs/issue_fix.py` (two readers) - `capabilities/explore/result_log.py` (three readers) All of these files also write with `json.dumps(..., ensure_ascii=False)`, which leaves U+0085/U+2028/U+2029 in a value verbatim, and `str.splitlines()` treats them as line breaks: one record becomes two fragments, `json.loads` fails on both, and the row is dropped or reported as invalid. Frame on LF instead. Validation: a new case in `tests/test_event_sourced_state_store.py` writes one event whose title carries U+0085 and asserts it round-trips; it raises `StateEventError` before the change and passes after. The other eight sites are the same one-line shape. `pytest -q tests/test_event_sourced_state_store.py tests/test_chat_store_input_validation.py` -> 23 passed, 1 failed, where that failure is `test_failure_before_replace_leaves_old_stream_intact` injecting `OSError("injected pre-publication failure")` inside the file lock; it fails identically with these edits reverted, so it is pre-existing on this host. `ruff check` on the changed files reports `All checks passed!`. Signed-off-by: kokokoXUY <13682395396@163.com> Keep the test imports above the module constant (E402).
1 parent 74d372c commit 56e04a3

7 files changed

Lines changed: 38 additions & 9 deletions

File tree

‎loopx/capabilities/explore/result_log.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ def append_explore_result_events(
506506
with exclusive_file_lock(log_path):
507507
existing_by_id: dict[str, dict[str, Any]] = {}
508508
if log_path.exists():
509-
for line in log_path.read_text(encoding="utf-8").splitlines():
509+
for line in log_path.read_text(encoding="utf-8").split("\n"):
510510
try:
511511
current = json.loads(line)
512512
except json.JSONDecodeError:
@@ -546,7 +546,7 @@ def load_explore_result_events(
546546
if not log_path.exists():
547547
return []
548548
events: list[dict[str, Any]] = []
549-
for line in log_path.read_text(encoding="utf-8").splitlines():
549+
for line in log_path.read_text(encoding="utf-8").split("\n"):
550550
stripped = line.strip()
551551
if not stripped:
552552
continue
@@ -579,7 +579,7 @@ def load_explore_result_events_strict(
579579
if not log_path.exists():
580580
return []
581581
events: list[dict[str, Any]] = []
582-
for line_number, line in enumerate(log_path.read_text(encoding="utf-8").splitlines(), start=1):
582+
for line_number, line in enumerate(log_path.read_text(encoding="utf-8").split("\n"), start=1):
583583
stripped = line.strip()
584584
if not stripped:
585585
continue

‎loopx/chat_store.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def _read_json(path: Path) -> dict[str, Any]:
7575

7676
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
7777
try:
78-
lines = path.read_text(encoding="utf-8").splitlines()
78+
lines = path.read_text(encoding="utf-8").split("\n")
7979
except OSError:
8080
return []
8181
rows: list[dict[str, Any]] = []

‎loopx/doctor.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,7 @@ def latest_promotion_readiness_event(runtime_root: Path, goal_id: str | None = N
625625
)
626626
for index_path, current_goal_id, source in indexes:
627627
try:
628-
lines = index_path.read_text(encoding="utf-8").splitlines()
628+
lines = index_path.read_text(encoding="utf-8").split("\n")
629629
except OSError:
630630
continue
631631
for line in lines:

‎loopx/domain_packs/issue_fix.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def promote_issue_fix_feasibility_ledger_jsonl(
170170
canonical_existing: dict[str, Any] | None = None
171171
if path.exists():
172172
for index, line in enumerate(
173-
path.read_text(encoding="utf-8").splitlines(), start=1
173+
path.read_text(encoding="utf-8").split("\n"), start=1
174174
):
175175
if not line.strip():
176176
continue
@@ -530,7 +530,7 @@ def retain_issue_fix_repository_snapshot_jsonl(
530530
path = Path(ledger_path)
531531
existing_rows: list[dict[str, Any]] = []
532532
if path.exists():
533-
for line in path.read_text(encoding="utf-8").splitlines():
533+
for line in path.read_text(encoding="utf-8").split("\n"):
534534
try:
535535
value = json.loads(line)
536536
except (TypeError, ValueError):

‎loopx/domain_state.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def upsert_domain_state_jsonl(
7171
candidate = {**payload, "domain_state_key": key}
7272
if path.exists():
7373
for index, line in enumerate(
74-
path.read_text(encoding="utf-8").splitlines(), start=1
74+
path.read_text(encoding="utf-8").split("\n"), start=1
7575
):
7676
if not line.strip():
7777
continue

‎loopx/event_sourced_state.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@ class AppendOnlyStateEventStore:
578578
def load(self) -> list[dict[str, Any]]:
579579
events: list[dict[str, Any]] = []
580580
if self.path.exists():
581-
for line_number, line in enumerate(self.path.read_text(encoding="utf-8").splitlines(), start=1):
581+
for line_number, line in enumerate(self.path.read_text(encoding="utf-8").split("\n"), start=1):
582582
if not line.strip():
583583
continue
584584
try:

‎tests/test_event_sourced_state_store.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
make_state_event,
1313
)
1414

15+
NEL = "\x85"
16+
1517

1618
def test_load_observes_events_appended_by_another_store(tmp_path: Path) -> None:
1719
event_log = tmp_path / "events.jsonl"
@@ -429,3 +431,30 @@ def test_concurrent_processes_publish_contiguous_batches(tmp_path: Path) -> None
429431
if row["event_id"].startswith(f"{worker}-")
430432
]
431433
assert positions == list(range(positions[0], positions[0] + 3))
434+
435+
436+
def test_load_reads_a_record_whose_value_carries_u0085(tmp_path: Path) -> None:
437+
"""The event log is one JSON document per LF.
438+
439+
`json.dumps(..., ensure_ascii=False)` keeps U+0085 inside a value verbatim,
440+
and `str.splitlines()` treats it as a line break, so a valid event arrived
441+
as two fragments and `load()` raised `StateEventError`.
442+
"""
443+
444+
event_log = tmp_path / "events.jsonl"
445+
store = AppendOnlyStateEventStore(event_log)
446+
event = make_state_event(
447+
event_id="evt-raw-separator",
448+
goal_id="goal-a",
449+
event_type=TODO_ADDED,
450+
refs={"todo_id": "todo_raw_separator"},
451+
payload={"role": "agent", "title": f"Observe the durable event.{NEL}"},
452+
recorded_at="2026-09-06T00:00:00Z",
453+
)
454+
event_log.write_text(
455+
json.dumps(event, ensure_ascii=False) + "\n", encoding="utf-8"
456+
)
457+
458+
loaded = store.load()
459+
460+
assert [item["event_id"] for item in loaded] == ["evt-raw-separator"]

0 commit comments

Comments
 (0)