From d380684b7814f4ed0088a26f74d9db953f48f4d4 Mon Sep 17 00:00:00 2001 From: LZH-YS1998 Date: Tue, 21 Jul 2026 11:18:33 +0800 Subject: [PATCH] fix(engine): fail resume closed when the pinned external agent is unavailable (#10) A company-runtime resume checkpoint pins each task to one exact execution backend. When that pinned external agent is disabled in the config (issue #10: all external agents off), every resume claimed the card, transitioned it to RUNNING, and crashed deterministically in the dispatch-time selector ("company runtime resume requires unavailable external agent"), feeding the restart -> resume -> crash loop. Gate availability at resume preparation instead, before any pin or claim: fail the single work item closed (FAILED + diagnostic blocked_reason + progress note) while the rest of the organization resumes normally. attempt_seq stays 0 - no dispatch attempt is burned. A missing adapter registry means availability is unknown, so the gate fails open and leaves the decision to the dispatch-time guard. Co-Authored-By: Claude Fable 5 --- opc/engine.py | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/opc/engine.py b/opc/engine.py index 8e7fb87..fef3890 100644 --- a/opc/engine.py +++ b/opc/engine.py @@ -6363,6 +6363,81 @@ class OPCEngine: f"company runtime resume checkpoint has no task identity snapshot for {task.id}" ) identity = self._checkpoint_task_execution_identity(task_snapshot) + # Resume availability gate: the checkpoint pins one exact + # execution backend, so a pinned external agent that is disabled + # (or whose binary vanished) makes this work item deterministically + # un-runnable — every dispatch would claim, transition to RUNNING, + # and crash on the selector's availability check. Fail the item + # closed HERE, before any pin/claim, with a visible diagnostic; + # the rest of the organization resumes normally and the failed- + # child settlement machinery hands the card to manager triage. + pinned_agent = str( + identity.get("selected_execution_agent", "") or "native" + ).strip() or "native" + work_item_already_terminal = bool( + work_item is not None + and getattr(work_item, "phase", None) in DONE_PHASES + ) + # A missing adapter registry means availability is UNKNOWN (engine + # not fully initialized / delegate context) — fail open and let the + # dispatch-time selector guard decide; only a registry that exists + # and excludes the pinned agent is proof of unavailability. + if ( + pinned_agent != "native" + and not work_item_already_terminal + and self.adapter_registry is not None + and pinned_agent not in self._available_external_agents() + ): + diagnostic = ( + f"Cannot resume work item: its execution is pinned to external agent " + f"'{pinned_agent}', which is currently disabled or unavailable. " + f"Re-enable '{pinned_agent}' in the config and restart to run it, " + "or re-issue the work as a new request." + ) + task.metadata = dict(task.metadata or {}) + for key in _COMPANY_RUNTIME_CONTROL_METADATA_KEYS: + task.metadata.pop(key, None) + progress = list(task.metadata.get("progress_log", []) or []) + progress.append(diagnostic) + task.metadata["progress_log"] = progress[-20:] + task.metadata["resume_unavailable_external_agent"] = pinned_agent + existing_result = dict(task.result or {}) + existing_result["content"] = diagnostic + task.result = existing_result + await self._fail_task_via_phase( + task, + reason="resume_external_agent_unavailable", + ) + if work_item_id and callable(update_work_item): + try: + await update_work_item( + work_item_id, + blocked_reason=diagnostic, + metadata_updates={"dispatch_hold": ""}, + ) + except Exception: + logger.opt(exception=True).debug( + "company runtime resume: blocked_reason write failed for {}", + work_item_id, + ) + if self.on_progress: + try: + await self.on_progress( + f"[Company:{projection_id_for_task(task) or task.title}] {diagnostic}" + ) + except Exception: + logger.opt(exception=True).debug( + "company runtime resume: unavailable-agent progress emit failed" + ) + logger.warning( + "company runtime resume: failed work item {} closed — pinned external " + "agent {!r} unavailable", + work_item_id or task.id, + pinned_agent, + ) + fresh = await self.store.get_task(task.id) + refreshed.append(fresh or task) + continue expected_role_session_id = str( identity.get("role_runtime_session_id", "") or "" ).strip()