fix(allocator): pre-rank exclusions and candidates_json transport (#776)
Expose exclude_issue_numbers on gitea_allocate_next_work, remove excluded numbers before ranking, normalize decoded-list and JSON-string candidates_json fail-closed, and return candidate-set fingerprints for dry-run/apply CAS. Same-owner leases on excluded issues surface a structured resume/release blocker. Closes #776 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
+61
-39
@@ -19202,7 +19202,9 @@ def gitea_allocate_next_work(
|
||||
repo: str | None = None,
|
||||
include_issues: bool = True,
|
||||
include_prs: bool = True,
|
||||
candidates_json: str | None = None,
|
||||
candidates_json: Any = None,
|
||||
exclude_issue_numbers: list[int] | None = None,
|
||||
expected_candidate_set_fingerprint: str | None = None,
|
||||
limit: int = 50,
|
||||
) -> dict:
|
||||
"""Controller-owned next-work allocator using the #613 control-plane DB (#600).
|
||||
@@ -19216,10 +19218,18 @@ def gitea_allocate_next_work(
|
||||
leases as the coordination source).
|
||||
|
||||
Outcomes include: ``assigned_work``, ``preview``, ``wait``,
|
||||
``blocked_by_terminal_path``, ``no_safe_work``, ``role_ineligible``.
|
||||
``blocked_by_terminal_path``, ``no_safe_work``, ``role_ineligible``,
|
||||
``blocked_by_excluded_own_lease``, ``candidate_set_drift``.
|
||||
|
||||
*candidates_json* may inject a JSON list of candidate dicts (tests /
|
||||
controller overrides). When omitted, open issues/PRs are loaded from Gitea.
|
||||
*candidates_json* may inject a candidate list as an already-decoded list
|
||||
(native MCP transport) or a JSON string (backward compatible). When
|
||||
omitted, open issues/PRs are loaded from Gitea.
|
||||
|
||||
*exclude_issue_numbers* (#776): typed list of issue/PR numbers removed
|
||||
before ranking. Omitted/empty retains prior behavior.
|
||||
|
||||
*expected_candidate_set_fingerprint* (#776 AC4): optional CAS pin from a
|
||||
prior dry-run; apply fails closed on material candidate-set drift.
|
||||
|
||||
#612 remains downstream: raw monitoring incidents are never candidates.
|
||||
"""
|
||||
@@ -19273,15 +19283,12 @@ def gitea_allocate_next_work(
|
||||
|
||||
inv_reasons: list[str] = []
|
||||
candidates: list[Any] = []
|
||||
if candidates_json:
|
||||
if candidates_json is not None and candidates_json != "":
|
||||
try:
|
||||
raw = json.loads(candidates_json)
|
||||
if not isinstance(raw, list):
|
||||
raise ValueError("candidates_json must be a JSON list")
|
||||
for item in raw:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
candidates.append(allocator_service.candidate_from_dict(item))
|
||||
# #776 AC3: accept decoded list or JSON string; reject malformed.
|
||||
candidates = allocator_service.normalize_candidates_payload(
|
||||
candidates_json
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {
|
||||
"success": False,
|
||||
@@ -19324,24 +19331,40 @@ def gitea_allocate_next_work(
|
||||
sid = (session_id or "").strip() or (
|
||||
f"{profile_name or 'session'}-{os.getpid()}-{uuid.uuid4().hex[:8]}"
|
||||
)
|
||||
result = allocator_service.allocate_next_work(
|
||||
db,
|
||||
session_id=sid,
|
||||
role=role_in,
|
||||
remote=remote if remote in REMOTES else remote,
|
||||
org=o,
|
||||
repo=r,
|
||||
candidates=candidates,
|
||||
apply=bool(apply),
|
||||
profile_name=profile_name,
|
||||
username=username,
|
||||
controller_instance_id=allocator_service.resolve_controller_instance_id(),
|
||||
)
|
||||
try:
|
||||
result = allocator_service.allocate_next_work(
|
||||
db,
|
||||
session_id=sid,
|
||||
role=role_in,
|
||||
remote=remote if remote in REMOTES else remote,
|
||||
org=o,
|
||||
repo=r,
|
||||
candidates=candidates,
|
||||
apply=bool(apply),
|
||||
profile_name=profile_name,
|
||||
username=username,
|
||||
controller_instance_id=allocator_service.resolve_controller_instance_id(),
|
||||
exclude_issue_numbers=exclude_issue_numbers,
|
||||
expected_candidate_set_fingerprint=expected_candidate_set_fingerprint,
|
||||
)
|
||||
except ValueError as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"outcome": allocator_service.OUTCOME_NO_SAFE,
|
||||
"apply": bool(apply),
|
||||
"reasons": [
|
||||
f"invalid allocator input: {_redact(str(exc))} (fail closed, #776)"
|
||||
],
|
||||
"assignment": None,
|
||||
"substrate": "control_plane_db",
|
||||
}
|
||||
if inv_reasons:
|
||||
result.setdefault("inventory_warnings", inv_reasons)
|
||||
result["candidate_count"] = len(candidates)
|
||||
result["inventory_source"] = (
|
||||
"candidates_json" if candidates_json else "gitea_live"
|
||||
"candidates_json"
|
||||
if candidates_json is not None and candidates_json != ""
|
||||
else "gitea_live"
|
||||
)
|
||||
result["inventory_complete"] = True
|
||||
result["selection_policy"] = allocator_service.SELECTION_POLICY
|
||||
@@ -19435,7 +19458,7 @@ def gitea_workflow_dashboard(
|
||||
include_issues: bool = True,
|
||||
include_prs: bool = True,
|
||||
include_non_active_leases: bool = True,
|
||||
candidates_json: str | None = None,
|
||||
candidates_json: Any = None,
|
||||
limit: int = 100,
|
||||
) -> dict:
|
||||
"""Read-only MCP workflow dashboard: queues, leases, blockers, next safe action (#605).
|
||||
@@ -19444,9 +19467,10 @@ def gitea_workflow_dashboard(
|
||||
still requires ``gitea_allocate_next_work``. Never presents blocked or
|
||||
terminal-locked items as safe next work.
|
||||
|
||||
*candidates_json* may inject a JSON list of candidate dicts (tests /
|
||||
offline fixtures). When omitted, open issues/PRs are loaded from Gitea.
|
||||
Incomplete live inventory fails closed (no safe suggestions).
|
||||
*candidates_json* may inject a candidate list as an already-decoded list
|
||||
or JSON string (#776 transport parity). When omitted, open issues/PRs are
|
||||
loaded from Gitea. Incomplete live inventory fails closed (no safe
|
||||
suggestions).
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
@@ -19471,15 +19495,11 @@ def gitea_workflow_dashboard(
|
||||
inv_reasons: list[str] = []
|
||||
candidates: list[Any] = []
|
||||
inventory_complete = True
|
||||
if candidates_json:
|
||||
if candidates_json is not None and candidates_json != "":
|
||||
try:
|
||||
raw = json.loads(candidates_json)
|
||||
if not isinstance(raw, list):
|
||||
raise ValueError("candidates_json must be a JSON list")
|
||||
for item in raw:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
candidates.append(allocator_service.candidate_from_dict(item))
|
||||
candidates = allocator_service.normalize_candidates_payload(
|
||||
candidates_json
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {
|
||||
"success": False,
|
||||
@@ -19570,7 +19590,9 @@ def gitea_workflow_dashboard(
|
||||
)
|
||||
payload = snap.as_dict()
|
||||
payload["inventory_source"] = (
|
||||
"candidates_json" if candidates_json else "gitea_live"
|
||||
"candidates_json"
|
||||
if candidates_json is not None and candidates_json != ""
|
||||
else "gitea_live"
|
||||
)
|
||||
payload["limit_applies_to"] = "lease_listing_only"
|
||||
return payload
|
||||
|
||||
Reference in New Issue
Block a user