chore: anchor fresh-start history to upstream
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""Independent, metadata-only project scope for history retrieval.
|
||||
|
||||
The caller supplies the current profile's SessionDB and an exact session id.
|
||||
Only ``status == 'ready'`` authorizes ``allowed_session_ids``. Re-resolve immediately
|
||||
before returning history and compare ``revision``; this is an optimistic
|
||||
revision, not a transaction spanning projects.db, state.db, and the filesystem.
|
||||
No active-project pointer, process cwd, transcript, or plugin data is consulted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
from contextlib import closing
|
||||
from functools import lru_cache
|
||||
|
||||
from hermes_cli import projects_db
|
||||
from hermes_constants import get_hermes_home
|
||||
from tui_gateway import git_probe
|
||||
from tui_gateway.project_tree import SessionProjectOwnership, base_name
|
||||
|
||||
_EXCLUDED_SOURCES = frozenset({"kanban", "subagent", "tool", "cron"})
|
||||
|
||||
|
||||
def _projects() -> list[dict]:
|
||||
path = projects_db.projects_db_path()
|
||||
if not path.exists():
|
||||
return []
|
||||
with closing(sqlite3.connect(path.resolve().as_uri() + "?mode=ro", uri=True)) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("BEGIN")
|
||||
return [p.to_dict() for p in projects_db.list_projects(conn, include_archived=True)]
|
||||
|
||||
|
||||
def _ownership(projects: list[dict]) -> SessionProjectOwnership:
|
||||
# Match Desktop's host policy without importing the RPC/server binding layer.
|
||||
home = os.path.realpath(os.path.expanduser("~"))
|
||||
broad = {os.path.normcase(os.path.realpath(p)) for p in
|
||||
(os.sep, home, os.path.dirname(home), "/home", "/Users") if p}
|
||||
hermes_home = os.path.normcase(os.path.realpath(str(get_hermes_home())))
|
||||
|
||||
def junk_cwd(path):
|
||||
real = os.path.normcase(os.path.realpath(path))
|
||||
return not path or real in broad or real == hermes_home
|
||||
|
||||
def junk_root(path):
|
||||
real = os.path.normcase(os.path.realpath(path))
|
||||
return junk_cwd(path) or real.startswith(hermes_home + os.sep)
|
||||
|
||||
return SessionProjectOwnership(
|
||||
projects, lru_cache(maxsize=None)(git_probe.resolve),
|
||||
is_junk_root=lru_cache(maxsize=None)(junk_root),
|
||||
is_junk_cwd=lru_cache(maxsize=None)(junk_cwd),
|
||||
exists=lru_cache(maxsize=None)(os.path.isdir))
|
||||
|
||||
|
||||
def resolve_project_scope(db, current_session_id: str) -> dict:
|
||||
"""Resolve one session and enumerate every same-project session metadata row.
|
||||
|
||||
Returns JSON-safe ``status``, ``project_key``, ``label``, ``revision``,
|
||||
``allowed_session_ids`` and ``coverage``. Only ``ready`` authorizes IDs;
|
||||
``scope_unresolved`` and ``scope_ambiguous`` deny. Database/probe errors
|
||||
propagate: callers must fail closed, never fall back to unscoped search.
|
||||
Hidden/background rows are excluded; ordinary archived rows are included.
|
||||
User IDs match exactly; absent/empty principals match only other absent/empty
|
||||
principals in this local profile DB, not identified users.
|
||||
"""
|
||||
projects = _projects()
|
||||
ownership = _ownership(projects)
|
||||
digest = hashlib.sha256()
|
||||
|
||||
def stamp(value):
|
||||
digest.update(json.dumps(value, sort_keys=True, separators=(",", ":"),
|
||||
ensure_ascii=True).encode("utf-8"))
|
||||
digest.update(b"\n")
|
||||
|
||||
stamp({"version": 1, "profile_home": str(get_hermes_home().resolve()),
|
||||
"current_session_id": current_session_id})
|
||||
for project in sorted(projects, key=lambda p: p["id"]):
|
||||
project["folders"] = sorted(project["folders"], key=lambda f: f["path"])
|
||||
stamp(project)
|
||||
buckets: dict[tuple, list[str]] = {}
|
||||
current = {"status": "session_not_found", "project_key": None, "project_kind": None}
|
||||
current_principal = None
|
||||
current_eligible = False
|
||||
scanned = 0
|
||||
with db._read_ctx() as conn:
|
||||
cursor = conn.execute(
|
||||
"SELECT id, cwd, git_repo_root, git_branch, parent_session_id, profile_name, "
|
||||
"source, user_id, archived, hidden FROM sessions ORDER BY id")
|
||||
try:
|
||||
for raw in cursor:
|
||||
row = dict(raw)
|
||||
scanned += 1
|
||||
assigned = ownership.classify(row)
|
||||
stamp([row, assigned])
|
||||
principal = row["user_id"] or None
|
||||
eligible = not row["hidden"] and row["source"] not in _EXCLUDED_SOURCES
|
||||
if row["id"] == current_session_id:
|
||||
current = assigned
|
||||
current_principal = principal
|
||||
current_eligible = eligible
|
||||
if assigned["status"] == "ok" and eligible:
|
||||
key = (ownership.identity(assigned), principal)
|
||||
buckets.setdefault(key, []).append(row["id"])
|
||||
finally:
|
||||
cursor.close()
|
||||
status = {"ok": "ready", "ambiguous": "scope_ambiguous"}.get(
|
||||
current["status"], "scope_unresolved")
|
||||
if not current_eligible:
|
||||
status = "scope_unresolved"
|
||||
allowed = buckets.get((ownership.identity(current), current_principal), []) if status == "ready" else []
|
||||
project_key = current["project_key"] if status == "ready" else None
|
||||
label = None
|
||||
if project_key:
|
||||
label = next((p["name"] for p in projects if p["id"] == project_key), None)
|
||||
label = label or base_name(project_key) or project_key
|
||||
return {
|
||||
"status": status, "project_key": project_key, "label": label,
|
||||
"revision": digest.hexdigest(), "allowed_session_ids": allowed,
|
||||
"coverage": {"metadata_complete": True, "scanned_sessions": scanned,
|
||||
"allowed_sessions": len(allowed), "archived_included": True,
|
||||
"excluded_sources": sorted(_EXCLUDED_SOURCES), "hidden_excluded": True,
|
||||
"principal_policy": "same_user_id" if current_principal else "unknown_local_only",
|
||||
"project_kind": current["project_kind"], "resolution": current["status"],
|
||||
"revision_policy": "optimistic_metadata_and_cached_git"}}
|
||||
Reference in New Issue
Block a user