Files
cleveragents-core/tools/_implementer_label_state.py
T
drew 0bc734c020 style: ruff format the controller-state-machine branch (288 files)
Applies `ruff format` to the accumulated formatting debt on this branch.
Formatting-only — no behavioral changes. Required for CI/lint's format
gate (`nox -s format -- --check`), which the branch was failing on 288
tracked files that drifted from ruff's canonical style.

In-progress WIP files are intentionally excluded so this commit stays a
clean formatting-only diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 00:09:17 -04:00

183 lines
7.1 KiB
Python

"""Attempt-tier label mutation for the implementer dispatcher.
Owns the ``auto/last-attempt-tier-{0,1,2}`` label set that the
in-cycle escalation loop uses for both operator visibility AND
cross-cycle resumption seeding.
**Dual role (updated 2026-05-12).** Earlier iterations of the plan
described these labels as "observability-only" with crash recovery
restarting at Tier 0. The shipped design uses them more
ambitiously: in addition to being mutated for the Forgejo UI, the
dispatcher reads them at cycle start via
:func:`dispatch_implementer._read_start_tier_from_labels` to seed
``start_tier = min(labeled_tier + 1, max_tier)``. A PR that
exhausted Tier 1 in a previous cycle resumes at Tier 2 instead of
re-running Tier 0, saving a worker session per crash-recovery
cycle. See
``docs/development/implementer-in-cycle-escalation-plan.md`` §
"Cross-cycle resumption".
The labels are NOT a strict source of truth — a stale label
(operator-set, crash before clear) can mis-seed the next cycle by
one tier. The cost is bounded: worst case is one extra higher-tier
attempt, vs. the savings of skipping a known-failed tier.
Two operations:
- :func:`apply_attempt_label` — set the label for the just-launched
tier, clear the labels for any other tier. Called at every tier
boundary so the Forgejo UI shows "this PR is currently being
worked on at tier N" while the worker session is in flight.
- :func:`clear_attempt_labels` — remove all three labels. Called
when the cycle ends successfully (the labels are stale after
resolution) and as a safety sweep on cycle exit paths where the
dispatcher may have left a stale tier label behind.
Both are **best-effort**: if a label has not been provisioned in
Forgejo, the underlying HTTP call returns a 404 and
:func:`_claim_runtime._add_label` / ``_remove_label`` already return
``False`` (their helpers swallow the label-not-found state). We log
WARNING and proceed; escalation continues without label
observability.
"""
from __future__ import annotations
import logging
import sys
from pathlib import Path
from typing import Any
_TOOLS_DIR = str(Path(__file__).resolve().parent)
if _TOOLS_DIR not in sys.path:
sys.path.insert(0, _TOOLS_DIR)
from _loader import ( # noqa: E402 type: ignore[import-not-found]
load_sibling as _load_sibling,
)
_claim_runtime = _load_sibling("_claim_runtime", "_claim_runtime.py")
_logger = logging.getLogger("implementer_label_state")
# The four labels owned by the in-cycle escalation loop. Kept here
# rather than in _claim_runtime so the constant lives next to the
# code that mutates it.
#
# 2026-05-16 expansion: tier -1 (``tier-min``) was added after the
# run-15 doom-spiral inspection traced repeated cycles re-picking
# tier-min on PR #30 to the absence of a tier-min label slot. With
# no label to mark "this PR already failed at tier-min", every
# fresh dispatcher cycle treated the PR as a true first attempt and
# the estimator was free to re-pick tier-min. Adding the label
# closes the loop for the deterministic-walk path
# (``dispatch_implementer._read_start_tier_from_labels``).
#
# The name suffix is ``-min`` not ``--1`` because Forgejo label
# names with double hyphens are ugly in the UI and the parsing
# logic in the dispatcher handles the ``-min`` literal explicitly
# (see ``_read_start_tier_from_labels``).
ATTEMPT_TIER_LABELS_BY_TIER: dict[int, str] = {
-1: "auto/last-attempt-tier-min",
0: "auto/last-attempt-tier-0",
1: "auto/last-attempt-tier-1",
2: "auto/last-attempt-tier-2",
}
# Convenience tuple — preserved for callers that iterate (e.g.
# ``apply_attempt_label`` clearing siblings, ``clear_attempt_labels``
# wiping the lot). Order is "lowest tier first" so iteration is
# predictable.
ATTEMPT_TIER_LABELS: tuple[str, ...] = tuple(
ATTEMPT_TIER_LABELS_BY_TIER[t] for t in sorted(ATTEMPT_TIER_LABELS_BY_TIER)
)
# Reverse lookup. Used by the dispatcher's label-read path to
# validate parsed integers and by tests for round-trip checks.
TIER_BY_LABEL: dict[str, int] = {v: k for k, v in ATTEMPT_TIER_LABELS_BY_TIER.items()}
def _label_for_tier(tier: int) -> str:
"""Map a tier integer to its label name. Raises ``ValueError``
on out-of-range — callers should never reach here with an
invalid tier (the escalation loop's max_tier guard catches it
earlier).
Accepts the full configured range including -1 (``tier-min``).
"""
if tier not in ATTEMPT_TIER_LABELS_BY_TIER:
valid = sorted(ATTEMPT_TIER_LABELS_BY_TIER)
raise ValueError(f"tier {tier} out of range; valid tiers are {valid}")
return ATTEMPT_TIER_LABELS_BY_TIER[tier]
def apply_attempt_label(cfg: Any, pr_number: int, tier: int) -> dict[str, Any]:
"""Apply ``auto/last-attempt-tier-{tier}`` and clear the other
tier labels from the PR.
Side-effect-only — returns a small status dict for the cycle
archive so an operator can verify the mutation fired:
.. code-block:: python
{
"applied": "auto/last-attempt-tier-1",
"cleared": ["auto/last-attempt-tier-0"],
"skipped_provisioning_missing": False,
}
On label-not-provisioned (Forgejo returns 404 from the label
lookup), the dispatcher logs WARNING and proceeds — escalation
is unaffected, but operators won't see the per-tier label in
the Forgejo UI until the label provisioner is run. The
``skipped_provisioning_missing`` flag in the return value gives
the cycle archive a clean signal an operator can grep for.
"""
target_label = _label_for_tier(tier)
applied = _claim_runtime._add_label(pr_number, target_label, cfg)
if not applied:
_logger.warning(
"apply_attempt_label: failed to add %r to PR #%s "
"(label may not be provisioned in Forgejo — run the "
"label provisioner; escalation continues without "
"per-tier label observability)",
target_label,
pr_number,
)
cleared: list[str] = []
for label in ATTEMPT_TIER_LABELS:
if label == target_label:
continue
# Best-effort: _remove_label returns False on
# label-not-provisioned. We don't log the soft fail — it's
# the symmetric case of "add" failing and the same warning
# above already surfaces the root cause.
if _claim_runtime._remove_label(pr_number, label, cfg):
cleared.append(label)
return {
"applied": target_label if applied else None,
"cleared": cleared,
"skipped_provisioning_missing": not applied,
}
def clear_attempt_labels(cfg: Any, pr_number: int) -> dict[str, Any]:
"""Remove all three ``auto/last-attempt-tier-*`` labels from the
PR.
Called on successful cycle exit (the stale labels are no longer
informative — the PR's status moves on) and as a safety sweep
on END_CYCLE / EXHAUSTED paths.
Returns ``{"cleared": [...]}`` — the list of labels actually
removed (vs. labels that were already absent). Best-effort:
failures log WARNING + proceed.
"""
cleared: list[str] = []
for label in ATTEMPT_TIER_LABELS:
if _claim_runtime._remove_label(pr_number, label, cfg):
cleared.append(label)
return {"cleared": cleared}