"""Append-only JSONL primitives for the Org OS state kernel.""" from __future__ import annotations import json import os from contextlib import nullcontext from typing import Any, Callable, ContextManager, Iterable def read_jsonl(path: str | None, on_error: Callable[[str], None] | None = None) -> list[dict[str, Any]]: if not path or not os.path.exists(path): return [] rows: list[dict[str, Any]] = [] try: with open(path, encoding="utf-8") as handle: for line in handle: try: value = json.loads(line) except Exception: continue if isinstance(value, dict): rows.append(value) except Exception as exc: if on_error: on_error(f"event 원장 읽기 실패({path}): {exc}") return rows def append_jsonl( path: str | None, event: dict[str, Any], *, file_lock: bool = False, on_error: Callable[[str], None] | None = None, ) -> bool: if not path: return False try: with open(path, "a", encoding="utf-8") as handle: if file_lock: try: import fcntl fcntl.flock(handle.fileno(), fcntl.LOCK_EX) except Exception: pass handle.write(json.dumps(event, ensure_ascii=False) + "\n") handle.flush() os.fsync(handle.fileno()) return True except Exception as exc: if on_error: on_error(f"event append 실패({path}): {exc}") return False def atomic_append( entries: Iterable[tuple[str | None, dict[str, Any]]], *, transaction_lock: ContextManager[Any] | None = None, ) -> None: """Append a group of events and truncate every participating tail on failure.""" normalized = list(entries) handles: list[tuple[Any, int]] = [] with (transaction_lock or nullcontext()): try: for path, _event in normalized: if not path: raise OSError("event path 해석 실패") handle = open(path, "a+", encoding="utf-8") try: import fcntl fcntl.flock(handle.fileno(), fcntl.LOCK_EX) except Exception: pass handle.seek(0, os.SEEK_END) handles.append((handle, handle.tell())) try: for (handle, _offset), (_path, event) in zip(handles, normalized): handle.write(json.dumps(event, ensure_ascii=False) + "\n") handle.flush() os.fsync(handle.fileno()) except Exception: for handle, offset in handles: handle.seek(offset) handle.truncate() handle.flush() raise finally: for handle, _offset in handles: try: handle.close() except Exception: pass