mirror of
https://github.com/andrewyng/openworker.git
synced 2026-08-30 22:53:41 +00:00
Merge pull request #56 from Saidheerajgollu/tolerate-corrupt-jsonl-on-load
Tolerate a corrupt line when loading a conversation .jsonl
This commit is contained in:
@@ -136,11 +136,21 @@ class ConversationStore:
|
||||
path = self._file(sid)
|
||||
if not path.exists():
|
||||
return None
|
||||
return [
|
||||
json.loads(line)
|
||||
for line in path.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
# Tolerate a corrupt/truncated line rather than failing the whole load. An append
|
||||
# interrupted mid-write (crash, disk full) leaves one malformed trailing line; a
|
||||
# bare `json.loads` in a comprehension would raise JSONDecodeError and make load()
|
||||
# throw every time thereafter — bricking that session on every surface that opens
|
||||
# it. Skip the bad line(s) and keep the recoverable history. (Every other JSON read
|
||||
# in this module is already tolerant; this one was the outlier.)
|
||||
messages: list[dict] = []
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
messages.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return messages
|
||||
|
||||
def _count(self, sid: str) -> int:
|
||||
path = self._file(sid)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""ConversationStore: a corrupt line in a .jsonl must not brick session load.
|
||||
|
||||
An append interrupted mid-write (crash, full disk) leaves one malformed trailing line.
|
||||
load() must skip it and return the recoverable history, not raise on every open.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from coworker.conversations import ConversationStore
|
||||
from coworker.sessions import SessionRecord
|
||||
|
||||
|
||||
def _seed(store: ConversationStore, sid: str, n: int) -> None:
|
||||
store.save(
|
||||
SessionRecord(
|
||||
session_id=sid,
|
||||
workspace="/tmp",
|
||||
model="m",
|
||||
mode="interactive",
|
||||
messages=[{"role": "user", "content": f"m{i}"} for i in range(n)],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_load_skips_a_corrupt_trailing_line(tmp_path):
|
||||
store = ConversationStore(tmp_path / "state")
|
||||
sid = "abc123def456"
|
||||
_seed(store, sid, 2)
|
||||
|
||||
# Simulate a torn write: append a truncated JSON line to the session's log.
|
||||
jsonl = tmp_path / "state" / "conversations" / f"{sid}.jsonl"
|
||||
with open(jsonl, "a", encoding="utf-8") as f:
|
||||
f.write('{"role": "user", "content": "unterm\n') # no closing brace/quote
|
||||
|
||||
loaded = store.load(sid) # must not raise
|
||||
assert loaded is not None
|
||||
# The two good messages survive; the corrupt line is dropped.
|
||||
assert [m["content"] for m in loaded.messages] == ["m0", "m1"]
|
||||
|
||||
|
||||
def test_load_skips_a_corrupt_middle_line(tmp_path):
|
||||
store = ConversationStore(tmp_path / "state")
|
||||
sid = "def456abc123"
|
||||
jsonl = tmp_path / "state" / "conversations" / f"{sid}.jsonl"
|
||||
jsonl.parent.mkdir(parents=True, exist_ok=True)
|
||||
jsonl.write_text(
|
||||
'{"role": "user", "content": "first"}\n'
|
||||
"not json at all\n"
|
||||
'{"role": "assistant", "content": "third"}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
# Register the session in the index so load() reaches the .jsonl.
|
||||
store._conn.execute(
|
||||
"INSERT INTO sessions (session_id, workspace, model, mode, title, n_msgs) "
|
||||
"VALUES (?, '/tmp', 'm', 'interactive', 't', 2)",
|
||||
(sid,),
|
||||
)
|
||||
store._conn.commit()
|
||||
|
||||
loaded = store.load(sid)
|
||||
assert loaded is not None
|
||||
assert [m["content"] for m in loaded.messages] == ["first", "third"]
|
||||
Reference in New Issue
Block a user