150 lines
6.2 KiB
Python
150 lines
6.2 KiB
Python
"""Source-backed recall for an attached live session, never a supplied storage ID.
|
|
|
|
Wire identity: session_id is the live UI ID; source_session_id is a historical
|
|
storage ID. The authorized record supplies the current storage ID and profile.
|
|
"""
|
|
|
|
from .method_ctx import HandlerRegistry, bind_module
|
|
|
|
_registry = HandlerRegistry()
|
|
method = _registry.method
|
|
|
|
|
|
_RECALL_FIELDS = {
|
|
"search": {"query", "search_cursor", "limit", "window", "sort"},
|
|
"read": {"source_session_id", "around_message_id", "after_message_id",
|
|
"before_message_id", "content_offset", "content_length", "window"},
|
|
"status": set(),
|
|
}
|
|
_RECALL_INTS = {
|
|
"limit": (1, 10), "window": (1, 20), "around_message_id": (1, 2**63 - 1),
|
|
"after_message_id": (0, 2**63 - 1), "before_message_id": (1, 2**63 - 1),
|
|
"content_offset": (0, 2**63 - 1), "content_length": (1, 4000),
|
|
}
|
|
|
|
|
|
def _recall_arguments(params, mode):
|
|
allowed = _RECALL_FIELDS[mode]
|
|
if set(params) - allowed - {"session_id", "profile"}:
|
|
raise ValueError("unsupported recall parameter")
|
|
args = {k: v for k, v in params.items() if k in allowed and v is not None}
|
|
for name, value in args.items():
|
|
if name in _RECALL_INTS:
|
|
low, high = _RECALL_INTS[name]
|
|
if type(value) is not int or not low <= value <= high:
|
|
raise ValueError(f"invalid {name}")
|
|
elif not isinstance(value, str):
|
|
raise ValueError(f"invalid {name}")
|
|
if "query" in args and len(args["query"]) > 4000:
|
|
raise ValueError("query too long")
|
|
if "search_cursor" in args and len(args["search_cursor"]) > 2048:
|
|
raise ValueError("search_cursor too long")
|
|
if "sort" in args and args["sort"] not in {"relevance", "newest", "oldest"}:
|
|
raise ValueError("invalid sort")
|
|
if "search_cursor" in args and not args.get("query", "").strip():
|
|
raise ValueError("search_cursor requires query")
|
|
if mode == "read":
|
|
if (args.get("content_offset", 0) or args.get("content_length", 4000) != 4000) and "around_message_id" not in args:
|
|
raise ValueError("content slicing requires around_message_id")
|
|
if "before_message_id" in args and "around_message_id" not in args:
|
|
raise ValueError("backward scan requires around_message_id")
|
|
target = args.pop("source_session_id", None)
|
|
if not target or len(target) > 512:
|
|
raise ValueError("source_session_id required")
|
|
args["session_id"] = target
|
|
return args
|
|
|
|
|
|
def _recall_storage_id(owner):
|
|
return getattr(owner.get("agent"), "session_id", None) or owner.get("session_key")
|
|
|
|
|
|
def _recall_scoped_query(rid, params):
|
|
from hermes_state_registry import acquire
|
|
from tools.session_search_project import ProjectRecall, project_session_search
|
|
|
|
if get_hermes_home().resolve() != params["_owner_home"]:
|
|
return _err(rid, 4001, "profile does not own this session")
|
|
db = acquire(get_hermes_home() / "state.db")
|
|
try:
|
|
recall = ProjectRecall(db, params["_current_storage_id"])
|
|
if recall.scope["status"] != "ready":
|
|
result = recall.response(success=False, status=recall.scope["status"], results=[])
|
|
elif params["_mode"] == "status":
|
|
result = recall.response(session_count=len(recall.allowed))
|
|
else:
|
|
result = json.loads(project_session_search(
|
|
db, params["_current_storage_id"], **params["_args"]))
|
|
# Keep the initial scope gate valid even if the query resolved a new scope.
|
|
checked = recall.response(evidence=result)
|
|
if not checked["success"]:
|
|
result = checked
|
|
elif "scope" not in result:
|
|
result["scope"] = checked["scope"]
|
|
return result
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
def _recall_rpc(rid, params, mode):
|
|
from hermes_cli.profiles import get_profile_dir, validate_profile_name
|
|
|
|
sid = params.get("session_id")
|
|
if not isinstance(sid, str):
|
|
return _err(rid, 4001, "session not found or not owned by this transport")
|
|
transport, owner = _current_session_steer_authority(sid)
|
|
if transport is None or owner is None:
|
|
return _err(rid, 4001, "session not found or not owned by this transport")
|
|
try:
|
|
args = _recall_arguments(params, mode)
|
|
except ValueError as exc:
|
|
return _err(rid, 4000, str(exc))
|
|
home = Path(owner.get("profile_home") or _hermes_home).resolve()
|
|
profile = params.get("profile")
|
|
if profile is not None:
|
|
try:
|
|
if not isinstance(profile, str):
|
|
raise ValueError("profile must be a string")
|
|
validate_profile_name(profile)
|
|
requested = Path(_profile_home(profile) or _hermes_home).resolve()
|
|
except (ValueError, OSError):
|
|
return _err(rid, 4000, "invalid profile")
|
|
if requested != home:
|
|
return _err(rid, 4001, "profile does not own this session")
|
|
profile = None
|
|
if home != Path(_hermes_home).resolve():
|
|
profile = "default" if home == get_profile_dir("default").resolve() else home.name
|
|
stored_id = _recall_storage_id(owner)
|
|
scoped = {"profile": profile, "_owner_home": home, "_current_storage_id": stored_id,
|
|
"_mode": mode, "_args": args}
|
|
try:
|
|
result = _profile_scoped(_recall_scoped_query)(rid, scoped)
|
|
except Exception:
|
|
logger.exception("Project recall store or scope unavailable")
|
|
return _err(rid, 5031, "project recall unavailable")
|
|
with _sessions_lock:
|
|
now_transport, now_owner = _current_session_steer_authority(sid)
|
|
if (now_transport is not transport or now_owner is not owner
|
|
or _recall_storage_id(owner) != stored_id
|
|
or Path(owner.get("profile_home") or _hermes_home).resolve() != home):
|
|
return _err(rid, 4001, "session authority changed during recall")
|
|
return result if "error" in result else _ok(rid, result)
|
|
|
|
|
|
@method("project.recall.search")
|
|
def _recall_search(rid, params):
|
|
return _recall_rpc(rid, params, "search")
|
|
|
|
|
|
@method("project.recall.read")
|
|
def _recall_read(rid, params):
|
|
return _recall_rpc(rid, params, "read")
|
|
|
|
|
|
@method("project.recall.status")
|
|
def _recall_status(rid, params):
|
|
return _recall_rpc(rid, params, "status")
|
|
|
|
|
|
def register(server):
|
|
bind_module(globals(), server) |