Files
OpenOPC/opc/layer2_organization/company_runtime.py
T
小东 cb6b8ed6e1 feat: harden company resume, external sessions, and Office UI i18n
Improve suspend/resume identity continuity and external broker session handling, add Office UI internationalization with rebuilt assets, and expand company org configs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 09:20:40 +08:00

2907 lines
146 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Persistent company-member runtime for company runtime execution."""
from __future__ import annotations
from contextvars import ContextVar, Token
from collections import defaultdict, deque
from dataclasses import asdict, dataclass, field
from datetime import datetime
from typing import Any, Awaitable, Callable
from loguru import logger
from opc.core.models import (
CompanyMemberSession,
CommsSemanticType,
DelegationRoleSession,
Phase,
ResidentAssignmentEnvelope,
Task,
TaskResult,
TaskStatus,
normalize_role_runtime_status,
)
from opc.core.worker_envelope import classify_worker_message
from opc.layer2_organization import comms as _comms
from opc.layer2_organization.collaboration_policy import render_ownership_contract
from opc.layer2_organization.phase import (
IN_REVIEW_PHASES,
is_dispatchable,
is_report_execution_work_item_metadata,
is_review_execution_work_item_metadata,
)
from opc.layer2_organization.metadata_ownership import sync_work_item_current_turn_mode
from opc.layer2_organization.session_scoping import (
external_resume_allowed_for_scope,
role_home_team_instance_id,
scoped_member_session_id,
scoped_queue_key,
task_session_scope_id,
)
from opc.layer2_organization.work_item_runtime import (
is_work_item_runtime_metadata,
mark_work_item_runtime,
)
from opc.layer2_organization.work_item_identity import (
projection_id_for_task,
turn_type_for_task,
work_item_identity_payload,
work_item_identity_payload_for_task,
work_item_projection_id_from_metadata,
work_item_turn_type_from_metadata,
)
from opc.layer2_organization.work_item_links import (
linked_work_item_id_for_task,
set_linked_work_item_id,
task_by_linked_work_item_id,
)
SaveRuntimeSessionFn = Callable[..., Awaitable[None]]
EmitRuntimeEventFn = Callable[[str, dict[str, Any]], Awaitable[None]]
@dataclass
class CompanyRuntimeState:
"""Mutable scheduler state for one top-level company run."""
member_sessions: dict[str, CompanyMemberSession] = field(default_factory=dict)
role_sessions: dict[str, DelegationRoleSession] = field(default_factory=dict)
role_queues: dict[str, deque[str]] = field(default_factory=lambda: defaultdict(deque))
queued_task_ids: set[str] = field(default_factory=set)
claimed_task_ids: set[str] = field(default_factory=set)
queued_work_item_ids: set[str] = field(default_factory=set)
claimed_work_item_ids: set[str] = field(default_factory=set)
home_team_instance_by_role: dict[str, str] = field(default_factory=dict)
# ── Canonical role_runtime_session_id generator ──────────────────────────
#
# Fix 5 PR1 — dropped ``team_instance_id`` from the session-identity key.
# The user's design is "同一角色 → 同一 session"a role has exactly one
# runtime session per run, shared across every team context it appears
# in. Work items still carry their team_instance_id (org placement is a
# per-work-item property), but the session that *executes* the work is
# role-scoped. This supports the serial-queue semantics landing in PR3
# (new work for a busy role goes to ``pending_work_item_ids`` instead
# of spawning a second claim on the same role).
#
# History: Fix 2 used the format ``role-runtime::{run}::{team|_no_team}::
# {role}`` to collapse the three divergent generators that produced 23
# rows per role in new16/app12. That fixed the duplicates within a team,
# but parallel branches of the same role across teams still split. PR1
# completes the collapse by keying on role alone.
#
# ``_NO_TEAM_SENTINEL`` and the 4-segment form are retained as strings
# so the DB migration in ``_migrate_role_sessions_merge_by_role`` can
# recognize legacy rows and fold them into the canonical 3-segment ID.
_NO_TEAM_SENTINEL = "_no_team"
_ROLE_RUNTIME_PREFIX = "role-runtime::"
def canonical_role_session_id(
*,
run_id: str,
role_id: str,
team_instance_id: str = "",
) -> str:
"""Single source of truth for role_runtime_session_id.
Format: ``role-runtime::{run_id}::{role_id}``
``team_instance_id`` is accepted for API backwards-compat with the
Fix-2-era callers but is *ignored*. Team context lives on the work
item, not on the session (see module docstring). Raises ``ValueError``
on missing ``run_id`` or ``role_id``.
"""
rid = str(run_id or "").strip()
if not rid:
raise ValueError("canonical_role_session_id: run_id is required")
role = str(role_id or "").strip()
if not role:
raise ValueError("canonical_role_session_id: role_id is required")
_ = team_instance_id # intentionally ignored; see module docstring
return f"{_ROLE_RUNTIME_PREFIX}{rid}::{role}"
def parse_role_session_id(role_session_id: str) -> tuple[str, str] | None:
"""Parse a canonical role_session_id back into ``(run_id, role_id)``.
Returns ``None`` for legacy/non-canonical forms (4-segment Fix-2
format, seat-embedded, ephemeral) so callers can detect them and
route through the migration path instead of trusting parsed bits.
Canonical form always has exactly 2 segments after the
``role-runtime::`` prefix.
"""
if not role_session_id:
return None
text = str(role_session_id).strip()
if not text.startswith(_ROLE_RUNTIME_PREFIX):
return None
tail = text[len(_ROLE_RUNTIME_PREFIX):]
parts = tail.split("::")
if len(parts) != 2:
return None
run_id, role_id = parts
if not run_id or not role_id:
return None
return run_id, role_id
class CompanyRuntime:
"""Keeps long-lived company-member session state across work-item turns."""
@staticmethod
def _set_member_session_status(
session: CompanyMemberSession,
status: str,
*,
focused_work_item_id: str | None = None,
) -> str:
if focused_work_item_id is not None:
session.focused_work_item_id = str(focused_work_item_id or "").strip()
normalized = normalize_role_runtime_status(
status,
session.focused_work_item_id,
)
if normalized == "idle":
session.focused_work_item_id = ""
session.status = normalized
session.resident_status = normalized
return normalized
@staticmethod
def _normalize_member_session_status(session: CompanyMemberSession) -> str:
return CompanyRuntime._set_member_session_status(
session,
session.status,
)
def __init__(
self,
*,
org_engine: Any | None,
communication: Any | None,
store: Any | None = None,
save_runtime_session: SaveRuntimeSessionFn | None = None,
emit_runtime_event: EmitRuntimeEventFn | None = None,
) -> None:
self.org_engine = org_engine
self.communication = communication
self.store = store
self.save_runtime_session = save_runtime_session
self.emit_runtime_event = emit_runtime_event
self._default_state = CompanyRuntimeState()
self._state_var: ContextVar[CompanyRuntimeState | None] = ContextVar(
f"company-runtime-state:{id(self)}",
default=None,
)
self.member_sessions = {}
self.role_sessions = {}
self.role_queues = defaultdict(deque)
self._queued_task_ids = set()
self._claimed_task_ids = set()
self._queued_work_item_ids = set()
self._claimed_work_item_ids = set()
# Phase A role-instance cache: role_id → home team_instance_id.
# The home team is where this role is the leader (seat.team_id
# matches f"team::{role_id}"); for leaf roles it's the only
# team they sit in. Included in the session / queue key so
# two parallel team_instances with the same role name
# (multi-branch) don't collide. Populated in bootstrap.
self._home_team_instance_by_role = {}
def create_state(self) -> CompanyRuntimeState:
return CompanyRuntimeState()
def use_state(self, state: CompanyRuntimeState) -> Token[CompanyRuntimeState | None]:
return self._state_var.set(state)
def reset_state(self, token: Token[CompanyRuntimeState | None]) -> None:
self._state_var.reset(token)
def _state(self) -> CompanyRuntimeState:
return self._state_var.get() or self._default_state
@property
def member_sessions(self) -> dict[str, CompanyMemberSession]:
return self._state().member_sessions
@member_sessions.setter
def member_sessions(self, value: dict[str, CompanyMemberSession]) -> None:
self._state().member_sessions = value
@property
def role_sessions(self) -> dict[str, DelegationRoleSession]:
return self._state().role_sessions
@role_sessions.setter
def role_sessions(self, value: dict[str, DelegationRoleSession]) -> None:
self._state().role_sessions = value
@property
def role_queues(self) -> dict[str, deque[str]]:
return self._state().role_queues
@role_queues.setter
def role_queues(self, value: dict[str, deque[str]]) -> None:
if isinstance(value, defaultdict):
self._state().role_queues = value
else:
self._state().role_queues = defaultdict(deque, value)
@property
def _queued_task_ids(self) -> set[str]:
return self._state().queued_task_ids
@_queued_task_ids.setter
def _queued_task_ids(self, value: set[str]) -> None:
self._state().queued_task_ids = value
@property
def _claimed_task_ids(self) -> set[str]:
return self._state().claimed_task_ids
@_claimed_task_ids.setter
def _claimed_task_ids(self, value: set[str]) -> None:
self._state().claimed_task_ids = value
@property
def _queued_work_item_ids(self) -> set[str]:
return self._state().queued_work_item_ids
@_queued_work_item_ids.setter
def _queued_work_item_ids(self, value: set[str]) -> None:
self._state().queued_work_item_ids = value
@property
def _claimed_work_item_ids(self) -> set[str]:
return self._state().claimed_work_item_ids
@_claimed_work_item_ids.setter
def _claimed_work_item_ids(self, value: set[str]) -> None:
self._state().claimed_work_item_ids = value
@property
def _home_team_instance_by_role(self) -> dict[str, str]:
return self._state().home_team_instance_by_role
@_home_team_instance_by_role.setter
def _home_team_instance_by_role(self, value: dict[str, str]) -> None:
self._state().home_team_instance_by_role = value
@staticmethod
def _direct_report_metadata_for_seat(
seat: dict[str, Any],
seats: list[dict[str, Any]],
) -> tuple[list[str], list[str]]:
role_id = str(seat.get("role_id", "") or "").strip()
seat_metadata = dict(seat.get("metadata", {}) or {})
managed_team_id = str(
seat.get("managed_team_id", "")
or seat_metadata.get("managed_team_id", "")
or ""
).strip()
if not role_id or not managed_team_id:
return [], []
report_roles: list[str] = []
report_seats: list[str] = []
for candidate in seats:
candidate_metadata = dict(candidate.get("metadata", {}) or {})
candidate_team_id = str(
candidate.get("team_id", "")
or candidate_metadata.get("team_id", "")
or ""
).strip()
if candidate_team_id != managed_team_id:
continue
candidate_manager_role_id = str(
candidate.get("manager_role_id", "")
or candidate_metadata.get("manager_role_id", "")
or ""
).strip()
if candidate_manager_role_id != role_id:
continue
candidate_role_id = str(candidate.get("role_id", "") or "").strip()
candidate_seat_id = str(candidate.get("seat_id", "") or "").strip()
if not candidate_role_id or not candidate_seat_id or candidate_role_id == role_id:
continue
report_roles.append(candidate_role_id)
report_seats.append(candidate_seat_id)
return sorted(dict.fromkeys(report_roles)), sorted(dict.fromkeys(report_seats))
@staticmethod
def _pending_reviews_from_board_summary(
session: CompanyMemberSession,
) -> list[dict[str, Any]]:
"""Return the list of child work items currently awaiting this manager's review.
The caller is expected to have already populated ``manager_board_summary``
(via ``_refresh_manager_board_state``). We filter the summary's
``upstream_summary`` for items in ``in_review`` whose ``review_owner_*``
matches this manager session.
"""
board_summary = dict((session.metadata or {}).get("manager_board_summary", {}) or {})
upstream = list(board_summary.get("upstream_summary", []) or [])
if not upstream:
return []
manager_role_id = str(session.role_id or "").strip().lower()
manager_seat_id = str(session.seat_id or (session.metadata or {}).get("seat_id", "") or "").strip().lower()
pending: list[dict[str, Any]] = []
for item in upstream:
if not isinstance(item, dict):
continue
phase = str(item.get("phase") or "").strip().lower()
kanban_column_value = str(item.get("kanban_column") or "").strip().lower()
if phase in {"approved", "failed", "cancelled"}:
continue
if (
kanban_column_value != "in_review"
and phase not in {"awaiting_manager_review", "awaiting_human"}
):
continue
owner_role = str(item.get("review_owner_role_id") or "").strip().lower()
owner_seat = str(item.get("review_owner_seat_id") or "").strip().lower()
if owner_role and manager_role_id and owner_role != manager_role_id:
# Reviewer is explicitly someone else (e.g. human gate).
continue
if owner_seat and manager_seat_id and owner_seat != manager_seat_id:
continue
pending.append(dict(item))
return pending
def _resolve_current_turn_mode(
self,
session: CompanyMemberSession,
task: Task | None = None,
) -> str:
task_metadata = dict(getattr(task, "metadata", {}) or {}) if task is not None else {}
task_context = dict(getattr(task, "context_snapshot", {}) or {}) if task is not None else {}
runtime_model = str(
task_metadata.get("runtime_model", "") if task is not None else ""
or (session.metadata or {}).get("runtime_model", "")
or ""
).strip()
if runtime_model != "multi_team_org":
return ""
explicit_task_turn_mode = str(
task_metadata.get("current_turn_mode")
or task_context.get("current_turn_mode")
or ""
).strip()
if (
bool(task_metadata.get("followup_routed_to_final_decider", False))
and explicit_task_turn_mode == "dispatch_required"
):
return "dispatch_required"
work_item_turn_type = (
turn_type_for_task(task, fallback="")
if task is not None
else (
work_item_turn_type_from_metadata(dict(session.current_assignment or {}), fallback="")
or work_item_turn_type_from_metadata(dict(session.current_work_item or {}), fallback="")
or work_item_turn_type_from_metadata(session.metadata or {}, fallback="")
)
)
direct_report_seat_ids = [
str(item).strip()
for item in list((session.metadata or {}).get("direct_report_seat_ids", []) or [])
if str(item).strip()
]
allowed_delegate_role_ids = [
str(item).strip()
for item in list((session.metadata or {}).get("allowed_delegate_role_ids", []) or [])
if str(item).strip()
]
managed_team_id = str((session.metadata or {}).get("managed_team_id", "") or "").strip()
manager_board_summary = dict((session.metadata or {}).get("manager_board_summary", {}) or {})
total_children = int(manager_board_summary.get("total_children", 0) or 0)
is_attention_work_item = bool(task_metadata.get("attention_work_item", False))
has_review_target = bool(str(task_metadata.get("review_target_work_item_id", "") or "").strip())
review_execution_turn = bool(
(task_metadata.get("review_execution_work_item", False) if task is not None else False)
or ((task_metadata.get("review_task", False) if task is not None else False) and has_review_target)
or has_review_target
or (work_item_turn_type == "review" and not is_attention_work_item and has_review_target)
)
report_execution_turn = bool(
(task_metadata.get("report_execution_work_item", False) if task is not None else False)
or work_item_turn_type == "report"
)
if work_item_turn_type == "deliver":
return "deliver_required"
if work_item_turn_type == "aggregate":
return "synthesize_required"
if report_execution_turn:
return "report_required"
if review_execution_turn:
return "review_execute"
if direct_report_seat_ids or allowed_delegate_role_ids or managed_team_id:
# Pending manager review takes priority over any other managerial
# turn mode: a manager must clear their review queue before
# dispatching or monitoring further children.
if self._pending_reviews_from_board_summary(session):
return "review_pending"
return "monitor_children" if total_children > 0 else "dispatch_required"
return "worker_execute"
def _update_current_turn_mode(
self,
session: CompanyMemberSession,
task: Task | None = None,
) -> str:
current_turn_mode = self._resolve_current_turn_mode(session, task)
session.current_turn_mode = current_turn_mode
session.metadata = dict(session.metadata or {})
if current_turn_mode:
session.metadata["current_turn_mode"] = current_turn_mode
else:
session.metadata.pop("current_turn_mode", None)
if session.current_assignment:
session.current_assignment = dict(session.current_assignment)
assignment_metadata = dict(session.current_assignment.get("metadata", {}) or {})
if current_turn_mode:
assignment_metadata["current_turn_mode"] = current_turn_mode
else:
assignment_metadata.pop("current_turn_mode", None)
session.current_assignment["metadata"] = assignment_metadata
if session.current_work_item:
session.current_work_item = {
**dict(session.current_work_item or {}),
**({"current_turn_mode": current_turn_mode} if current_turn_mode else {}),
}
return current_turn_mode
async def _sync_current_turn_mode_to_work_item(
self,
task: Task | None,
current_turn_mode: str,
) -> None:
if task is None or not self.store:
return
wid = linked_work_item_id_for_task(task)
if not wid:
return
try:
await sync_work_item_current_turn_mode(self.store, wid, current_turn_mode)
except Exception:
logger.opt(exception=True).debug(
"WorkItem current_turn_mode sync failed task=%s work_item=%s",
getattr(task, "id", ""),
wid,
)
async def _refresh_manager_board_state(
self,
session: CompanyMemberSession,
task: Task | None = None,
) -> dict[str, Any]:
if not self.store:
return {}
summarize = getattr(self.store, "summarize_parent_status", None)
if not callable(summarize):
return {}
run_id = str(
(task.metadata or {}).get("delegation_run_id", "") if task is not None else ""
or (session.metadata or {}).get("delegation_run_id", "")
or ""
).strip()
manager_seat_id = str(session.seat_id or (session.metadata or {}).get("seat_id", "") or "").strip()
parent_work_item_id = str(
session.focused_work_item_id
or (linked_work_item_id_for_task(task) if task is not None else "")
or ""
).strip()
if not run_id or not manager_seat_id or not parent_work_item_id:
session.metadata = dict(session.metadata or {})
session.metadata.pop("manager_board_summary", None)
session.metadata.pop("parent_board_scope", None)
return {}
summary = await summarize(
run_id,
manager_seat_id=manager_seat_id,
parent_work_item_id=parent_work_item_id,
)
session.metadata = dict(session.metadata or {})
session.metadata["delegation_run_id"] = run_id
if summary.get("total_children", 0):
session.metadata["manager_board_summary"] = dict(summary)
session.metadata["parent_board_scope"] = f"{manager_seat_id}:{parent_work_item_id}"
else:
session.metadata.pop("manager_board_summary", None)
session.metadata.pop("parent_board_scope", None)
pending_reviews = self._pending_reviews_from_board_summary(session)
if pending_reviews:
session.metadata["pending_review_items"] = pending_reviews
else:
session.metadata.pop("pending_review_items", None)
return dict(summary)
async def bootstrap(self, tasks: list[Task]) -> None:
if any(is_work_item_runtime_metadata(getattr(task, "metadata", {}) or {}) for task in tasks):
await self._bootstrap_work_item_runtime_sessions(tasks)
await self.refresh_inbox_state(tasks)
return
await self._bootstrap_role_sessions(tasks)
for task in sorted(tasks, key=lambda item: item.created_at):
created = False
member_session_id = self._member_session_id(
task,
role_id=self._role_id(task),
employee_id=self._employee_id(task),
)
if member_session_id not in self.member_sessions:
created = True
session = self._ensure_member_session(task)
task.metadata = dict(task.metadata)
task.metadata["member_session_id"] = session.member_session_id
task.metadata["member_session_state"] = self._serialize_session(session)
if created:
await self._persist_session(session, task=task)
await self._emit(
"member_session_started",
{
"member_session_id": session.member_session_id,
"role_id": session.role_id,
"employee_id": session.employee_id,
"task_id": task.id,
},
)
await self.refresh_inbox_state(tasks)
async def _bootstrap_work_item_runtime_sessions(self, tasks: list[Task]) -> None:
runtime_tasks = [
task
for task in tasks
if is_work_item_runtime_metadata(getattr(task, "metadata", {}) or {})
]
if not runtime_tasks:
return
root_task = sorted(runtime_tasks, key=lambda item: item.created_at)[0]
run_id = str((root_task.metadata or {}).get("delegation_run_id", "") or "").strip()
runtime_topology = dict((root_task.metadata or {}).get("runtime_topology", {}) or {})
if self.store is not None and bool(getattr(self.store, "is_ready", False)) and run_id and hasattr(self.store, "get_delegation_run"):
run = await self.store.get_delegation_run(run_id)
if run is not None:
runtime_topology = dict((getattr(run, "metadata", {}) or {}).get("runtime_topology", {}) or runtime_topology)
await self._bootstrap_role_sessions(runtime_tasks)
seats: list[dict[str, Any]] = []
list_seat_states = getattr(self.store, "list_delegation_seat_states", None)
if callable(list_seat_states) and run_id:
try:
persisted = await list_seat_states(run_id)
except Exception:
persisted = []
for item in persisted:
seat_payload = getattr(item, "__dict__", None)
if isinstance(seat_payload, dict):
seat_data = dict(seat_payload)
seat_data["metadata"] = dict(seat_data.get("metadata", {}) or {})
seats.append(seat_data)
if not seats:
seats = [dict(item) for item in list(runtime_topology.get("seats", []) or []) if isinstance(item, dict)]
task_by_seat = {
self._seat_id(task): task
for task in runtime_tasks
if self._seat_id(task)
}
# Role-instance model (Phase A): seats are organizational anchors,
# not identity. Group seats by role so that e.g. CMO's "upward"
# seat in CEO's team and "downward" seat in CMO's own team share
# **one** CompanyMemberSession + DelegationRoleSession. The seat
# that is the role's leader (team_id == f"team::{role}") is
# preferred as the session's primary seat; other seats are
# recorded in role_session.seat_ids for org lookups.
seats_by_role: dict[str, list[dict[str, Any]]] = defaultdict(list)
for seat in seats:
role_id = str(seat.get("role_id", "") or "").strip()
seat_id = str(seat.get("seat_id", "") or "").strip()
if role_id and seat_id:
seats_by_role[role_id].append(seat)
for role_id, role_seats in seats_by_role.items():
# Primary seat = leader seat if present, else the first seat.
# Leader seat has team_id == f"team::{role_id}" by convention.
primary_seat = next(
(s for s in role_seats if str(s.get("team_id", "") or "").strip() == f"team::{role_id}"),
role_seats[0],
)
primary_seat_id = str(primary_seat.get("seat_id", "") or "").strip()
all_seat_ids = sorted({str(s.get("seat_id", "") or "").strip() for s in role_seats if s.get("seat_id")})
project_id = str(root_task.project_id or "default").strip() or "default"
seat_employee_id = (
str(primary_seat.get("employee_id", "") or "").strip()
or self._employee_id(root_task)
)
# Pick a representative task: prefer one whose seat is the
# primary seat; fall back to any task with this role. Only use
# a "this role's task" as the explicit_id source — falling back
# to root_task would leak the root's member_session_id across
# all roles and collapse them into one session.
representative_task = task_by_seat.get(primary_seat_id)
if representative_task is None:
representative_task = next(
(task for task in runtime_tasks if self._role_id(task) == role_id),
None,
)
scope_source_task = representative_task or root_task
session_scope_id = task_session_scope_id(scope_source_task)
# Fix 5 PR4: compute home_team_instance_id for diagnostics only
# (surfaced on session.team_instance_id + session.metadata for
# the UI and logs). The session / queue keys no longer include
# it — same role = one session across every team context.
home_team_instance_id = role_home_team_instance_id(
role_id, seats
) or str(primary_seat.get("team_instance_id", "") or "").strip()
if home_team_instance_id:
self._home_team_instance_by_role[role_id] = home_team_instance_id
explicit_member_session_id = (
str((representative_task.metadata or {}).get("member_session_id", "") or "").strip()
if representative_task is not None
else ""
)
member_session_id = scoped_member_session_id(
project_id=project_id,
session_scope_id=session_scope_id,
role_id=role_id,
employee_id=seat_employee_id,
explicit_id=explicit_member_session_id,
)
if run_id:
role_session_id = canonical_role_session_id(
run_id=run_id,
role_id=role_id,
team_instance_id=home_team_instance_id,
)
else:
# Pre-run ephemeral path (tests / in-memory scratch runs
# that never hit the DB). Distinct prefix so it never
# collides with canonical rows.
role_session_id = f"role-session::ephemeral::{member_session_id}"
direct_report_role_ids, direct_report_seat_ids = self._direct_report_metadata_for_seat(primary_seat, seats)
session = self.member_sessions.get(member_session_id)
if session is None:
session = CompanyMemberSession(
member_session_id=member_session_id,
role_id=role_id,
employee_id=seat_employee_id,
)
self.member_sessions[member_session_id] = session
session.team_instance_id = home_team_instance_id
session.team_id = str(primary_seat.get("team_id", "") or "").strip()
session.seat_id = primary_seat_id
session.seat_state_id = str(primary_seat.get("seat_state_id", "") or "").strip()
# Collect merged metadata across all of this role's seats.
all_contact_role_ids: list[str] = []
all_allowed_delegate_role_ids: list[str] = []
all_managed_team_ids: list[str] = []
all_manager_seat_ids: list[str] = []
for s in role_seats:
all_contact_role_ids.extend(list(s.get("contact_role_ids", []) or []))
all_allowed_delegate_role_ids.extend(list(s.get("allowed_delegate_role_ids", []) or []))
mt = str(s.get("managed_team_id", "") or "").strip()
if mt:
all_managed_team_ids.append(mt)
ms = str(s.get("manager_seat_id", "") or "").strip()
if ms:
all_manager_seat_ids.append(ms)
session.metadata = mark_work_item_runtime({
**dict(session.metadata or {}),
"seat_id": primary_seat_id,
"seat_ids": all_seat_ids,
"team_id": session.team_id,
"team_instance_id": home_team_instance_id,
"manager_seat_id": str(primary_seat.get("manager_seat_id", "") or "").strip(),
"managed_team_id": str(primary_seat.get("managed_team_id", "") or "").strip(),
"managed_team_ids": sorted(set(all_managed_team_ids)),
"contact_role_ids": sorted(set(all_contact_role_ids)),
"allowed_delegate_role_ids": sorted(set(all_allowed_delegate_role_ids)),
"direct_report_role_ids": direct_report_role_ids,
"direct_report_seat_ids": direct_report_seat_ids,
"session_scope_id": session_scope_id,
**dict(primary_seat.get("metadata", {}) or {}),
})
session.manager_role_id = str(primary_seat.get("manager_role_id", "") or "").strip()
session.manager_role_ids = sorted(
{
*list(session.manager_role_ids or []),
*[
str(item).strip()
for item in [session.manager_role_id, *all_contact_role_ids]
if str(item).strip()
],
}
)
role_session = self.role_sessions.get(role_session_id)
if role_session is None:
role_session = DelegationRoleSession(
role_session_id=role_session_id,
run_id=run_id,
project_id=project_id,
team_instance_id=home_team_instance_id,
team_id=session.team_id,
role_id=role_id,
seat_id=primary_seat_id,
seat_state_id=str(primary_seat.get("seat_state_id", "") or "").strip(),
employee_id=session.employee_id,
manager_role_ids=list(session.manager_role_ids),
manager_seat_ids=sorted(set(all_manager_seat_ids)),
seat_ids=all_seat_ids,
status="idle",
metadata=mark_work_item_runtime({
"shared_role_executor": True,
"session_scope_id": session_scope_id,
}),
)
self.role_sessions[role_session_id] = role_session
if self.store and hasattr(self.store, "save_delegation_role_session"):
await self.store.save_delegation_role_session(role_session)
else:
role_session.project_id = getattr(role_session, "project_id", "") or project_id
role_session.team_instance_id = getattr(role_session, "team_instance_id", "") or home_team_instance_id
role_session.team_id = getattr(role_session, "team_id", "") or session.team_id
role_session.seat_id = getattr(role_session, "seat_id", "") or primary_seat_id
role_session.seat_state_id = getattr(role_session, "seat_state_id", "") or str(primary_seat.get("seat_state_id", "") or "").strip()
role_session.manager_seat_ids = sorted(
{
*list(getattr(role_session, "manager_seat_ids", []) or []),
*all_manager_seat_ids,
}
)
role_session.seat_ids = sorted(
{*list(getattr(role_session, "seat_ids", []) or []), *all_seat_ids}
)
role_session.metadata = {
**dict(getattr(role_session, "metadata", {}) or {}),
"session_scope_id": session_scope_id,
}
session.role_session_id = role_session.role_session_id
self._sync_member_session_from_role_session(session, role_session)
# Attach identity to every task for this role (regardless of which seat it sits in).
for seat_entry in role_seats:
entry_seat_id = str(seat_entry.get("seat_id", "") or "").strip()
seat_task = task_by_seat.get(entry_seat_id)
if seat_task is None:
continue
seat_task.metadata = dict(seat_task.metadata)
seat_task.metadata["member_session_id"] = session.member_session_id
seat_task.metadata["member_session_state"] = self._serialize_session(session)
seat_task.metadata["delegation_role_session_id"] = role_session.role_session_id
async def _bootstrap_role_sessions(self, tasks: list[Task]) -> None:
if self.store is not None and not bool(getattr(self.store, "is_ready", False)):
return
run_ids = {
str((task.metadata or {}).get("delegation_run_id", "") or "").strip()
for task in tasks
if str((task.metadata or {}).get("delegation_run_id", "") or "").strip()
}
if not run_ids:
return
for run_id in sorted(run_ids):
existing_sessions: list[DelegationRoleSession] = []
if self.store and hasattr(self.store, "list_delegation_role_sessions"):
existing_sessions = await self.store.list_delegation_role_sessions(run_id)
task_by_role: dict[str, Task] = {}
for task in tasks:
if str((task.metadata or {}).get("delegation_run_id", "") or "").strip() != run_id:
continue
role_id = self._role_id(task)
if role_id and role_id not in task_by_role:
task_by_role[role_id] = task
for session in existing_sessions:
self.role_sessions[session.role_session_id] = session
role_task = task_by_role.get(session.role_id)
if role_task is not None:
self._attach_role_session_to_task(role_task, session)
for role_id, task in task_by_role.items():
role_session_id = self._role_session_id(task, role_id=role_id)
if role_session_id in self.role_sessions:
continue
manager_role_ids = self._manager_role_ids(task)
employee_id = self._employee_id(task)
role_session = DelegationRoleSession(
role_session_id=role_session_id,
run_id=run_id,
role_id=role_id,
employee_id=employee_id,
manager_role_ids=manager_role_ids,
status="idle",
)
self.role_sessions[role_session_id] = role_session
if self.store and hasattr(self.store, "save_delegation_role_session"):
await self.store.save_delegation_role_session(role_session)
self._attach_role_session_to_task(task, role_session)
def _attach_role_session_to_task(self, task: Task, role_session: DelegationRoleSession) -> None:
task.metadata = dict(task.metadata)
task.metadata["delegation_role_session_id"] = role_session.role_session_id
def _sync_member_session_from_role_session(
self,
session: CompanyMemberSession,
role_session: DelegationRoleSession | None,
) -> None:
if role_session is None:
return
session.role_session_id = role_session.role_session_id
session.focused_work_item_id = str(getattr(role_session, "focused_work_item_id", "") or "").strip()
role_status = normalize_role_runtime_status(
getattr(role_session, "status", ""),
session.focused_work_item_id,
)
if role_status == "idle":
session.focused_work_item_id = ""
session.status = role_status
session.resident_status = role_status
session.background_work_item_ids = list(getattr(role_session, "background_work_item_ids", []) or [])
session.manager_role_ids = list(getattr(role_session, "manager_role_ids", []) or session.manager_role_ids or [])
session.adapter_session_state = dict(getattr(role_session, "adapter_session_state", {}) or {})
session.inbox_state = dict(getattr(role_session, "inbox_state", {}) or {})
session.memory_slices_by_work_item = dict(getattr(role_session, "memory_slices_by_work_item", {}) or {})
session.resume_state = {
**dict(getattr(role_session, "resume_state", {}) or {}),
**dict(session.resume_state or {}),
}
session.current_work_item = (
{}
if role_status == "idle"
else dict(getattr(role_session, "current_work_item", {}) or {})
)
session.latest_notification = dict(getattr(role_session, "latest_notification", {}) or {})
session.manager_digest = dict(getattr(role_session, "manager_digest", {}) or {})
async def reset_for_company_runtime_resume(
self,
tasks: list[Task],
*,
payload: dict[str, Any] | None = None,
) -> None:
"""Clear stale in-memory claims/sessions before replaying a suspended run.
Stop kills the live coroutine/subprocess. The DB claim is released by
the engine, but the long-lived Office UI process may still have member
sessions marked ``running`` from the cancelled turn. If those remain,
``claim_runnable_tasks`` correctly refuses to dispatch new work. Resume
therefore has to converge memory back to DB truth before the executor's
first dispatch tick.
"""
payload = dict(payload or {})
affected_task_ids = {
str(getattr(task, "id", "") or "").strip()
for task in tasks
if str(getattr(task, "id", "") or "").strip()
}
affected_work_item_ids = {
linked_work_item_id_for_task(task)
for task in tasks
if linked_work_item_id_for_task(task)
}
affected_role_session_ids = {
str((getattr(task, "metadata", {}) or {}).get("delegation_role_session_id", "") or "").strip()
for task in tasks
if str((getattr(task, "metadata", {}) or {}).get("delegation_role_session_id", "") or "").strip()
}
affected_role_session_ids.update(
str(item).strip()
for item in list(payload.get("role_runtime_session_ids", []) or [])
if str(item).strip()
)
if not affected_task_ids and not affected_work_item_ids and not affected_role_session_ids:
return
self._claimed_task_ids.difference_update(affected_task_ids)
self._queued_task_ids.difference_update(affected_task_ids)
self._claimed_work_item_ids.difference_update(affected_work_item_ids)
self._queued_work_item_ids.difference_update(affected_work_item_ids)
blocked_queue_entries = {
*affected_task_ids,
*(f"review-task::{task_id}" for task_id in affected_task_ids),
*(f"work-item::{work_item_id}" for work_item_id in affected_work_item_ids),
*(f"review-work-item::{work_item_id}" for work_item_id in affected_work_item_ids),
}
for queue_key, queue in list(self.role_queues.items()):
if not queue:
continue
self.role_queues[queue_key] = deque(
entry for entry in queue if str(entry or "").strip() not in blocked_queue_entries
)
now = datetime.now()
update_role_session = getattr(self.store, "update_delegation_role_session", None) if self.store else None
for session in list(self.member_sessions.values()):
role_session_id = str(getattr(session, "role_session_id", "") or "").strip()
focused_work_item_id = str(getattr(session, "focused_work_item_id", "") or "").strip()
current_task_id = str(getattr(session, "current_task_id", "") or "").strip()
affected = (
role_session_id in affected_role_session_ids
or focused_work_item_id in affected_work_item_ids
or current_task_id in affected_task_ids
)
if not affected:
continue
session.status = "idle"
session.resident_status = "idle"
session.current_task_id = ""
session.focused_work_item_id = ""
session.current_work_item = {}
session.current_assignment = {}
session.updated_at = now
role_session = self.role_sessions.get(role_session_id)
if role_session is not None:
role_session.status = "idle"
role_session.focused_work_item_id = ""
role_session.current_work_item = {}
role_session.updated_at = now
if callable(update_role_session) and role_session_id:
try:
await update_role_session(
role_session_id,
focused_work_item_id="",
current_work_item={},
status="idle",
metadata_updates={
"last_resume_memory_reset_at": now.isoformat(),
"last_resume_checkpoint_id": str(payload.get("checkpoint_id", "") or ""),
},
)
except Exception:
logger.opt(exception=True).debug("company runtime resume reset: role session persist failed")
def _role_session_id(self, task: Task, *, role_id: str) -> str:
explicit = str((task.metadata or {}).get("delegation_role_session_id", "") or "").strip()
if explicit:
return explicit
run_id = str((task.metadata or {}).get("delegation_run_id", "") or "").strip()
if run_id and role_id:
# Fix 5 PR1: same role = one session per run; team_instance_id
# is not part of the session key anymore. The multi-source
# team resolution fallback that used to live here is obsolete.
return canonical_role_session_id(run_id=run_id, role_id=role_id)
return f"role-session::ephemeral::{self._member_session_id(task, role_id=role_id, employee_id=self._employee_id(task))}"
def _manager_role_ids(self, task: Task) -> list[str]:
explicit = [
str(item).strip()
for item in list((task.metadata or {}).get("manager_role_ids", []) or [])
if str(item).strip()
]
if explicit:
return sorted(dict.fromkeys(explicit))
direct_manager = str((task.metadata or {}).get("manager_role_id", "") or "").strip()
if direct_manager:
return [direct_manager]
if self.org_engine is not None:
agent = self.org_engine.get_agent(self._role_id(task))
manager_role = str(getattr(agent, "reports_to", "") or "").strip()
if manager_role and manager_role != "owner":
return [manager_role]
return []
async def refresh_inbox_state(self, tasks: list[Task]) -> None:
if not self.communication or not hasattr(self.communication, "read_inbox"):
return
task_scope_ids = self._task_scope_ids(tasks)
representative_task_by_key: dict[str, Task] = {}
for task in sorted(tasks, key=lambda item: item.created_at, reverse=True):
key = self._queue_key_for_task(task)
if key and key not in representative_task_by_key:
representative_task_by_key[key] = task
for session in self.member_sessions.values():
self._normalize_member_session_status(session)
representative_task = representative_task_by_key.get(self._queue_key_for_session(session))
if representative_task is None:
representative_task = representative_task_by_key.get(session.role_id)
messages = await self.communication.read_inbox(
agent_id=session.role_id,
task=representative_task,
task_ids=task_scope_ids,
unread_only=True,
limit=12,
mark_read=False,
)
classified = [classify_worker_message(dict(item)) for item in messages if isinstance(item, dict)]
sorted_messages = sorted(classified, key=lambda item: self._message_sort_key(session, item))
actionable_chat = [dict(item) for item in sorted_messages if item.get("message_class") == "chat" and bool(item.get("actionable", True))]
protocol_backlog = [dict(item) for item in sorted_messages if item.get("message_class") == "protocol"]
notification_backlog = [dict(item) for item in sorted_messages if item.get("message_class") == "notification"]
session.actionable_chat = actionable_chat[:8]
session.protocol_backlog = protocol_backlog[:8]
session.notification_backlog = notification_backlog[:8]
session.actionable_inbox_count = len(actionable_chat)
session.protocol_backlog_count = len(protocol_backlog)
session.notification_backlog_count = len(notification_backlog)
session.latest_notification = self._latest_notification(notification_backlog)
inbox_initialized = bool((session.metadata or {}).get("inbox_initialized", False)) or session.inbox_cursor > 0
if not inbox_initialized:
session.pending_inbox = []
session.queued_inbox = [dict(item) for item in actionable_chat[:12]]
session.inbox_cursor = max(
session.inbox_cursor,
len(session.queued_inbox) + len(protocol_backlog) + len(notification_backlog),
)
session.metadata = dict(session.metadata or {})
session.metadata["inbox_initialized"] = True
else:
seen_ids = {
str(item).strip()
for item in list(session.resume_state.get("seen_inbox_message_ids", []))
if str(item).strip()
}
for item in classified:
msg_id = str(item.get("msg_id", "")).strip()
if msg_id:
seen_ids.add(msg_id)
session.pending_inbox = [dict(item) for item in actionable_chat[:8]]
session.resume_state = dict(session.resume_state)
session.resume_state["seen_inbox_message_ids"] = sorted(seen_ids)
session.inbox_cursor = len(seen_ids)
session.current_work_item = self._build_current_work_item(session, representative_task)
await self._refresh_manager_board_state(session, representative_task)
current_turn_mode = self._update_current_turn_mode(session, representative_task)
await self._sync_current_turn_mode_to_work_item(representative_task, current_turn_mode)
session.manager_digest = self._build_manager_digest(session, representative_task)
session.inbox_state = {
"actionable_chat": [dict(item) for item in session.actionable_chat],
"protocol_backlog": [dict(item) for item in session.protocol_backlog],
"notification_backlog": [dict(item) for item in session.notification_backlog],
"pending_inbox": [dict(item) for item in session.pending_inbox],
"queued_inbox": [dict(item) for item in session.queued_inbox],
"latest_notification": dict(session.latest_notification or {}),
"actionable_inbox_count": session.actionable_inbox_count,
"protocol_backlog_count": session.protocol_backlog_count,
"notification_backlog_count": session.notification_backlog_count,
"current_work_item": dict(session.current_work_item or {}),
"current_turn_mode": str(session.current_turn_mode or "").strip(),
"manager_board_summary": dict((session.metadata or {}).get("manager_board_summary", {}) or {}),
"manager_digest": dict(session.manager_digest or {}),
}
role_session = self._role_session_for_member_session(session)
if role_session is not None:
role_session.inbox_state = dict(session.inbox_state)
role_session.current_work_item = dict(session.current_work_item or {})
role_session.latest_notification = dict(session.latest_notification or {})
role_session.manager_digest = dict(session.manager_digest or {})
role_session.updated_at = datetime.now()
if self.store and bool(getattr(self.store, "is_ready", False)) and hasattr(self.store, "save_delegation_role_session"):
await self.store.save_delegation_role_session(role_session)
if representative_task is not None:
representative_task.metadata = dict(representative_task.metadata)
representative_task.metadata["current_turn_mode"] = str(session.current_turn_mode or "").strip()
representative_task.metadata["member_session_state"] = self._serialize_session(session)
representative_task.context_snapshot = dict(representative_task.context_snapshot)
representative_task.context_snapshot["current_turn_mode"] = str(session.current_turn_mode or "").strip()
representative_task.context_snapshot["member_session"] = self._serialize_session(session)
session.updated_at = datetime.now()
await self._persist_session(session, task=representative_task)
await self._emit(
"member_inbox_updated",
{
"member_session_id": session.member_session_id,
"role_id": session.role_id,
"employee_id": session.employee_id,
"pending_count": len(session.pending_inbox),
"actionable_inbox_count": session.actionable_inbox_count,
"protocol_backlog_count": session.protocol_backlog_count,
"notification_backlog_count": session.notification_backlog_count,
"resident_status": session.resident_status,
"latest_notification": dict(session.latest_notification or {}),
"message_priority": self._highest_message_priority(session),
"inbox_cursor": session.inbox_cursor,
},
)
def get_message_triggered_sessions(self) -> list[tuple["CompanyMemberSession", dict]]:
"""Return (session, message) pairs for idle agents that have actionable
messages needing a reply. The caller (CompanyWorkItemExecutor) can
create lightweight response tasks so the agent actually acts on the
message instead of letting it sit in the inbox.
"""
triggered: list[tuple["CompanyMemberSession", dict]] = []
for session in self.member_sessions.values():
if self._normalize_member_session_status(session) != "idle":
continue
for msg in list(session.inbox_state.get("actionable_chat", []) or []):
if not isinstance(msg, dict):
continue
if msg.get("reply_needed") or msg.get("urgency") in {"blocking", "high"}:
triggered.append((session, msg))
break # one trigger per session per cycle
return triggered
def enqueue_runnable_tasks(self, tasks: list[Task]) -> None:
"""Append PENDING plain tasks to their role queue (dedup-safe).
Phase B/#7 note: no longer uses ``_queued_task_ids`` as a
gate. The queue itself is scanned for an existing entry —
the DB (``task.status``) is the only source of truth for
runnability; the deque provides dedup. Claim-time race
safety still relies on ``_claimed_task_ids`` in
``claim_runnable_tasks`` (that set is actively maintained
through complete_claim, so removing it would need a different
race guard).
"""
for task in tasks:
if not self._is_runnable(task):
continue
queue_key = self._queue_key_for_task(task)
if not queue_key:
continue
queue = self.role_queues[queue_key]
# Dedup by scanning the deque instead of consulting an
# easily-drifted shadow set.
review_tag = f"review-task::{task.id}"
if task.id in queue or review_tag in queue:
continue
# Kanban-push review tasks take priority over regular work so a
# manager role always clears its review backlog before dispatching
# or executing its own work.
if bool((task.metadata or {}).get("review_task", False)):
queue.appendleft(review_tag)
else:
queue.append(task.id)
def enqueue_runnable_work_items(
self,
work_items: list[Any],
*,
task_by_work_item_id: dict[str, Task] | None = None,
) -> None:
"""Append dispatchable work items to their role queue.
Phase B/#7 note: no longer uses ``_queued_work_item_ids`` as a
gate. Gate is ``is_dispatchable`` (DB truth on phase + claim)
plus a queue-scan for existing entries.
"""
for work_item in work_items:
work_item_id = str(getattr(work_item, "work_item_id", "") or "").strip()
metadata = dict(getattr(work_item, "metadata", {}) or {})
session_scope_id = str(metadata.get("session_scope_id", "") or "").strip()
if not session_scope_id and task_by_work_item_id is not None:
task = task_by_work_item_id.get(work_item_id)
if task is not None:
session_scope_id = task_session_scope_id(task)
if session_scope_id:
work_item.metadata = {
**metadata,
"session_scope_id": session_scope_id,
}
metadata = dict(work_item.metadata or {})
role_id = str(
getattr(work_item, "role_id", "")
or metadata.get("role_id", "")
or ""
).strip()
# Fix 5 PR4: queue key is role-scoped only. Same role across
# every team context lands in one queue — aligned with the
# role-scoped canonical_role_session_id from PR1.
queue_key = scoped_queue_key(
session_scope_id=session_scope_id,
role_id=role_id,
)
if not work_item_id or not queue_key:
continue
if not is_dispatchable(work_item):
continue
queue = self.role_queues[queue_key]
work_tag = f"work-item::{work_item_id}"
review_tag = f"review-work-item::{work_item_id}"
if work_tag in queue or review_tag in queue:
continue
if is_review_execution_work_item_metadata(metadata):
queue.appendleft(review_tag)
else:
queue.append(work_tag)
async def claim_runnable_tasks(
self,
tasks: list[Task],
work_items: list[Any] | None = None,
) -> list[tuple[CompanyMemberSession, Task]]:
hydrate_links = getattr(self.store, "hydrate_task_work_item_links", None) if self.store is not None else None
if callable(hydrate_links):
try:
await hydrate_links(tasks)
except Exception:
logger.opt(exception=True).debug("claim_runnable_tasks: link hydration failed")
task_map = {task.id: task for task in tasks}
task_by_work_item_id = task_by_linked_work_item_id(tasks)
work_item_map = {
str(getattr(work_item, "work_item_id", "") or "").strip(): work_item
for work_item in list(work_items or [])
if str(getattr(work_item, "work_item_id", "") or "").strip()
}
claims: list[tuple[CompanyMemberSession, Task]] = []
sessions = sorted(self.member_sessions.values(), key=self._session_sort_key)
def _skip(reason: str, **ctx: Any) -> None:
"""Log a per-iteration skip with enough context to later
answer "why wasn't this role dispatched on tick T?"."""
detail = " ".join(f"{k}={v}" for k, v in ctx.items() if v is not None)
logger.debug(f"claim skip: {reason} {detail}")
for session in sessions:
session_status = self._normalize_member_session_status(session)
session_label = f"role={session.role_id} sid={session.member_session_id}"
queue = self.role_queues.get(self._queue_key_for_session(session))
# Kanban-push soft-wake: a manager seat that is `blocked` on its
# own AWAITING_PEER / AWAITING_* task must still be able to
# process review tasks that arrive in its queue (otherwise the
# review turn cannot run until something external unblocks the
# manager, which in the kanban-push model is itself the review).
can_soft_wake = (
session_status == "blocked"
and queue is not None
and any(
entry.startswith("review-task::") or entry.startswith("review-work-item::")
for entry in queue
)
)
if session_status == "running":
logger.trace(
"claim skip: session.status blocks claim session={} status={}",
session_label,
session_status,
)
continue
if session_status == "blocked" and not can_soft_wake:
_skip(
"session.status=blocked and no review-soft-wake entry in queue",
session=session_label,
)
continue
role_session = self._role_session_for_member_session(session)
role_session_status = ""
if role_session is not None:
role_session.status = normalize_role_runtime_status(
role_session.status,
role_session.focused_work_item_id,
)
if role_session.status == "idle":
role_session.focused_work_item_id = ""
role_session_status = role_session.status
if role_session_status == "running":
logger.trace(
"claim skip: role_session already running session={} role_session_id={}",
session_label,
role_session.role_session_id,
)
continue
if not queue:
# Empty queue is expected in steady state; log only at TRACE.
logger.trace(f"claim skip: empty queue session={session_label}")
continue
while queue:
queued_item_id = self._pop_next_queue_entry(queue)
work_item = None
task = None
if queued_item_id.startswith("review-task::"):
task_id = queued_item_id.split("::", 1)[1]
self._queued_task_ids.discard(task_id)
if task_id in self._claimed_task_ids:
_skip("review-task already claimed by this dispatcher",
session=session_label, task_id=task_id)
continue
task = task_map.get(task_id)
if task is None:
_skip("review-task not in tasks list this tick",
session=session_label, task_id=task_id)
continue
if not self._is_runnable(task):
_skip("review-task status not pending",
session=session_label, task_id=task_id,
status=getattr(task, "status", None))
continue
self._claimed_task_ids.add(task_id)
elif queued_item_id.startswith("review-work-item::") or queued_item_id.startswith("work-item::"):
work_item_id = queued_item_id.split("::", 1)[1]
self._queued_work_item_ids.discard(work_item_id)
if work_item_id in self._claimed_work_item_ids:
_skip("work_item already claimed by this dispatcher",
session=session_label, work_item_id=work_item_id)
continue
work_item = work_item_map.get(work_item_id)
if work_item is None:
_skip("work_item not in work_items list this tick",
session=session_label, work_item_id=work_item_id)
continue
metadata = dict(getattr(work_item, "metadata", {}) or {})
if not is_dispatchable(work_item):
queued_behind = str(
metadata.get("queued_behind_session", "") or ""
).strip()
reason = (
"valid queued behind role session"
if queued_behind
else "phase not runnable and not orphan"
)
_skip(f"is_dispatchable=False ({reason})",
session=session_label, work_item_id=work_item_id,
phase=getattr(getattr(work_item, "phase", None), "value", None),
claim=getattr(work_item, "claimed_by_role_runtime_session_id", ""),
queued_behind_session=queued_behind or None)
continue
if queued_item_id.startswith("review-work-item::"):
target_work_item_id = str(metadata.get("review_target_work_item_id", "") or "").strip()
target_work_item = work_item_map.get(target_work_item_id)
if target_work_item is None:
_skip("review-work-item's target not in work_items",
session=session_label, review_wid=work_item_id,
target_wid=target_work_item_id)
continue
if target_work_item.phase not in IN_REVIEW_PHASES:
_skip("review-work-item target no longer in review phase",
session=session_label, review_wid=work_item_id,
target_phase=getattr(target_work_item.phase, "value", ""))
continue
task = task_by_work_item_id.get(work_item_id)
if task is None:
get_runtime_task = getattr(self.store, "get_runtime_task_for_work_item", None) if self.store is not None else None
linked_task = None
if callable(get_runtime_task):
try:
linked_task = await get_runtime_task(work_item_id)
except Exception:
logger.opt(exception=True).debug("claim_runnable_tasks: linked runtime task lookup failed")
if linked_task is not None:
set_linked_work_item_id(linked_task, work_item_id)
task_map[linked_task.id] = linked_task
task_by_work_item_id[work_item_id] = linked_task
task = linked_task
else:
_skip("no task materialized for work_item this tick",
session=session_label, work_item_id=work_item_id)
continue
else:
task_id = queued_item_id
self._queued_task_ids.discard(task_id)
if task_id in self._claimed_task_ids:
_skip("plain task already claimed",
session=session_label, task_id=task_id)
continue
task = task_map.get(task_id)
if task is None:
_skip("plain task not in tasks list this tick",
session=session_label, task_id=task_id)
continue
if not self._is_runnable(task):
_skip("plain task status not pending",
session=session_label, task_id=task_id,
status=getattr(task, "status", None))
continue
self._claimed_task_ids.add(task_id)
if work_item is not None:
claimed = await self._claim_role_session_work_item(
session,
work_item,
task,
)
if not claimed:
fresh_work_item = None
get_work_item = getattr(
self.store,
"get_delegation_work_item",
None,
)
if callable(get_work_item):
fresh_work_item = await get_work_item(work_item_id)
if fresh_work_item is not None:
work_item_map[work_item_id] = fresh_work_item
_skip(
"atomic WorkItem claim lost to a phase/hold/owner update",
session=session_label,
work_item_id=work_item_id,
)
continue
self._claimed_work_item_ids.add(work_item_id)
if can_soft_wake and (
bool((task.metadata or {}).get("review_task", False))
or bool((task.metadata or {}).get("review_execution_work_item", False))
):
# Stash the prior focus so complete_claim can restore
# the `blocked` state when the review turn ends. The
# session only gets here if the original task was still
# awaiting something, so un-focusing it entirely would
# lose the peer_wait context.
session.metadata = dict(session.metadata or {})
session.metadata["_review_preempt_prev_task_id"] = str(session.current_task_id or "")
session.metadata["_review_preempt_prev_work_item_id"] = str(session.focused_work_item_id or "")
session.metadata["_review_preempt_active"] = True
session.current_task_id = task.id
session.focused_work_item_id = linked_work_item_id_for_task(task)
self._set_member_session_status(session, "running")
session.updated_at = datetime.now()
self.prepare_task_for_session(session, task)
await self._sync_current_turn_mode_to_work_item(task, session.current_turn_mode)
if work_item is not None:
# #7: mirror the claim onto the in-memory work_item so
# subsequent iterations in the same claim pass see it
# and is_dispatchable returns False (race safety after
# removing the _queued_* memory-set gate).
try:
role_session = self._role_session_for_member_session(session)
if role_session is not None:
work_item.claimed_by_role_runtime_session_id = role_session.role_session_id
except Exception:
logger.opt(exception=True).debug("post-claim in-memory mirror failed")
self._set_member_session_status(session, "running")
await self._persist_session(session)
await self._emit(
"member_claimed_work_item",
{
"member_session_id": session.member_session_id,
"role_id": session.role_id,
"employee_id": session.employee_id,
"seat_id": str((session.metadata or {}).get("seat_id", "") or "").strip(),
"team_id": str((session.metadata or {}).get("team_id", "") or "").strip(),
"task_id": task.id,
**work_item_identity_payload_for_task(task),
"message_priority": task.metadata.get("message_priority", "ready_queue"),
"work_item_id": linked_work_item_id_for_task(task),
"role_session_id": session.role_session_id,
},
)
claims.append((session, task))
break
return claims
def prepare_task_for_session(self, session: CompanyMemberSession, task: Task) -> None:
task.metadata = dict(task.metadata)
task.context_snapshot = dict(task.context_snapshot)
role_session = self._ensure_role_session(task)
manager_role = session.manager_role_id or str(session.resume_state.get("manager_role_id", "") or "").strip()
if not manager_role and self.org_engine is not None:
agent = self.org_engine.get_agent(session.role_id)
manager_role = str(getattr(agent, "reports_to", "") or "").strip()
session.manager_role_id = manager_role
session.manager_role_ids = sorted(dict.fromkeys([*session.manager_role_ids, *self._manager_role_ids(task)]))
if role_session is not None:
session.role_session_id = role_session.role_session_id
session.focused_work_item_id = role_session.focused_work_item_id or linked_work_item_id_for_task(task)
session.background_work_item_ids = list(role_session.background_work_item_ids or [])
session.memory_slices_by_work_item = dict(role_session.memory_slices_by_work_item or {})
session.adapter_session_state = dict(role_session.adapter_session_state or {})
session.inbox_state = dict(role_session.inbox_state or session.inbox_state)
session.manager_role_ids = list(role_session.manager_role_ids or session.manager_role_ids)
session.resume_state = {**dict(role_session.resume_state or {}), **dict(session.resume_state or {})}
session.current_work_item = dict(role_session.current_work_item or session.current_work_item)
session.latest_notification = dict(role_session.latest_notification or session.latest_notification)
session.manager_digest = dict(role_session.manager_digest or session.manager_digest)
session.team_instance_id = str(getattr(role_session, "team_instance_id", "") or session.team_instance_id or "").strip()
session.team_id = str(getattr(role_session, "team_id", "") or session.team_id or "").strip()
session.seat_id = str(getattr(role_session, "seat_id", "") or session.seat_id or "").strip()
session.seat_state_id = str(getattr(role_session, "seat_state_id", "") or session.seat_state_id or "").strip()
session.metadata = {
**dict(session.metadata or {}),
"session_scope_id": task_session_scope_id(task),
}
assignment = self._build_assignment_envelope(session, task)
session.current_assignment = dict(assignment)
session.current_work_item = self._build_current_work_item(session, task)
self._update_current_turn_mode(session, task)
session.manager_digest = self._build_manager_digest(session, task)
task.metadata["member_session_id"] = session.member_session_id
task.metadata["current_turn_mode"] = str(session.current_turn_mode or "").strip()
if session.role_session_id:
task.metadata["delegation_role_session_id"] = session.role_session_id
adapter_session_state = dict(session.adapter_session_state or {})
resume_scope_id = str(
adapter_session_state.get("external_resume_session_scope_id", "")
or adapter_session_state.get("session_scope_id", "")
or ""
).strip()
assigned_agent = str(task.assigned_external_agent or "").strip()
state_agent = str(
adapter_session_state.get("external_resume_agent_type")
or adapter_session_state.get("selected_execution_agent")
or ""
).strip()
if assigned_agent:
agent_entry = adapter_session_state.get(assigned_agent)
if isinstance(agent_entry, dict):
adapter_session_state = {**adapter_session_state, **dict(agent_entry)}
entry_token = str(
agent_entry.get("external_resume_session_id")
or agent_entry.get("resume_session_id")
or agent_entry.get("provider_session_id")
or ""
).strip()
if entry_token:
adapter_session_state["external_resume_session_id"] = entry_token
state_agent = assigned_agent
if not external_resume_allowed_for_scope(task, resume_scope_id=resume_scope_id):
adapter_session_state.pop("external_resume_session_id", None)
adapter_session_state.pop("external_resume_session_scope_id", None)
adapter_session_state.pop("external_resume_agent_type", None)
external_resume_session_id = str(adapter_session_state.get("external_resume_session_id", "") or "").strip()
if external_resume_session_id and assigned_agent and state_agent == assigned_agent:
task.metadata["external_resume_session_id"] = external_resume_session_id
task.metadata["external_resume_session_scope_id"] = (
resume_scope_id or task_session_scope_id(task)
)
task.metadata["external_resume_agent_type"] = assigned_agent
else:
task.metadata.pop("external_resume_session_id", None)
task.metadata.pop("external_resume_session_scope_id", None)
task.metadata.pop("external_resume_agent_type", None)
session_state_payload = self._serialize_session(session)
session_state_payload["adapter_session_state"] = dict(adapter_session_state)
task.metadata["member_session_state"] = session_state_payload
if adapter_session_state:
task.context_snapshot["seat_adapter_session_state"] = dict(adapter_session_state)
seat_id = str((session.metadata or {}).get("seat_id", "") or "").strip()
if seat_id:
# ``delegation_*`` fields are the WorkItem owner envelope and must
# not be overwritten by a shared resident role session. Keep the
# runtime/session seat as explicit runtime audit instead.
task.metadata["runtime_session_seat_id"] = seat_id
task.metadata["runtime_session_team_id"] = str(
(session.metadata or {}).get("team_id", "") or session.team_id or ""
).strip()
task.metadata["runtime_session_team_instance_id"] = str(
(session.metadata or {}).get("team_instance_id", "")
or session.team_instance_id
or ""
).strip()
task.metadata.setdefault("seat_contact_role_ids", list((session.metadata or {}).get("contact_role_ids", []) or []))
task.metadata.setdefault("allowed_delegate_role_ids", list((session.metadata or {}).get("allowed_delegate_role_ids", []) or []))
task.metadata["direct_report_role_ids"] = list((session.metadata or {}).get("direct_report_role_ids", []) or [])
task.metadata["direct_report_seat_ids"] = list((session.metadata or {}).get("direct_report_seat_ids", []) or [])
task.metadata.setdefault("managed_team_id", str((session.metadata or {}).get("managed_team_id", "") or "").strip())
task.metadata.setdefault("manager_seat_id", str((session.metadata or {}).get("manager_seat_id", "") or "").strip())
task.metadata["message_priority"] = self._highest_message_priority(session)
task.metadata["resident_assignment"] = dict(session.current_assignment or assignment)
task.context_snapshot["member_session"] = dict(session_state_payload)
task.context_snapshot["resident_assignment"] = dict(session.current_assignment or assignment)
task.context_snapshot["current_turn_mode"] = str(session.current_turn_mode or "").strip()
if role_session is not None:
task.context_snapshot["delegation_role_session"] = self._serialize_role_session(role_session)
if session.working_memory:
task.context_snapshot["member_working_memory"] = list(session.working_memory[-8:])
if session.resume_state:
task.context_snapshot["member_resume_state"] = dict(session.resume_state)
task.context_snapshot.setdefault("runtime_resume", dict(session.resume_state))
pending_inbox = list(session.pending_inbox or [])
if session.queued_inbox:
pending_inbox = [*pending_inbox, *list(session.queued_inbox)]
session.pending_inbox = pending_inbox[-8:]
session.actionable_chat = list(session.pending_inbox)
session.queued_inbox = []
queued_session_state = self._serialize_session(session)
queued_session_state["adapter_session_state"] = dict(adapter_session_state)
task.metadata["member_session_state"] = queued_session_state
if pending_inbox:
task.context_snapshot["company_member_inbox"] = list(pending_inbox[-8:])
if session.protocol_backlog:
task.context_snapshot["company_member_protocol_backlog"] = list(session.protocol_backlog[:6])
if session.notification_backlog:
task.context_snapshot["company_member_notification_backlog"] = list(session.notification_backlog[:6])
if session.latest_notification:
task.context_snapshot["latest_company_notification"] = dict(session.latest_notification)
if session.manager_digest:
task.context_snapshot["manager_digest"] = dict(session.manager_digest)
manager_board_summary = dict((session.metadata or {}).get("manager_board_summary", {}) or {})
if manager_board_summary:
task.context_snapshot["manager_board_summary"] = manager_board_summary
pending_review_items = list(
(session.metadata or {}).get("pending_review_items", []) or []
)
if pending_review_items:
task.context_snapshot["pending_review_items"] = [
dict(item) for item in pending_review_items if isinstance(item, dict)
]
task.metadata["pending_review_items"] = [
dict(item) for item in pending_review_items if isinstance(item, dict)
]
else:
task.context_snapshot.pop("pending_review_items", None)
task.metadata.pop("pending_review_items", None)
task.context_snapshot["resident_status"] = session.resident_status
async def complete_claim(
self,
session: CompanyMemberSession,
task: Task,
result: TaskResult | None = None,
) -> None:
multi_team_org = str((task.metadata or {}).get("runtime_model", "") or "").strip() == "multi_team_org"
synthetic_inbox_turn = bool((task.metadata or {}).get("synthetic_inbox_turn", False))
self._claimed_task_ids.discard(task.id)
work_item_id = linked_work_item_id_for_task(task)
if work_item_id:
self._claimed_work_item_ids.discard(work_item_id)
completed_focus_id = str(session.focused_work_item_id or work_item_id).strip()
next_status = "idle"
true_blocking_statuses = {
TaskStatus.AWAITING_PEER,
TaskStatus.AWAITING_HUMAN,
TaskStatus.BLOCKED,
}
if not multi_team_org:
true_blocking_statuses.update(
{
TaskStatus.AWAITING_MANAGER_REVIEW,
TaskStatus.AWAITING_REVIEW,
}
)
if task.status in true_blocking_statuses or dict(task.metadata.get("peer_wait", {}) or {}):
next_status = "blocked"
elif (not multi_team_org) and any(
bool(item.get("requires_ack", False))
for item in list(task.context_snapshot.get("pending_handoffs", []) or [])
if isinstance(item, dict)
):
next_status = "blocked"
# Kanban-push soft-wake restore: if this turn was a review Task that
# preempted a blocked manager session, and that session's prior task
# is still awaiting something, restore the `blocked` state + the
# original focus so the manager remains parked where it was.
review_preempt_active = bool((session.metadata or {}).get("_review_preempt_active", False))
restored_to_blocked = False
if review_preempt_active and (
bool((task.metadata or {}).get("review_task", False))
or bool((task.metadata or {}).get("review_execution_work_item", False))
):
prev_task_id = str((session.metadata or {}).get("_review_preempt_prev_task_id", "") or "")
prev_focus = str((session.metadata or {}).get("_review_preempt_prev_work_item_id", "") or "")
restore_blocked = bool(prev_focus and prev_task_id)
if restore_blocked and self.store is not None and hasattr(self.store, "get_task"):
try:
prev_task = await self.store.get_task(prev_task_id)
except Exception:
prev_task = None
if prev_task is not None:
prev_peer_wait = dict((prev_task.metadata or {}).get("peer_wait", {}) or {})
restore_blocked = bool(
prev_task.status in true_blocking_statuses
or prev_peer_wait
)
next_status = "blocked" if restore_blocked else "idle"
session.current_task_id = prev_task_id if restore_blocked else ""
session.focused_work_item_id = prev_focus if restore_blocked else ""
self._set_member_session_status(session, next_status)
session.metadata = dict(session.metadata or {})
session.metadata.pop("_review_preempt_active", None)
session.metadata.pop("_review_preempt_prev_task_id", None)
session.metadata.pop("_review_preempt_prev_work_item_id", None)
restored_to_blocked = True
if not restored_to_blocked:
session.current_task_id = task.id if next_status == "blocked" else ""
if next_status == "blocked":
session.focused_work_item_id = completed_focus_id
else:
session.focused_work_item_id = ""
self._set_member_session_status(session, next_status)
session.updated_at = datetime.now()
released_assignment = dict(session.current_assignment or {})
summary = self._task_summary(task, result)
if summary:
session.working_memory = [*session.working_memory, summary][-12:]
focused_work_item_id = completed_focus_id
if focused_work_item_id:
existing_slice = list(session.memory_slices_by_work_item.get(focused_work_item_id, []) or [])
session.memory_slices_by_work_item[focused_work_item_id] = [*existing_slice, summary][-12:]
runtime_state = dict(task.metadata.get("runtime_v2", {}) or {})
if not runtime_state and result is not None:
runtime_state = self._runtime_state_from_result(result)
if runtime_state:
session.resume_state = {
**dict(session.resume_state),
**runtime_state,
}
await self._refresh_adapter_session_state_from_store(session)
adapter_state_updates = self._adapter_state_from_result(task, result)
if adapter_state_updates:
session.adapter_session_state = {
**dict(session.adapter_session_state or {}),
**adapter_state_updates,
}
if next_status != "running":
session.current_assignment = {}
session.current_work_item = self._build_current_work_item(session, task)
await self._refresh_manager_board_state(session, task)
current_turn_mode = self._update_current_turn_mode(session, task)
await self._sync_current_turn_mode_to_work_item(task, current_turn_mode)
session.manager_digest = self._build_manager_digest(session, task)
session.inbox_state = {
**dict(session.inbox_state or {}),
"latest_notification": dict(session.latest_notification or {}),
"current_work_item": dict(session.current_work_item or {}),
"current_turn_mode": str(session.current_turn_mode or "").strip(),
"manager_board_summary": dict((session.metadata or {}).get("manager_board_summary", {}) or {}),
"manager_digest": dict(session.manager_digest or {}),
}
role_session = self._role_session_for_member_session(session)
if role_session is not None:
role_session.status = normalize_role_runtime_status(
next_status,
session.focused_work_item_id if next_status == "blocked" else "",
)
role_session.focused_work_item_id = session.focused_work_item_id if next_status == "blocked" else ""
role_session.background_work_item_ids = [item for item in session.background_work_item_ids if item != role_session.focused_work_item_id]
role_session.resume_state = dict(session.resume_state)
role_session.inbox_state = dict(session.inbox_state)
role_session.memory_slices_by_work_item = dict(session.memory_slices_by_work_item)
role_session.adapter_session_state = dict(session.adapter_session_state)
role_session.current_work_item = dict(session.current_work_item or {})
role_session.latest_notification = dict(session.latest_notification or {})
role_session.manager_digest = dict(session.manager_digest or {})
role_session.updated_at = datetime.now()
if self.store and bool(getattr(self.store, "is_ready", False)) and hasattr(self.store, "save_delegation_role_session"):
await self.store.save_delegation_role_session(role_session)
task.metadata = dict(task.metadata)
task.metadata["member_session_state"] = self._serialize_session(session)
task.metadata["current_turn_mode"] = str(session.current_turn_mode or "").strip()
task.context_snapshot = dict(task.context_snapshot)
task.context_snapshot["member_session"] = self._serialize_session(session)
task.context_snapshot["current_turn_mode"] = str(session.current_turn_mode or "").strip()
await self._persist_session(session, task=task)
projection_id = projection_id_for_task(task)
status_body = (
f"Resident teammate `{session.role_id}` is now `{next_status}` after "
f"work item `{projection_id or task.id}`."
)
if self.communication and session.manager_role_id and hasattr(self.communication, "send_manager_notification") and not synthetic_inbox_turn:
assignment_id = str(released_assignment.get("assignment_id", "") or "").strip()
# A worker's successful completion of a delegated work item should
# automatically request the manager's review. The work item will
# already have transitioned to review, and here we ensure the
# manager gets an actionable notification
# with reply_needed=True so the runtime will wake the manager up
# (via _queue_multi_team_response_tasks / claim_runnable_tasks).
review_required = bool(
multi_team_org
and work_item_id
and next_status == "idle"
and task.status == TaskStatus.DONE
)
if summary or review_required:
completion_semantic = (
CommsSemanticType.APPROVAL_REQUEST
if review_required
else CommsSemanticType.COMPLETION
if multi_team_org
else CommsSemanticType.WORK_ITEM_RESULT
)
if review_required:
completion_subject = f"Review needed: {task.title}"
elif multi_team_org:
completion_subject = f"Completion: {task.title}"
else:
completion_subject = f"Work item result: {projection_id or task.title}"
completion_body = summary or (
f"`{session.role_id}` completed work item `{work_item_id}` "
f"(task `{task.title or task.id}`) without a structured report. "
f"Please verify the deliverable directly before approving."
if review_required
else ""
)
completion_metadata = {
**work_item_identity_payload(projection_id=projection_id, turn_type=turn_type_for_task(task, fallback="")),
"assignment_id": assignment_id,
"current_work_item": dict(session.current_work_item or {}),
"manager_board_summary": dict((session.metadata or {}).get("manager_board_summary", {}) or {}),
}
if review_required:
completion_metadata["review_required"] = True
completion_metadata["work_item_id"] = work_item_id
completion_metadata["completion_report"] = summary
await self.communication.send_manager_notification(
from_agent=session.role_id,
task=task,
semantic_type=completion_semantic,
subject=completion_subject,
body=completion_body,
metadata=completion_metadata,
reply_needed=review_required,
)
status_semantic = CommsSemanticType.STATUS_DIGEST if multi_team_org else CommsSemanticType.IDLE_NOTIFICATION
status_subject = f"Status digest: {task.title}" if multi_team_org else f"Resident teammate {next_status}"
status_reply_needed = False
if next_status == "blocked":
status_semantic = CommsSemanticType.BLOCKER if multi_team_org else CommsSemanticType.BLOCKED_ON_DECISION
status_subject = f"Blocked: {task.title}" if multi_team_org else f"Resident teammate blocked: {projection_id or task.title}"
waiting_on = [
str(item).strip()
for item in list(
dict(task.metadata.get("peer_wait", {}) or {}).get("waiting_on_agents")
or dict(task.metadata.get("peer_wait", {}) or {}).get("awaiting_replies_from")
or []
)
if str(item).strip()
]
waiting_suffix = f" Waiting on: {', '.join(waiting_on)}." if waiting_on else ""
status_body = (
f"Resident teammate `{session.role_id}` is blocked after "
f"work item `{projection_id or task.id}`.{waiting_suffix}"
)
status_reply_needed = True
await self.communication.send_manager_notification(
from_agent=session.role_id,
task=task,
semantic_type=status_semantic,
subject=status_subject,
body=status_body,
metadata={
**work_item_identity_payload(projection_id=projection_id, turn_type=turn_type_for_task(task, fallback="")),
"assignment_id": assignment_id,
"resident_status": next_status,
"current_work_item": dict(session.current_work_item or {}),
"manager_digest": dict(session.manager_digest or {}),
"manager_board_summary": dict((session.metadata or {}).get("manager_board_summary", {}) or {}),
},
reply_needed=status_reply_needed,
)
notification_payload = {
"member_session_id": session.member_session_id,
"worker_id": session.member_session_id,
"worker_type": "company_member",
"notification_kind": (
"blocked"
if next_status == "blocked"
else "status_digest"
if multi_team_org
else "idle"
),
"summary": summary or status_body,
"resident_status": next_status,
"role_id": session.role_id,
"employee_id": session.employee_id,
"task_id": task.id,
"session_id": str(task.session_id or task.parent_session_id or "").strip(),
**work_item_identity_payload_for_task(task),
}
await self._emit("member_idle", notification_payload)
await self._emit("worker_notification", notification_payload)
def session_for_task(self, task: Task) -> CompanyMemberSession:
session_id = str(task.metadata.get("member_session_id", "")).strip()
if session_id and session_id in self.member_sessions:
return self.member_sessions[session_id]
return self._ensure_member_session(task)
def _role_session_for_member_session(self, session: CompanyMemberSession) -> DelegationRoleSession | None:
role_session_id = str(session.role_session_id or "").strip()
if role_session_id and role_session_id in self.role_sessions:
return self.role_sessions[role_session_id]
session_scope_id = str((session.metadata or {}).get("session_scope_id", "") or "").strip()
for candidate in self.role_sessions.values():
candidate_scope = str((getattr(candidate, "metadata", {}) or {}).get("session_scope_id", "") or "").strip()
if session_scope_id and candidate_scope != session_scope_id:
continue
if candidate.seat_id and candidate.seat_id == session.seat_id:
return candidate
for candidate in self.role_sessions.values():
candidate_scope = str((getattr(candidate, "metadata", {}) or {}).get("session_scope_id", "") or "").strip()
if session_scope_id and candidate_scope != session_scope_id:
continue
if candidate.role_id == session.role_id and candidate.seat_id == session.seat_id:
return candidate
return None
async def _refresh_adapter_session_state_from_store(
self,
session: CompanyMemberSession,
) -> None:
role_session_id = str(session.role_session_id or "").strip()
getter = getattr(self.store, "get_delegation_role_session", None)
if not role_session_id or not callable(getter):
return
try:
persisted = await getter(role_session_id)
except Exception:
logger.opt(exception=True).debug(
"company runtime adapter-state refresh failed"
)
return
if persisted is None:
return
adapter_state = dict(
getattr(persisted, "adapter_session_state", {}) or {}
)
session.adapter_session_state = adapter_state
role_session = self.role_sessions.get(role_session_id)
if role_session is not None:
role_session.adapter_session_state = dict(adapter_state)
def _ensure_role_session(self, task: Task) -> DelegationRoleSession | None:
role_id = self._role_id(task)
if not role_id:
return None
role_session_id = self._role_session_id(task, role_id=role_id)
existing = self.role_sessions.get(role_session_id)
if existing is not None:
existing.status = normalize_role_runtime_status(
existing.status,
existing.focused_work_item_id,
)
if existing.status == "idle":
existing.focused_work_item_id = ""
existing.metadata = {
**dict(existing.metadata or {}),
"session_scope_id": task_session_scope_id(task),
}
return existing
run_id = str((task.metadata or {}).get("delegation_run_id", "") or "").strip()
role_session = DelegationRoleSession(
role_session_id=role_session_id,
run_id=run_id,
role_id=role_id,
employee_id=self._employee_id(task),
manager_role_ids=self._manager_role_ids(task),
status="idle",
metadata={"session_scope_id": task_session_scope_id(task)},
)
self.role_sessions[role_session_id] = role_session
return role_session
async def _claim_role_session_work_item(
self,
session: CompanyMemberSession,
work_item: Any,
task: Task,
) -> bool:
"""Atomically claim ``work_item`` for the role-instance behind
``session``.
In the role-instance model the claim identity is
``role_runtime_session_id``. Seat / manager-seat columns are
still written for org-chart lookups but they are NOT part of
the claim key — only the role session is. Pure in-memory runtimes have
no durable race to arbitrate and keep the same local claim semantics.
"""
work_item_id = str(getattr(work_item, "work_item_id", "") or "").strip()
if not work_item_id:
return False
role_session = self._ensure_role_session(task)
if role_session is None:
return False
work_item_revision = 0
try:
work_item_revision = int((getattr(work_item, "metadata", {}) or {}).get("manager_mutation_revision") or 0)
except (TypeError, ValueError):
work_item_revision = 0
store_ready = self.store is not None and bool(
getattr(self.store, "is_ready", False)
)
claim = (
getattr(self.store, "claim_delegation_work_item_if_dispatchable", None)
if store_ready
else None
)
if store_ready:
# A durable runtime must win the store CAS before it mutates any
# in-memory scheduling state. This is the Stop/shutdown race
# boundary: a missing CAS API is a failed claim, not permission to
# fall back to the former best-effort update path.
if not callable(claim):
return False
persisted = await claim(
work_item_id,
expected_phase=getattr(work_item, "phase", Phase.READY),
role_runtime_session_id=role_session.role_session_id,
seat_id=str(getattr(session, "seat_id", "") or "").strip(),
task_id=task.id,
work_item_revision=work_item_revision,
)
if persisted is None:
return False
work_item.phase = persisted.phase
work_item.role_runtime_session_id = persisted.role_runtime_session_id
work_item.claimed_by_role_runtime_session_id = (
persisted.claimed_by_role_runtime_session_id
)
work_item.claimed_by_seat_id = persisted.claimed_by_seat_id
work_item.metadata = dict(persisted.metadata or {})
ready_background_ids = [
item_id
for item_id in list(role_session.background_work_item_ids or [])
if item_id and item_id != work_item_id
]
role_session.focused_work_item_id = work_item_id
role_session.background_work_item_ids = ready_background_ids
role_session.status = "running"
role_session.updated_at = datetime.now()
session.role_session_id = role_session.role_session_id
session.focused_work_item_id = work_item_id
session.background_work_item_ids = list(role_session.background_work_item_ids)
task.metadata = dict(task.metadata)
task.metadata["delegation_role_session_id"] = role_session.role_session_id
task.metadata["started_work_item_revision"] = work_item_revision
task.metadata["claimed_work_item_revision"] = work_item_revision
if self.store and bool(getattr(self.store, "is_ready", False)) and hasattr(self.store, "save_delegation_role_session"):
await self.store.save_delegation_role_session(role_session)
return True
def ensure_role_instance_session(
self, task: Task
) -> tuple[CompanyMemberSession, DelegationRoleSession | None]:
"""Role-instance upsert.
Phase A collapsed "per-seat session" into "per-role-instance
session" — the ``CompanyMemberSession`` (runtime scheduling
view) and the ``DelegationRoleSession`` (persistent seat
memory view) are 1-to-1 coupled by ``role_session_id``. This
helper returns both, upserting the pair atomically.
Callers that only want one of the two use the backwards-compat
``_ensure_member_session`` / ``_ensure_role_session`` below.
New call sites should prefer this helper so the 1:1 invariant
is explicit.
"""
member = self._ensure_member_session(task)
role = self._role_session_for_member_session(member)
return member, role
def _ensure_member_session(self, task: Task) -> CompanyMemberSession:
"""Upsert the CompanyMemberSession for ``task``'s role.
The linked DelegationRoleSession is created as a side-effect
(see ``_ensure_role_session`` call below) to preserve the 1:1
invariant — callers that want both entities should use
``ensure_role_instance_session`` for clarity.
"""
role_id = self._role_id(task)
employee_id = self._employee_id(task)
member_session_id = self._member_session_id(task, role_id=role_id, employee_id=employee_id)
existing = self.member_sessions.get(member_session_id)
if existing is not None:
self._merge_task_session_state(existing, task)
self._sync_member_session_from_role_session(
existing,
self._ensure_role_session(task),
)
return existing
session = CompanyMemberSession(
member_session_id=member_session_id,
role_id=role_id,
employee_id=employee_id,
)
session.team_id = str((task.metadata or {}).get("delegation_team_id", "") or "").strip()
session.seat_id = self._seat_id(task)
session.metadata = {
**dict(session.metadata or {}),
"team_id": session.team_id,
"seat_id": session.seat_id,
"session_scope_id": task_session_scope_id(task),
"manager_seat_id": str((task.metadata or {}).get("manager_seat_id", "") or "").strip(),
"managed_team_id": str((task.metadata or {}).get("managed_team_id", "") or "").strip(),
"contact_role_ids": list((task.metadata or {}).get("seat_contact_role_ids", []) or []),
"allowed_delegate_role_ids": list((task.metadata or {}).get("allowed_delegate_role_ids", []) or []),
"direct_report_role_ids": list((task.metadata or {}).get("direct_report_role_ids", []) or []),
"direct_report_seat_ids": list((task.metadata or {}).get("direct_report_seat_ids", []) or []),
}
self._merge_task_session_state(session, task)
session.metadata = {
**dict(session.metadata or {}),
"session_scope_id": task_session_scope_id(task),
}
role_session = self._ensure_role_session(task)
if role_session is not None:
self._sync_member_session_from_role_session(session, role_session)
self.member_sessions[member_session_id] = session
return session
def _merge_task_session_state(self, session: CompanyMemberSession, task: Task) -> None:
persisted = dict(task.metadata.get("member_session_state", {}) or {})
persisted_metadata = dict(persisted.get("metadata", {}) or {})
if persisted_metadata:
session.metadata = {
**dict(session.metadata or {}),
**persisted_metadata,
}
persisted_status = str(persisted.get("status", "") or "").strip()
persisted_resident_status = str(persisted.get("resident_status", "") or "").strip()
persisted_role_session_id = str(persisted.get("role_session_id", "") or task.metadata.get("delegation_role_session_id", "") or "").strip()
if persisted_role_session_id:
session.role_session_id = persisted_role_session_id
team_instance_id = str(persisted.get("team_instance_id", "") or "").strip()
if team_instance_id:
session.team_instance_id = team_instance_id
team_id = str(persisted.get("team_id", "") or "").strip()
if team_id:
session.team_id = team_id
seat_id = str(persisted.get("seat_id", "") or task.metadata.get("delegation_seat_id", "") or "").strip()
if seat_id:
session.seat_id = seat_id
seat_state_id = str(persisted.get("seat_state_id", "") or "").strip()
if seat_state_id:
session.seat_state_id = seat_state_id
persisted_current_task = str(persisted.get("current_task_id", "") or "").strip()
if persisted_current_task:
session.current_task_id = persisted_current_task
focused_work_item_id = str(persisted.get("focused_work_item_id", "") or "").strip()
if focused_work_item_id:
session.focused_work_item_id = focused_work_item_id
if persisted_status or persisted_resident_status:
self._set_member_session_status(
session,
persisted_status or persisted_resident_status,
)
background_work_item_ids = persisted.get("background_work_item_ids")
if isinstance(background_work_item_ids, list):
session.background_work_item_ids = [str(item).strip() for item in background_work_item_ids if str(item).strip()]
working_memory = list(persisted.get("working_memory", []) or [])
if working_memory:
merged_memory = [*session.working_memory, *[str(item).strip() for item in working_memory if str(item).strip()]]
session.working_memory = list(dict.fromkeys(merged_memory))[-12:]
memory_slices_by_work_item = persisted.get("memory_slices_by_work_item")
if isinstance(memory_slices_by_work_item, dict):
session.memory_slices_by_work_item = {
str(key).strip(): [str(item).strip() for item in list(value or []) if str(item).strip()]
for key, value in memory_slices_by_work_item.items()
if str(key).strip()
}
inbox_cursor = persisted.get("inbox_cursor")
if isinstance(inbox_cursor, int):
session.inbox_cursor = max(session.inbox_cursor, inbox_cursor)
pending_inbox = persisted.get("pending_inbox")
if isinstance(pending_inbox, list) and pending_inbox:
session.pending_inbox = [dict(item) for item in pending_inbox[:8] if isinstance(item, dict)]
queued_inbox = persisted.get("queued_inbox")
if isinstance(queued_inbox, list) and queued_inbox:
session.queued_inbox = [dict(item) for item in queued_inbox[:12] if isinstance(item, dict)]
actionable_chat = persisted.get("actionable_chat")
if isinstance(actionable_chat, list) and actionable_chat:
session.actionable_chat = [dict(item) for item in actionable_chat[:8] if isinstance(item, dict)]
protocol_backlog = persisted.get("protocol_backlog")
if isinstance(protocol_backlog, list) and protocol_backlog:
session.protocol_backlog = [dict(item) for item in protocol_backlog[:8] if isinstance(item, dict)]
notification_backlog = persisted.get("notification_backlog")
if isinstance(notification_backlog, list) and notification_backlog:
session.notification_backlog = [dict(item) for item in notification_backlog[:8] if isinstance(item, dict)]
latest_notification = persisted.get("latest_notification")
if isinstance(latest_notification, dict) and latest_notification:
session.latest_notification = dict(latest_notification)
current_work_item = persisted.get("current_work_item")
if isinstance(current_work_item, dict) and current_work_item:
session.current_work_item = dict(current_work_item)
manager_digest = persisted.get("manager_digest")
if isinstance(manager_digest, dict) and manager_digest:
session.manager_digest = dict(manager_digest)
inbox_state = persisted.get("inbox_state")
if isinstance(inbox_state, dict) and inbox_state:
session.inbox_state = dict(inbox_state)
for field_name in (
"actionable_inbox_count",
"protocol_backlog_count",
"notification_backlog_count",
):
value = persisted.get(field_name)
if isinstance(value, int):
setattr(session, field_name, value)
resume_state = persisted.get("resume_state")
if isinstance(resume_state, dict):
session.resume_state = {
**dict(session.resume_state),
**resume_state,
}
current_turn_mode = str(
persisted.get("current_turn_mode", "")
or persisted_metadata.get("current_turn_mode", "")
or task.metadata.get("current_turn_mode", "")
or dict(task.context_snapshot.get("member_session", {}) or {}).get("current_turn_mode", "")
or task.context_snapshot.get("current_turn_mode", "")
or ""
).strip()
if current_turn_mode:
session.current_turn_mode = current_turn_mode
if session.seat_id:
session.metadata = {
**dict(session.metadata or {}),
"seat_id": session.seat_id,
"team_id": session.team_id,
}
for key in (
"manager_seat_id",
"managed_team_id",
):
value = str((task.metadata or {}).get(key, "") or "").strip()
if value:
session.metadata = {
**dict(session.metadata or {}),
key: value,
}
for key in (
"seat_contact_role_ids",
"allowed_delegate_role_ids",
"direct_report_role_ids",
"direct_report_seat_ids",
):
values = [str(item).strip() for item in list((task.metadata or {}).get(key, []) or []) if str(item).strip()]
if values:
session.metadata = {
**dict(session.metadata or {}),
(
"contact_role_ids" if key == "seat_contact_role_ids"
else "allowed_delegate_role_ids" if key == "allowed_delegate_role_ids"
else key
): values,
}
if session.current_turn_mode:
session.metadata = {
**dict(session.metadata or {}),
"current_turn_mode": session.current_turn_mode,
}
manager_role_id = str(persisted.get("manager_role_id", "") or "").strip()
if manager_role_id:
session.manager_role_id = manager_role_id
manager_role_ids = persisted.get("manager_role_ids")
if isinstance(manager_role_ids, list):
session.manager_role_ids = [str(item).strip() for item in manager_role_ids if str(item).strip()]
adapter_session_state = persisted.get("adapter_session_state")
if isinstance(adapter_session_state, dict):
session.adapter_session_state = dict(adapter_session_state)
assignment = dict(persisted.get("current_assignment", {}) or {})
if assignment:
session.current_assignment = assignment
if self.org_engine is not None:
agent = self.org_engine.get_agent(session.role_id)
manager_role = str(getattr(agent, "reports_to", "") or "").strip()
if manager_role and manager_role != "owner":
session.manager_role_id = manager_role
session.manager_role_ids = sorted(dict.fromkeys([*session.manager_role_ids, manager_role]))
session.resume_state = dict(session.resume_state)
session.resume_state["manager_role_id"] = manager_role
self._normalize_member_session_status(session)
if task.result and isinstance(task.result, dict) and task.result.get("content"):
summary = str(task.result.get("content", "")).strip()
if summary:
session.working_memory = [*session.working_memory, summary][-12:]
async def _persist_session(self, session: CompanyMemberSession, task: Task | None = None) -> None:
self._normalize_member_session_status(session)
await self._persist_seat_state(session, task=task)
if not self.save_runtime_session:
if self.store and bool(getattr(self.store, "is_ready", False)) and hasattr(self.store, "save_delegation_role_session"):
role_session = self._role_session_for_member_session(session)
if role_session is not None:
await self.store.save_delegation_role_session(role_session)
return
role_session = self._role_session_for_member_session(session)
project_id = str(getattr(task, "project_id", "") or getattr(role_session, "project_id", "") or "default")
runtime_task_id = str(getattr(task, "id", "") or session.current_task_id or "").strip() or None
runtime_session_id = str(getattr(task, "session_id", None) or "").strip() or None
await self.save_runtime_session(
runtime_session_id=session.member_session_id,
project_id=project_id,
session_id=runtime_session_id,
task_id=runtime_task_id,
status=session.status,
metadata=self._serialize_session(session),
)
if self.store and bool(getattr(self.store, "is_ready", False)) and hasattr(self.store, "save_delegation_role_session"):
if role_session is not None:
await self.store.save_delegation_role_session(role_session)
async def _emit(self, event_type: str, payload: dict[str, Any]) -> None:
if self.emit_runtime_event is None:
return
await self.emit_runtime_event(event_type, payload)
def _highest_message_priority(self, session: CompanyMemberSession) -> str:
inbox = self._session_priority_messages(session)
if not inbox:
return "ready_queue"
top = inbox[0]
return str(self._message_priority_label(session, top))
def _message_sort_key(self, session: CompanyMemberSession, message: dict[str, Any]) -> tuple[int, str]:
priority = self._message_priority_value(session, message)
msg_id = str(message.get("msg_id", "")).strip()
return (priority, msg_id)
def _message_priority_value(self, session: CompanyMemberSession, message: dict[str, Any]) -> int:
manager_role = str(session.manager_role_id or session.resume_state.get("manager_role_id", "")).strip()
from_agent = str(message.get("from_agent", "") or "").strip()
message_class = str(message.get("message_class", "") or dict(message.get("metadata", {}) or {}).get("message_class", "")).strip().lower()
protocol_type = str(message.get("protocol_type", "") or dict(message.get("metadata", {}) or {}).get("protocol_type", "")).strip().lower()
actionable = bool(message.get("actionable", dict(message.get("metadata", {}) or {}).get("actionable", True)))
if protocol_type == "shutdown_request":
return -1
if manager_role and from_agent == manager_role:
return 0
if message_class == "protocol":
return 1
if bool(message.get("reply_needed")) or str(message.get("urgency", "")).strip().lower() == "blocking":
return 2
if actionable:
return 3
if message_class == "notification":
return 4
return 5
def _message_priority_label(self, session: CompanyMemberSession, message: dict[str, Any]) -> str:
priority = self._message_priority_value(session, message)
if priority == 0:
return "manager"
if priority == 1:
return "protocol"
if priority == 2:
return "blocking_reply"
if priority == 3:
return "peer_dm"
return "notification"
def _get_role_config(self, role_id: str) -> Any | None:
if not self.org_engine:
return None
org_config = getattr(getattr(self.org_engine, "config", None), "org", None)
if org_config is not None:
for role in getattr(org_config, "roles", []) or []:
if role.id == role_id:
return role
effective_roles = getattr(self.org_engine, "_effective_roles", None)
if callable(effective_roles):
try:
for role in list(effective_roles()) or []:
if getattr(role, "id", "") == role_id:
return role
except Exception:
pass
return None
def _session_sort_key(self, session: CompanyMemberSession) -> tuple[int, str]:
priority = 3
inbox = self._session_priority_messages(session)
if inbox:
priority = self._message_priority_value(session, inbox[0])
# Coordinators with accumulated notifications get scheduling priority
role_cfg = self._get_role_config(session.role_id)
if role_cfg and (
getattr(role_cfg, "role_type", "worker") == "coordinator"
or bool(list(getattr(role_cfg, "can_spawn", []) or []))
):
threshold = 3
policy = getattr(role_cfg, "coordinator_policy", None)
if policy:
threshold = getattr(policy, "inbox_threshold", 3)
pending_count = len(session.inbox_state.get("actionable_chat", []))
if pending_count >= threshold:
priority = max(priority - 10, -10) # boost priority
return (priority, session.member_session_id)
@staticmethod
def _is_runnable(task: Task) -> bool:
return getattr(task, "status", None) and str(task.status.value if hasattr(task.status, "value") else task.status) == "pending"
@staticmethod
def _pop_next_queue_entry(queue: deque[str]) -> str:
"""Pop the next queue entry, prioritizing review tasks.
Review tasks are always pulled before any other work in the seat's
queue — this enforces "manager must clear pending reviews before
dispatching or executing own work" at the scheduler level rather
than at the prompt level.
"""
for index, entry in enumerate(queue):
if entry.startswith("review-task::") or entry.startswith("review-work-item::"):
if index == 0:
return queue.popleft()
del queue[index]
return entry
return queue.popleft()
@staticmethod
def _role_id(task: Task) -> str:
return str(task.assigned_to or task.metadata.get("work_item_role_id", "") or "").strip()
@staticmethod
def _seat_id(task: Task) -> str:
metadata = dict(getattr(task, "metadata", {}) or {})
return str(
metadata.get("delegation_seat_id", "")
or metadata.get("seat_id", "")
or ""
).strip()
def _queue_key_for_task(self, task: Task) -> str:
# Fix 5 PR4: role-scoped (no team_instance slot). Same role =
# same queue regardless of which team's work arrived. The role's
# single session drains this queue serially per PR3.
return scoped_queue_key(
session_scope_id=task_session_scope_id(task),
role_id=self._role_id(task),
)
def _queue_key_for_session(self, session: CompanyMemberSession) -> str:
# Fix 5 PR4: matches _queue_key_for_task — role-scoped only.
return scoped_queue_key(
session_scope_id=str((session.metadata or {}).get("session_scope_id", "") or "").strip(),
role_id=str(session.role_id or "").strip(),
)
def _employee_id(self, task: Task) -> str:
assignment = dict(task.metadata.get("employee_assignment", {}) or {})
employee_id = str(assignment.get("employee_id", "") or "").strip()
if employee_id:
return employee_id
role_id = self._role_id(task)
return f"{role_id}-default-session"
def _member_session_id(self, task: Task, *, role_id: str, employee_id: str) -> str:
# Fix 5 PR4: role-scoped (no team_instance slot). One role = one
# member_session across every team context, aligned with the
# role-scoped canonical_role_session_id.
return scoped_member_session_id(
project_id=str(getattr(task, "project_id", "") or "default").strip() or "default",
session_scope_id=task_session_scope_id(task),
role_id=role_id,
employee_id=employee_id,
explicit_id=str((task.metadata or {}).get("member_session_id", "") or "").strip(),
)
def _build_assignment_envelope(self, session: CompanyMemberSession, task: Task) -> dict[str, Any]:
workspace_root = (
str(task.metadata.get("comms_workspace_root", "") or "").strip()
or str(task.metadata.get("workspace_root", "") or "").strip()
or str(task.metadata.get("target_output_dir", "") or "").strip()
)
team_memory_digest = ""
team_memory_metadata: dict[str, Any] = {}
if workspace_root:
try:
layout = _comms.resolve_layout(
workspace_root,
str(task.project_id or "default").strip() or "default",
str(task.parent_session_id or task.session_id or "default").strip() or "default",
)
_comms.ensure_layout(layout, [session.role_id, session.manager_role_id or session.role_id])
team_memory_payload = _comms.read_team_memory_digest_payload(layout, max_chars=1200)
team_memory_digest = str(team_memory_payload.get("digest", "") or "").strip()
team_memory_metadata = {
key: value
for key, value in team_memory_payload.items()
if key != "digest"
}
except Exception:
team_memory_digest = ""
team_memory_metadata = {}
# Fix 5 PR5: team_instance / team_id / seat_id belong to the
# CURRENT task — the session now serves multiple team contexts
# over its lifetime, so ``session.*`` reflects only the most
# recently observed context and is wrong for any other task.
# Prefer per-task metadata; fall back to session as a last resort.
task_metadata = dict(task.metadata or {})
assignment_team_instance = (
str(task_metadata.get("delegation_team_instance_id", "") or "").strip()
or str(task_metadata.get("team_instance_id", "") or "").strip()
or str(session.team_instance_id or "").strip()
)
assignment_team_id = (
str(task_metadata.get("delegation_team_id", "") or "").strip()
or str(task_metadata.get("team_id", "") or "").strip()
or str(session.team_id or (session.metadata or {}).get("team_id", "") or "").strip()
)
assignment_seat_id = (
str(task_metadata.get("delegation_seat_id", "") or "").strip()
or str(task_metadata.get("seat_id", "") or "").strip()
or str(session.seat_id or (session.metadata or {}).get("seat_id", "") or "").strip()
)
runtime_status = self._normalize_member_session_status(session)
assignment = ResidentAssignmentEnvelope(
member_session_id=session.member_session_id,
team_instance_id=assignment_team_instance,
team_id=assignment_team_id,
seat_id=assignment_seat_id,
seat_state_id=str(session.seat_state_id or "").strip(),
role_runtime_session_id=str(session.role_session_id or "").strip(),
work_item_projection_id=projection_id_for_task(task),
work_item_turn_type=turn_type_for_task(task, fallback=""),
role_id=session.role_id,
employee_id=session.employee_id,
manager_role_id=session.manager_role_id,
task_id=str(task.id or "").strip(),
session_id=str(task.session_id or task.parent_session_id or "").strip(),
write_scope=str(task.metadata.get("write_scope", "") or "").strip(),
ownership_contract=render_ownership_contract(task),
dependency_snapshot=[str(dep).strip() for dep in list(task.dependencies or []) if str(dep).strip()],
pending_inbox=list([*list(session.pending_inbox or []), *list(session.queued_inbox or [])][-8:]),
actionable_chat=list(session.actionable_chat[:8]),
protocol_backlog=list(session.protocol_backlog[:6]),
latest_notification=dict(session.latest_notification or {}),
resident_status=runtime_status,
team_memory_digest=team_memory_digest,
artifact_refs=list(task.metadata.get("artifacts", []) or []),
metadata={
"message_priority": self._highest_message_priority(session),
"actionable_inbox_count": session.actionable_inbox_count,
"protocol_backlog_count": session.protocol_backlog_count,
"notification_backlog_count": session.notification_backlog_count,
"delegation_role_session_id": str(session.role_session_id or "").strip(),
"focused_work_item_id": str(session.focused_work_item_id or "").strip(),
"work_item_id": str(linked_work_item_id_for_task(task) or session.focused_work_item_id or "").strip(),
# Fix 5 PR5: task-scoped (see envelope-level note above).
"team_id": assignment_team_id,
"seat_id": assignment_seat_id,
"manager_seat_id": str((session.metadata or {}).get("manager_seat_id", "") or "").strip(),
"manager_board_summary": dict((session.metadata or {}).get("manager_board_summary", {}) or {}),
"parent_board_scope": str((session.metadata or {}).get("parent_board_scope", "") or "").strip(),
"current_turn_mode": str(session.current_turn_mode or "").strip(),
**team_memory_metadata,
},
)
return asdict(assignment)
@staticmethod
def _session_priority_messages(session: CompanyMemberSession) -> list[dict[str, Any]]:
return [
*list(session.protocol_backlog or []),
*list(session.pending_inbox or session.queued_inbox or []),
*list(session.notification_backlog or []),
]
@staticmethod
def _latest_notification(messages: list[dict[str, Any]]) -> dict[str, Any]:
if not messages:
return {}
def _timestamp(item: dict[str, Any]) -> str:
return str(
item.get("timestamp")
or item.get("sent_at")
or dict(item.get("metadata", {}) or {}).get("timestamp")
or ""
)
latest = max(messages, key=_timestamp)
return dict(latest)
def _build_current_work_item(
self,
session: CompanyMemberSession,
task: Task | None = None,
) -> dict[str, Any]:
task_work_item_id = ""
if task is not None:
task_work_item_id = linked_work_item_id_for_task(task)
work_item_id = str(
session.focused_work_item_id
or dict(session.current_assignment or {}).get("metadata", {}).get("work_item_id", "")
or task_work_item_id
).strip()
if not work_item_id and not task and not session.current_assignment:
return {}
title = ""
if task is not None:
title = str(task.title or "").strip()
if not title:
title = work_item_projection_id_from_metadata(dict(session.current_assignment or {}), fallback="")
current_assignment = dict(session.current_assignment or {})
projection_id = projection_id_for_task(task) if task is not None else work_item_projection_id_from_metadata(current_assignment, fallback="")
turn_type = turn_type_for_task(task, fallback="") if task is not None else work_item_turn_type_from_metadata(current_assignment, fallback="")
return {
"work_item_id": work_item_id,
"task_id": str(getattr(task, "id", "") or session.current_task_id or "").strip(),
"title": title,
"status": self._normalize_member_session_status(session),
"role_id": str(session.role_id or "").strip(),
"seat_id": str(session.seat_id or (session.metadata or {}).get("seat_id", "") or "").strip(),
**work_item_identity_payload(projection_id=projection_id, turn_type=turn_type),
}
def _build_pending_decisions(self, session: CompanyMemberSession) -> list[dict[str, Any]]:
decisions: list[dict[str, Any]] = []
for item in list(session.protocol_backlog or [])[:8]:
metadata = dict(item.get("metadata", {}) or {})
protocol_type = str(item.get("protocol_type") or metadata.get("protocol_type") or "").strip()
semantic_type = str(item.get("semantic_type") or metadata.get("semantic_type") or "").strip()
message_class = str(item.get("message_class") or metadata.get("message_class") or "").strip().lower()
if not protocol_type and (message_class == "protocol" or item.get("reply_needed")):
protocol_type = "decision_request"
if not (protocol_type or semantic_type or item.get("reply_needed")):
continue
decisions.append(
{
"msg_id": str(item.get("msg_id", "") or "").strip(),
"from_agent": str(item.get("from_agent", "") or "").strip(),
"subject": str(item.get("subject", "") or "").strip(),
"protocol_type": protocol_type or semantic_type,
"reply_needed": bool(item.get("reply_needed")),
}
)
return decisions[:4]
def _blocked_reason(
self,
session: CompanyMemberSession,
task: Task | None = None,
) -> str:
if task is not None:
peer_wait = dict((task.metadata or {}).get("peer_wait", {}) or {})
waiting_on = [
str(item).strip()
for item in list(
peer_wait.get("waiting_on_agents")
or peer_wait.get("awaiting_replies_from")
or []
)
if str(item).strip()
]
if waiting_on:
return f"Waiting on {', '.join(waiting_on)}"
for item in [*list(session.protocol_backlog or []), *list(session.notification_backlog or [])]:
urgency = str(item.get("urgency", "") or "").strip().lower()
if item.get("reply_needed") or urgency == "blocking":
return str(item.get("subject", "") or item.get("body", "") or "").strip()
board_summary = dict((session.metadata or {}).get("manager_board_summary", {}) or {})
blocked_reasons = [
str(item).strip()
for item in list(board_summary.get("blocked_reasons", []) or [])
if str(item).strip()
]
if blocked_reasons:
return blocked_reasons[0]
return ""
def _build_manager_digest(
self,
session: CompanyMemberSession,
task: Task | None = None,
) -> dict[str, Any]:
latest_notification = dict(session.latest_notification or {})
last_deliverable_summary = ""
notification_kind = str(
latest_notification.get("notification_kind")
or dict(latest_notification.get("metadata", {}) or {}).get("notification_kind")
or ""
).strip()
if notification_kind in {"handoff_ready", "delivery_candidate", "idle", "completion", "status_digest", "task_complete"}:
last_deliverable_summary = str(
latest_notification.get("summary")
or latest_notification.get("body")
or latest_notification.get("subject")
or ""
).strip()
if not last_deliverable_summary:
for item in list(session.notification_backlog or []):
item_kind = str(
item.get("notification_kind")
or dict(item.get("metadata", {}) or {}).get("notification_kind")
or ""
).strip()
if item_kind in {"handoff_ready", "delivery_candidate", "idle", "completion", "status_digest", "task_complete"}:
last_deliverable_summary = str(
item.get("summary")
or item.get("body")
or item.get("subject")
or ""
).strip()
if last_deliverable_summary:
break
if not last_deliverable_summary and session.notification_backlog:
first_notification = dict(session.notification_backlog[0] or {})
last_deliverable_summary = str(
first_notification.get("summary")
or first_notification.get("body")
or first_notification.get("subject")
or ""
).strip()
if not last_deliverable_summary and session.working_memory:
last_deliverable_summary = str(session.working_memory[-1] or "").strip()
return {
"actionable_chat": [dict(item) for item in list(session.actionable_chat or [])[:6]],
"protocol_backlog": [dict(item) for item in list(session.protocol_backlog or [])[:6]],
"notification_backlog": [dict(item) for item in list(session.notification_backlog or [])[:6]],
"latest_notification": latest_notification,
"manager_board_summary": dict((session.metadata or {}).get("manager_board_summary", {}) or {}),
"parent_board_scope": str((session.metadata or {}).get("parent_board_scope", "") or "").strip(),
"current_turn_mode": str(session.current_turn_mode or "").strip(),
"resident_status": self._normalize_member_session_status(session),
"current_work_item": self._build_current_work_item(session, task),
"last_deliverable_summary": last_deliverable_summary,
"blocked_reason": self._blocked_reason(session, task),
"pending_decisions": self._build_pending_decisions(session),
}
async def _persist_seat_state(
self,
session: CompanyMemberSession,
*,
task: Task | None = None,
) -> None:
if not (self.store and bool(getattr(self.store, "is_ready", False))):
return
save_seat_state = getattr(self.store, "save_seat_state", None) or getattr(self.store, "save_delegation_seat_state", None)
if not callable(save_seat_state):
return
role_session = self._role_session_for_member_session(session)
seat_state = None
get_seat_state = getattr(self.store, "get_seat_state", None) or getattr(self.store, "get_delegation_seat_state", None)
if session.seat_state_id and callable(get_seat_state):
seat_state = await get_seat_state(session.seat_state_id)
if seat_state is None and session.seat_id:
list_seat_states = getattr(self.store, "list_seat_states", None) or getattr(self.store, "list_delegation_seat_states", None)
if callable(list_seat_states):
run_id = str(
getattr(task, "metadata", {}).get("delegation_run_id", "") if task is not None else ""
or getattr(role_session, "run_id", "") or ""
).strip()
if run_id:
candidates = await list_seat_states(run_id=run_id, seat_id=session.seat_id)
seat_state = candidates[0] if candidates else None
if seat_state is None:
return
seat_state.member_session_id = session.member_session_id
seat_state.role_runtime_session_id = str(session.role_session_id or seat_state.role_runtime_session_id or "").strip()
session_status = self._normalize_member_session_status(session)
active_work_item_id = str(session.focused_work_item_id or "").strip()
seat_state.status = session_status
seat_state.resident_status = session_status
seat_state.current_task_id = (
str(session.current_task_id or getattr(task, "id", "") or "").strip()
if session_status != "idle"
else ""
)
seat_state.current_work_item_id = (
active_work_item_id
if session_status != "idle"
else ""
)
seat_state.inbox_state = dict(session.inbox_state or {})
seat_state.resume_state = dict(session.resume_state or {})
seat_state.current_work_item = dict(session.current_work_item or self._build_current_work_item(session, task))
seat_state.latest_notification = dict(session.latest_notification or {})
seat_state.manager_digest = dict(session.manager_digest or self._build_manager_digest(session, task))
seat_state.metadata = {
**dict(seat_state.metadata or {}),
"manager_digest": dict(seat_state.manager_digest),
"current_turn_mode": str(session.current_turn_mode or "").strip(),
}
seat_state.updated_at = datetime.now()
await save_seat_state(seat_state)
def _serialize_session(self, session: CompanyMemberSession) -> dict[str, Any]:
self._normalize_member_session_status(session)
payload = asdict(session)
payload["created_at"] = session.created_at.isoformat()
payload["updated_at"] = session.updated_at.isoformat()
# Fix 5 PR7: expose pending queue depth + ids on the member
# session payload so the kanban / UI can show "CTO has 2 queued
# tasks" without a separate round-trip. Drawn from the linked
# role_session (CompanyMemberSession itself doesn't hold the queue
# — that state lives on RoleRuntimeSession per PR3).
pending_ids: list[str] = []
role_session_id = str(session.role_session_id or "").strip()
if role_session_id:
role_session = self.role_sessions.get(role_session_id)
if role_session is not None:
pending_ids = list(
getattr(role_session, "pending_work_item_ids", []) or []
)
payload["pending_work_item_ids"] = pending_ids
payload["pending_queue_depth"] = len(pending_ids)
return payload
@staticmethod
def _serialize_role_session(session: DelegationRoleSession) -> dict[str, Any]:
session.status = normalize_role_runtime_status(
session.status,
session.focused_work_item_id,
)
if session.status == "idle":
session.focused_work_item_id = ""
payload = asdict(session)
payload["created_at"] = session.created_at.isoformat()
payload["updated_at"] = session.updated_at.isoformat()
return payload
@staticmethod
def _task_scope_ids(tasks: list[Task]) -> list[str]:
scope: list[str] = []
seen: set[str] = set()
for task in tasks:
for candidate in [str(task.id).strip(), *[str(item).strip() for item in list(task.metadata.get("execution_task_ids", []) or [])]]:
if candidate and candidate not in seen:
seen.add(candidate)
scope.append(candidate)
return scope
@staticmethod
def _task_summary(task: Task, result: TaskResult | None) -> str:
summary = str(task.metadata.get("work_item_summary", "") or "").strip()
if summary:
return summary
if result is not None and result.content:
return str(result.content).strip()
if isinstance(task.result, dict) and task.result.get("content"):
return str(task.result.get("content", "")).strip()
return ""
@staticmethod
def _runtime_state_from_result(result: TaskResult) -> dict[str, Any]:
artifacts = dict(result.artifacts or {})
runtime_session_id = str(artifacts.get("runtime_session_id", "") or "").strip()
if not runtime_session_id:
return {}
return {
"runtime_session_id": runtime_session_id,
"resume_cursor": artifacts.get("resume_cursor"),
"active_subagents": list(artifacts.get("active_subagents", []) or []),
"permission_requests": list(artifacts.get("permission_requests", []) or []),
"compaction_boundaries": list(artifacts.get("compaction_boundaries", []) or []),
"compaction_records": list(artifacts.get("compaction_records", artifacts.get("compaction_boundaries", [])) or []),
"task_ledger": list(artifacts.get("task_ledger", []) or []),
"prefetch_hits": list(artifacts.get("prefetch_hits", []) or []),
"verification": dict(artifacts.get("verification", {}) or {}),
"verification_evidence": dict(artifacts.get("verification_evidence", {}) or {}),
"verification_verdict": str(artifacts.get("verification_verdict", "") or "").strip(),
"artifact_manifest": list(artifacts.get("artifact_manifest", []) or []),
"resume_state": dict(artifacts.get("resume_state", {}) or {}),
"worktree_path": str(artifacts.get("worktree_path", "") or "").strip(),
}
@staticmethod
def _adapter_state_from_result(task: Task, result: TaskResult | None) -> dict[str, Any]:
if result is None:
return {}
artifacts = dict(result.artifacts or {})
state: dict[str, Any] = {}
session_scope_id = task_session_scope_id(task)
if session_scope_id:
state["session_scope_id"] = session_scope_id
selected_execution_agent = str(
task.metadata.get("selected_execution_agent", "")
or task.assigned_external_agent
or ""
).strip()
if selected_execution_agent:
state["selected_execution_agent"] = selected_execution_agent
resume_agent_type = str(
artifacts.get("agent")
or artifacts.get("resume_agent_type")
or task.metadata.get("external_resume_agent_type", "")
or ""
).strip()
external_resume_session_id = str(
task.metadata.get("external_resume_session_id", "")
or artifacts.get("resume_session_id", "")
or artifacts.get("provider_session_id", "")
or artifacts.get("resume_session_token", "")
or ""
).strip()
if (
external_resume_session_id
and result.status == TaskStatus.DONE
and resume_agent_type
):
state["external_resume_session_id"] = external_resume_session_id
state["external_resume_agent_type"] = resume_agent_type
state[resume_agent_type] = {
"resume_session_id": str(artifacts.get("resume_session_id", "") or external_resume_session_id).strip(),
"provider_session_id": str(artifacts.get("provider_session_id", "") or "").strip(),
"updated_at": datetime.now().isoformat(),
"last_task_id": str(task.id or "").strip(),
"last_project_id": str(task.project_id or "").strip(),
}
if session_scope_id:
state["external_resume_session_scope_id"] = session_scope_id
resume_source_session = str(artifacts.get("resume_source_session", "") or "").strip()
if resume_source_session:
state["resume_source_session"] = resume_source_session
latest_external_status = str(artifacts.get("status", "") or "").strip()
if latest_external_status:
state["latest_external_status"] = latest_external_status
return state