fix: unify company runtime recovery lifecycle

This commit is contained in:
LZH-YS1998
2026-07-14 14:35:43 +08:00
parent 5e02364eb4
commit b8202bbe9e
56 changed files with 8753 additions and 3542 deletions
+71 -80
View File
@@ -39,6 +39,11 @@ from opc.layer2_organization.phase import (
should_hide_work_item_from_company_kanban,
verdict,
)
from opc.layer2_organization.company_runtime_identity import (
ACTIVE_COMPANY_RUNTIME_CHECKPOINT_STATUSES,
COMPANY_RUNTIME_CHECKPOINT_TYPES,
build_company_runtime_identity_index,
)
from opc.layer2_organization.work_item_context_view import WorkItemContextView
from opc.layer2_organization.work_item_identity import (
WORK_ITEM_PROJECTION_ID_KEY,
@@ -1503,6 +1508,11 @@ def _primary_session_tasks_by_session_id(
*,
task_meta_map: dict[str, dict[str, Any]] | None = None,
) -> tuple[dict[str, Any], list[str]]:
identity_index = build_company_runtime_identity_index(tasks)
company_identities = {
identity.runtime_session_id: identity
for identity in identity_index.identities
}
primary_tasks_by_session_id: dict[str, Any] = {}
ordered_session_ids: list[str] = []
for task in tasks:
@@ -1513,6 +1523,19 @@ def _primary_session_tasks_by_session_id(
if bool(task_meta.get("review_task", False)):
continue
session_id = str(getattr(task, "session_id", "") or "").strip()
company_identity = company_identities.get(session_id)
if company_identity is not None:
anchor = identity_index.task(company_identity.ui_anchor_task_id)
if anchor is not None and session_id not in primary_tasks_by_session_id:
primary_tasks_by_session_id[session_id] = anchor
ordered_session_ids.append(session_id)
if anchor is not None:
# A shared final-decider/work-item Task must never replace a
# pure UI anchor that owns the same session id.
continue
# Without a pure anchor this scope has no primary chat container.
# Never synthesize one from a role/work-item Task.
continue
if not session_id or _task_parent_session_link(task, task_meta):
continue
current = primary_tasks_by_session_id.get(session_id)
@@ -1530,31 +1553,16 @@ def _primary_session_tasks_by_session_id(
return primary_tasks_by_session_id, ordered_session_ids
def _shared_role_identity_tasks_by_session_id(
def _company_config_source_tasks_by_session_id(
tasks: list[Any],
*,
task_meta_map: dict[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
identity_tasks_by_session_id: dict[str, Any] = {}
for task in tasks:
task_id = str(getattr(task, "id", "") or "").strip()
task_meta = (
task_meta_map.get(task_id, {}) if task_meta_map is not None and task_id else _task_metadata(task)
)
session_id = _shared_role_session_key(task, task_meta)
if not session_id:
continue
current = identity_tasks_by_session_id.get(session_id)
if current is None:
identity_tasks_by_session_id[session_id] = task
continue
current_id = str(getattr(current, "id", "") or "").strip()
current_meta = (
task_meta_map.get(current_id, {}) if task_meta_map is not None and current_id else _task_metadata(current)
)
if _session_representative_rank(task, task_meta) > _session_representative_rank(current, current_meta):
identity_tasks_by_session_id[session_id] = task
return identity_tasks_by_session_id
identity_index = build_company_runtime_identity_index(tasks)
return {
identity.runtime_session_id: task
for identity in identity_index.identities
if identity.config_source_task_id
and (task := identity_index.task(identity.config_source_task_id)) is not None
}
async def build_company_kanban_projection(
@@ -2594,31 +2602,7 @@ async def _build_company_runtime_control_by_task(
if not store:
return {}
parent_task_by_session: dict[str, str] = {}
tasks_by_parent_session: dict[str, list[Any]] = {}
for task in tasks:
metadata = dict(getattr(task, "metadata", {}) or {})
mode = str(metadata.get("mode", "") or metadata.get("exec_mode", "") or "").strip().lower()
is_company_runtime_task = bool(
mode in {"company", "org", "custom"}
or str(getattr(task, "parent_session_id", "") or "").strip()
or metadata.get("company_profile")
or metadata.get("company_work_item_plan")
or metadata.get("work_item_runtime")
or metadata.get("work_item_projection_id")
)
if not is_company_runtime_task:
continue
session_id = str(getattr(task, "session_id", "") or "").strip()
parent_session_id = str(getattr(task, "parent_session_id", "") or "").strip()
task_id = str(getattr(task, "id", "") or "").strip()
if session_id and not parent_session_id:
parent_task_by_session[session_id] = task_id
runtime_parent_session_id = parent_session_id or session_id
if runtime_parent_session_id:
tasks_by_parent_session.setdefault(runtime_parent_session_id, []).append(task)
checkpoints_by_session: dict[str, Any] = {}
checkpoints: list[Any] = []
getter = getattr(store, "get_execution_checkpoints", None)
if not callable(getter):
getter = getattr(store, "get_pending_checkpoints", None)
@@ -2626,29 +2610,25 @@ async def _build_company_runtime_control_by_task(
try:
kwargs = {
"project_id": project_id,
"checkpoint_types": ["company_runtime_suspended", "company_runtime_interrupted"],
"checkpoint_types": sorted(COMPANY_RUNTIME_CHECKPOINT_TYPES),
}
if getattr(getter, "__name__", "") == "get_execution_checkpoints":
kwargs["statuses"] = ["pending", "resuming"]
kwargs["statuses"] = sorted(ACTIVE_COMPANY_RUNTIME_CHECKPOINT_STATUSES)
checkpoints = await getter(**kwargs)
for checkpoint in checkpoints:
sid = str(getattr(checkpoint, "session_id", "") or "").strip()
if sid and sid not in checkpoints_by_session:
checkpoints_by_session[sid] = checkpoint
except Exception:
logger.opt(exception=True).debug("snapshot: failed to load company runtime checkpoints")
checkpoints = []
identity_index = build_company_runtime_identity_index(tasks, checkpoints)
result: dict[str, dict[str, Any]] = {}
for parent_session_id, group in tasks_by_parent_session.items():
checkpoint = checkpoints_by_session.get(parent_session_id)
parent_task_id = parent_task_by_session.get(parent_session_id, "")
if not parent_task_id:
for task in group:
if not str(getattr(task, "parent_session_id", "") or "").strip():
parent_task_id = str(getattr(task, "id", "") or "").strip()
break
if not parent_task_id and group:
parent_task_id = str(getattr(group[0], "id", "") or "").strip()
for identity in identity_index.identities:
group = [
task
for task_id in identity.runtime_task_ids
if (task := identity_index.task(task_id)) is not None
]
checkpoint = identity.checkpoint
def _task_status_value(task: Any) -> str:
status = getattr(task, "status", "")
@@ -2660,10 +2640,19 @@ async def _build_company_runtime_control_by_task(
task for task in group
if _task_status_value(task) not in {"done", "failed", "cancelled"}
]
has_running_task = any(
_task_status_value(task) == "running"
for task in non_terminal_group
)
# Persisted RUNNING is only a projection. The controller-local
# execution registry is the sole proof that this process still owns a
# coroutine capable of monitoring and persisting the run.
runtime_is_live = getattr(engine, "_task_runtime_is_live", None)
has_running_task = False
if callable(runtime_is_live):
for task in non_terminal_group:
live_result = runtime_is_live(task)
if inspect.isawaitable(live_result):
live_result = await live_result
if live_result is True:
has_running_task = True
break
any_stop_in_progress = any(
str((getattr(task, "metadata", {}) or {}).get("company_runtime_stop_state", "") or "").strip()
in {"suspending", "suspended", "resuming_after_suspending"}
@@ -2708,8 +2697,7 @@ async def _build_company_runtime_control_by_task(
"runtime_control_state": state,
"can_stop": state == "running",
"can_resume": state == "suspended",
"resume_parent_task_id": parent_task_id,
"resume_parent_session_id": parent_session_id,
"resume_parent_session_id": identity.runtime_session_id,
"pending_runtime_checkpoint_id": pending_checkpoint_id,
"stop_intent_id": str(checkpoint_payload.get("stop_intent_id", "") or ""),
}
@@ -3014,9 +3002,8 @@ async def build_project_index_sync(
session_tasks,
task_meta_map=task_meta_map,
)
shared_identity_tasks_by_session_id = _shared_role_identity_tasks_by_session_id(
company_config_tasks_by_session_id = _company_config_source_tasks_by_session_id(
session_tasks,
task_meta_map=task_meta_map,
)
child_tasks_by_parent: dict[str, list[Any]] = {}
for task in session_tasks:
@@ -3087,11 +3074,12 @@ async def build_project_index_sync(
representative_task = primary_tasks_by_session_id.get(session_id)
representative_task_id = str(getattr(representative_task, "id", "") or "").strip()
shared_session_id = _shared_role_session_key(t, t_meta)
if shared_session_id and representative_task_id and representative_task_id != task_id:
continue
if shared_session_id:
if not representative_task_id or representative_task_id != task_id:
continue
identity_task = t
identity_meta = t_meta
shared_identity_task = shared_identity_tasks_by_session_id.get(session_id)
shared_identity_task = company_config_tasks_by_session_id.get(session_id)
shared_identity_task_id = str(getattr(shared_identity_task, "id", "") or "").strip()
if shared_identity_task_id and shared_identity_task_id != task_id:
identity_task = shared_identity_task
@@ -3569,9 +3557,8 @@ async def build_collab_sync(
session_tasks,
task_meta_map=task_meta_map,
)
shared_identity_tasks_by_session_id = _shared_role_identity_tasks_by_session_id(
company_config_tasks_by_session_id = _company_config_source_tasks_by_session_id(
session_tasks,
task_meta_map=task_meta_map,
)
child_tasks_by_parent: dict[str, list[Any]] = {}
for task in session_tasks:
@@ -3616,11 +3603,15 @@ async def build_collab_sync(
representative_task = primary_tasks_by_session_id.get(session_id)
representative_task_id = str(getattr(representative_task, "id", "") or "").strip()
shared_session_id = _shared_role_session_key(t, t_meta)
if shared_session_id and representative_task_id and representative_task_id != str(getattr(t, "id", "") or "").strip():
continue
if shared_session_id:
if (
not representative_task_id
or representative_task_id != str(getattr(t, "id", "") or "").strip()
):
continue
identity_task = t
identity_meta = t_meta
shared_identity_task = shared_identity_tasks_by_session_id.get(session_id)
shared_identity_task = company_config_tasks_by_session_id.get(session_id)
shared_identity_task_id = str(getattr(shared_identity_task, "id", "") or "").strip()
if shared_identity_task_id and shared_identity_task_id != str(getattr(t, "id", "") or "").strip():
identity_task = shared_identity_task