"""Shared helpers for company-mode session scoping and continuity guards. Phase A (role-instance model): the session / queue key is keyed by ``(session_scope, role_id)`` — *not* by seat. A role that appears as a member in multiple teams (e.g. CMO is both CEO's subordinate and the leader of her own team) has **one** session and **one** queue. The seat id stays as organizational metadata but no longer affects identity. If a later refactor needs to support multiple parallel instances of the same role (e.g. two CMOs in parallel branches of a run), pass ``team_instance_id`` to disambiguate — it is appended to the key when present. """ from __future__ import annotations from opc.core.models import Task from opc.layer2_organization.work_item_runtime import is_work_item_runtime_metadata def task_session_scope_id(task: Task) -> str: """Return the top-level session scope for a company-mode task.""" metadata = dict(getattr(task, "metadata", {}) or {}) return str( getattr(task, "parent_session_id", "") or metadata.get("parent_session_id", "") or getattr(task, "session_id", "") or metadata.get("session_id", "") or "" ).strip() def scoped_member_session_id( *, project_id: str, session_scope_id: str, role_id: str, employee_id: str, team_instance_id: str = "", explicit_id: str = "", ) -> str: """Build a role-instance member session id. One per ``(project, session_scope, [team_instance], role, employee)``. Previously this was keyed by seat; that is gone in the role-instance model. Same role → same session, across upward/downward work. ``team_instance_id`` is optional — included when multiple concurrent instances of the same role exist in a single run. For standard single-branch company mode it's left blank. """ explicit = str(explicit_id or "").strip() if explicit: return explicit project = str(project_id or "default").strip() or "default" scope = str(session_scope_id or "").strip() role = str(role_id or "unknown").strip() or "unknown" employee = str(employee_id or "default").strip() or "default" team_instance = str(team_instance_id or "").strip() scoped_prefix = f"{project}::{scope}" if scope else project parts: list[str] = [scoped_prefix] if team_instance: parts.append(team_instance) parts.append(role) parts.append(employee) return "role-session::" + "::".join(parts) def scoped_queue_key( *, session_scope_id: str, role_id: str = "", team_instance_id: str = "", seat_id: str = "", # deprecated, ignored — kept for arg compat while callers migrate ) -> str: """Build the per-role dispatch queue key. Role-scoped (not seat-scoped). ``team_instance_id`` is only appended when present (future-proof for multi-branch). ``seat_id`` is ignored. """ role = str(role_id or "").strip() scope = str(session_scope_id or "").strip() team_instance = str(team_instance_id or "").strip() if not role: return "" parts: list[str] = [] if scope: parts.append(scope) if team_instance: parts.append(team_instance) parts.append(role) return "::".join(parts) def role_home_team_instance_id( role_id: str, seats: list[dict] | None, ) -> str: """Return the ``team_instance_id`` where ``role_id`` is the leader. Convention: the leader seat of a role has ``team_id == f"team::{role_id}"``. For leaf roles (no own team), returns the team_instance of any seat that lists this role. Returns empty string if no seat matches. """ role = str(role_id or "").strip() if not role: return "" seat_list = [dict(seat) for seat in (seats or []) if isinstance(seat, dict)] # Prefer the leader seat (role's own team). for seat in seat_list: if str(seat.get("role_id", "") or "").strip() != role: continue if str(seat.get("team_id", "") or "").strip() == f"team::{role}": ti = str(seat.get("team_instance_id", "") or "").strip() if ti: return ti # Fallback: first seat listing this role. for seat in seat_list: if str(seat.get("role_id", "") or "").strip() != role: continue ti = str(seat.get("team_instance_id", "") or "").strip() if ti: return ti return "" def is_top_level_company_session(task: Task) -> bool: """Return whether the task belongs to a top-level actor-runtime session.""" metadata = dict(getattr(task, "metadata", {}) or {}) if not is_work_item_runtime_metadata(metadata): return False session_id = str( getattr(task, "session_id", "") or metadata.get("session_id", "") or "" ).strip() parent_session_id = str( getattr(task, "parent_session_id", "") or metadata.get("parent_session_id", "") or "" ).strip() return bool(session_id and parent_session_id and session_id == parent_session_id) def external_resume_allowed_for_scope(task: Task, *, resume_scope_id: str = "") -> bool: """Only allow external session continuation when the scope matches.""" metadata = dict(getattr(task, "metadata", {}) or {}) if bool(metadata.get("allow_external_resume_on_top_level_session", False)): return True current_scope = task_session_scope_id(task) if not current_scope: return True resume_scope = str(resume_scope_id or "").strip() if resume_scope: return resume_scope == current_scope return not is_top_level_company_session(task)