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 <noreply@anthropic.com>
This commit is contained in:
@@ -6363,6 +6363,81 @@ class OPCEngine:
|
|||||||
f"company runtime resume checkpoint has no task identity snapshot for {task.id}"
|
f"company runtime resume checkpoint has no task identity snapshot for {task.id}"
|
||||||
)
|
)
|
||||||
identity = self._checkpoint_task_execution_identity(task_snapshot)
|
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(
|
expected_role_session_id = str(
|
||||||
identity.get("role_runtime_session_id", "") or ""
|
identity.get("role_runtime_session_id", "") or ""
|
||||||
).strip()
|
).strip()
|
||||||
|
|||||||
Reference in New Issue
Block a user