from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Literal
from core.utils.logger import logger
SessionResolutionStatus = Literal["found", "not_found", "ambiguous"]
@dataclass(frozen=True)
class SessionRunResolution:
"""Result of resolving a public session id to an internal run directory."""
status: SessionResolutionStatus
run_ids: tuple[str, ...] = ()
@property
def run_id(self) -> str:
return self.run_ids[0] if self.status == "found" and self.run_ids else ""
def resolve_run_by_session(output_root: str | Path, session_id: str) -> SessionRunResolution:
"""Resolve one original run for ``session_id`` without ever guessing.
``run_id`` is an internal storage key. Agents normally retain only the
public ``session_id`` and use this resolver for continuation, staged work,
and patch rendering. Historical ``resume: true`` records are ignored
because they are attempts to continue an original run, not new runs.
"""
if not session_id:
return SessionRunResolution("not_found")
output_path = Path(output_root)
if not output_path.is_dir():
return SessionRunResolution("not_found")
matches: dict[str, Path] = {}
for run_json in output_path.glob("*/run.json"):
try:
data = json.loads(run_json.read_text(encoding="utf-8"))
except (OSError, ValueError) as error:
logger.warning(f"failed to load {run_json}: {error}")
continue
if not isinstance(data, dict):
continue
if data.get("session_id") != session_id or data.get("resume"):
continue
run_id = data.get("run_id")
if isinstance(run_id, str) and run_id:
matches[run_id] = run_json
run_ids = tuple(sorted(matches))
if not run_ids:
return SessionRunResolution("not_found")
if len(run_ids) == 1:
logger.info(
f"Recovered run_id={run_ids[0]!r} for session_id={session_id!r}"
)
return SessionRunResolution("found", run_ids)
logger.warning(
f"session_id {session_id!r} matches {len(run_ids)} original runs: "
f"{list(run_ids)}. Refusing to guess."
)
return SessionRunResolution("ambiguous", run_ids)
def session_resolution_message(
resolution: SessionRunResolution,
session_id: str,
*,
action: str,
) -> str:
"""Return a concise structured-CLI error for a failed resolution."""
if resolution.status == "ambiguous":
return (
f"cannot {action}: session_id={session_id!r} matches multiple runs "
f"({', '.join(resolution.run_ids)}). Use a unique session id; "
"--run-id is reserved for manual disambiguation."
)
return (
f"cannot {action}: no prior run.json matches session_id={session_id!r}."
)