Files
wehub-resource-sync 0418dc5cf9
CI / Python tests (3.10) (push) Has been cancelled
CI / Python tests (3.11) (push) Has been cancelled
CI / Python quality (push) Has been cancelled
CI / Frontend typecheck (push) Has been cancelled
Autopilot Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:45:00 +08:00

203 lines
6.8 KiB
Python

"""Session persistence for ``ohmo``."""
from __future__ import annotations
import json
import hashlib
import time
from pathlib import Path
from typing import Any
from uuid import uuid4
from openharness.api.usage import UsageSnapshot
from openharness.engine.messages import ConversationMessage, sanitize_conversation_messages
from openharness.services.session_backend import SessionBackend
from openharness.services.session_storage import (
_persistable_tool_metadata,
_sanitize_snapshot_payload,
)
from openharness.utils.fs import atomic_write_text
from ohmo.workspace import get_sessions_dir
def get_session_dir(workspace: str | Path | None = None) -> Path:
"""Return the ohmo sessions directory."""
session_dir = get_sessions_dir(workspace)
session_dir.mkdir(parents=True, exist_ok=True)
return session_dir
def _session_key_token(session_key: str) -> str:
return hashlib.sha1(session_key.encode("utf-8")).hexdigest()[:12]
def _session_key_latest_path(workspace: str | Path | None, session_key: str) -> Path:
session_dir = get_session_dir(workspace)
token = _session_key_token(session_key)
return session_dir / f"latest-{token}.json"
def save_session_snapshot(
*,
cwd: str | Path,
workspace: str | Path | None = None,
model: str,
system_prompt: str,
messages: list[ConversationMessage],
usage: UsageSnapshot,
session_id: str | None = None,
session_key: str | None = None,
tool_metadata: dict[str, object] | None = None,
) -> Path:
"""Persist the latest ohmo session snapshot."""
session_dir = get_session_dir(workspace)
sid = session_id or uuid4().hex[:12]
now = time.time()
messages = sanitize_conversation_messages(messages)
summary = ""
for msg in messages:
if msg.role == "user" and msg.text.strip():
summary = msg.text.strip()[:80]
break
payload = {
"app": "ohmo",
"session_id": sid,
"session_key": session_key,
"cwd": str(Path(cwd).resolve()),
"model": model,
"system_prompt": system_prompt,
"messages": [message.model_dump(mode="json") for message in messages],
"usage": usage.model_dump(),
"tool_metadata": _persistable_tool_metadata(tool_metadata),
"created_at": now,
"summary": summary,
"message_count": len(messages),
}
data = json.dumps(payload, indent=2) + "\n"
latest_path = session_dir / "latest.json"
atomic_write_text(latest_path, data)
if session_key:
atomic_write_text(_session_key_latest_path(workspace, session_key), data)
session_path = session_dir / f"session-{sid}.json"
atomic_write_text(session_path, data)
return latest_path
def load_latest(workspace: str | Path | None = None) -> dict[str, Any] | None:
path = get_session_dir(workspace) / "latest.json"
if not path.exists():
return None
return _sanitize_snapshot_payload(json.loads(path.read_text(encoding="utf-8")))
def load_latest_for_session_key(workspace: str | Path | None, session_key: str) -> dict[str, Any] | None:
path = _session_key_latest_path(workspace, session_key)
if path.exists():
return _sanitize_snapshot_payload(json.loads(path.read_text(encoding="utf-8")))
return None
def list_snapshots(workspace: str | Path | None = None, limit: int = 20) -> list[dict[str, Any]]:
session_dir = get_session_dir(workspace)
sessions: list[dict[str, Any]] = []
for path in sorted(session_dir.glob("session-*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
continue
sessions.append(
{
"session_id": data.get("session_id", path.stem.replace("session-", "")),
"summary": data.get("summary", ""),
"message_count": data.get("message_count", len(data.get("messages", []))),
"model": data.get("model", ""),
"created_at": data.get("created_at", path.stat().st_mtime),
}
)
if len(sessions) >= limit:
break
return sessions
def load_by_id(workspace: str | Path | None, session_id: str) -> dict[str, Any] | None:
path = get_session_dir(workspace) / f"session-{session_id}.json"
if path.exists():
return _sanitize_snapshot_payload(json.loads(path.read_text(encoding="utf-8")))
latest = load_latest(workspace)
if latest and (latest.get("session_id") == session_id or session_id == "latest"):
return latest
return None
def export_session_markdown(
*,
cwd: str | Path,
workspace: str | Path | None = None,
messages: list[ConversationMessage],
) -> Path:
path = get_session_dir(workspace) / "transcript.md"
parts = ["# ohmo Session Transcript"]
for message in messages:
parts.append(f"\n## {message.role.capitalize()}\n")
text = message.text.strip()
if text:
parts.append(text)
atomic_write_text(path, "\n".join(parts).strip() + "\n")
return path
class OhmoSessionBackend(SessionBackend):
"""Session backend rooted in ``.ohmo/sessions``."""
def __init__(self, workspace: str | Path | None = None) -> None:
self._workspace = workspace
def get_session_dir(self, cwd: str | Path) -> Path:
return get_session_dir(self._workspace)
def save_snapshot(
self,
*,
cwd: str | Path,
model: str,
system_prompt: str,
messages: list[ConversationMessage],
usage: UsageSnapshot,
session_id: str | None = None,
session_key: str | None = None,
tool_metadata: dict[str, object] | None = None,
) -> Path:
return save_session_snapshot(
cwd=cwd,
workspace=self._workspace,
model=model,
system_prompt=system_prompt,
messages=messages,
usage=usage,
session_id=session_id,
session_key=session_key,
tool_metadata=tool_metadata,
)
def load_latest(self, cwd: str | Path) -> dict[str, Any] | None:
return load_latest(self._workspace)
def list_snapshots(self, cwd: str | Path, limit: int = 20) -> list[dict[str, Any]]:
return list_snapshots(self._workspace, limit=limit)
def load_by_id(self, cwd: str | Path, session_id: str) -> dict[str, Any] | None:
return load_by_id(self._workspace, session_id)
def load_latest_for_session_key(self, session_key: str) -> dict[str, Any] | None:
return load_latest_for_session_key(self._workspace, session_key)
def export_markdown(
self,
*,
cwd: str | Path,
messages: list[ConversationMessage],
) -> Path:
return export_session_markdown(cwd=cwd, workspace=self._workspace, messages=messages)