Files
submmit/modelhub_submmit_api/outcome_tracker.py

701 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta
import os
from pathlib import Path
from typing import Any
from architecture_compatibility import (
DEFAULT_ARCHITECTURE_BLOCK_TTL_DAYS,
EXPLICIT_ARCHITECTURE_FAILURE_CATEGORY,
EXPLICIT_ARCHITECTURE_FAILURE_REASON,
architecture_compatibility_key,
architecture_profile,
)
from common import append_jsonl, parse_datetime, read_jsonl, update_jsonl, utc_now
from failure_log_inspector import fetch_and_classify_failure_log
from history_stats import classify_failure, is_failure, is_success
from llm_classifier import LLMAssistedClassifier
from modelhub_client import ModelHubClient, ModelHubClientPool
DEFAULT_OUTCOMES_PATH = Path("outcomes/submissions.jsonl")
FAILURE_ENRICHMENT_LIMIT = 40
FAILURE_ENRICHMENT_WORKERS = 4
FAILURE_ENRICHMENT_MAX_ATTEMPTS = 3
def _now_iso() -> str:
return utc_now().isoformat()
class OutcomeTracker:
def __init__(self, path: Path | str) -> None:
self.path = Path(path)
self._records: list[dict[str, Any]] = []
self._by_task_id: dict[str, dict[str, Any]] = {}
self._by_model_gpu: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
self._failed_model_gpus: dict[tuple[str, str], datetime] = {}
self._failure_llm_classifier: LLMAssistedClassifier | None = None
self._records = read_jsonl(self.path)
self._rebuild_indexes()
last_sync_times = [
parse_datetime(record.get("lastSyncTime"))
for record in self._records
if record.get("lastSyncTime")
]
self._last_sync_time: datetime = max(last_sync_times) if last_sync_times else utc_now() - timedelta(days=7)
def set_failure_llm_classifier(self, classifier: LLMAssistedClassifier | None) -> None:
self._failure_llm_classifier = classifier
def _rebuild_indexes(self) -> None:
self._by_task_id.clear()
self._by_model_gpu.clear()
for record in self._records:
task_id = record.get("taskId")
if task_id:
self._by_task_id[str(task_id)] = record
model_id = record.get("modelId") or ""
target_gpu = record.get("targetGpu") or ""
self._by_model_gpu[(model_id, target_gpu)].append(record)
self._rebuild_failed_index()
def record_submission(
self,
model_id: str,
target_gpu: str,
framework: str,
task_type: str,
task_id: str | None,
submit_time: str,
model_profile: dict[str, Any] | None = None,
) -> None:
record: dict[str, Any] = {
"modelId": model_id,
"targetGpu": target_gpu,
"framework": framework,
"taskType": task_type,
"taskId": task_id,
"submitTime": submit_time,
"lastSyncTime": None,
"status": "pending",
"verifyResult": None,
"outcome": "pending",
"failReason": None,
"modelProfile": dict(model_profile or {}),
}
self._records.append(record)
if task_id:
self._by_task_id[task_id] = record
self._by_model_gpu[(model_id, target_gpu)].append(record)
append_jsonl(self.path, record)
def mark_policy_cancellations(self, decisions: list[dict[str, Any]]) -> int:
"""Persist intentional task stops so they never become failure evidence."""
marked = 0
marked_at = _now_iso()
for decision in decisions:
task_id_value = decision.get("taskId")
if task_id_value is None:
continue
task_id = str(task_id_value)
existing = self._by_task_id.get(task_id)
if existing is not None and existing.get("outcome") in {"success", "failed"}:
continue
reasons = list(decision.get("cleanupReasons") or [decision.get("reason")])
reasons = [str(reason) for reason in reasons if reason]
if existing is None:
existing = {
"modelId": decision.get("modelId") or "",
"targetGpu": decision.get("gpuType") or decision.get("targetGpu") or "",
"framework": decision.get("framework") or "",
"taskType": decision.get("taskType") or "",
"taskId": task_id,
"submitTime": decision.get("submitTime") or marked_at,
"verifyResult": None,
}
self._records.append(existing)
self._by_task_id[task_id] = existing
self._by_model_gpu[(existing["modelId"], existing["targetGpu"])].append(existing)
existing.update(
{
"lastSyncTime": marked_at,
"status": "cancellation_requested",
"outcome": "policy_cancelled",
"failReason": None,
"policyCancelled": True,
"policyCancellationReasons": reasons,
"policyCancelledAt": marked_at,
}
)
marked += 1
if marked:
self._rebuild_failed_index()
return marked
def sync_from_api(self, client: ModelHubClient | ModelHubClientPool) -> int:
try:
begin = self._last_sync_time
end = utc_now()
# Use fanout for pools to sync outcomes across all accounts
list_kwargs: dict[str, Any] = {"begin_time": begin, "end_time": end, "page_size": 100, "only_mine": True}
if isinstance(client, ModelHubClientPool):
list_kwargs["_fanout_all"] = True
tasks = client.list_tasks(**list_kwargs)
except Exception:
return 0
updated_count = 0
enrichment_candidates: list[dict[str, Any]] = []
for task in tasks:
task_id = str(task.get("taskId")) if task.get("taskId") is not None else None
if not task_id:
continue
existing = self._by_task_id.get(task_id)
if existing is not None:
if existing.get("outcome") in {"pending", "policy_cancelled"}:
self._update_record_from_task(existing, task)
if existing.get("outcome") == "failed" and existing.get("logCosUrl"):
enrichment_candidates.append(existing)
updated_count += 1
else:
status = str(task.get("status") or "").lower()
if status in {"success", "failed", "error", "cancelled", "completed"}:
record = self._create_record_from_task(task)
self._records.append(record)
self._by_task_id[task_id] = record
model_id = record["modelId"]
target_gpu = record["targetGpu"]
self._by_model_gpu[(model_id, target_gpu)].append(record)
updated_count += 1
# Retry a small bounded set of our own failed submissions. Historical
# tasks without the locally recorded framework/profile are intentionally
# excluded to avoid downloading thousands of old log archives at once.
candidate_ids = {id(record) for record in enrichment_candidates}
for record in self._records:
if len(enrichment_candidates) >= FAILURE_ENRICHMENT_LIMIT:
break
if id(record) in candidate_ids:
continue
if (
record.get("outcome") == "failed"
and record.get("logCosUrl")
and record.get("framework")
and record.get("modelProfile")
and not record.get("failureCategory")
and int(record.get("failureEnrichmentAttempts") or 0) < FAILURE_ENRICHMENT_MAX_ATTEMPTS
):
enrichment_candidates.append(record)
candidate_ids.add(id(record))
enrichment_attempts = self._enrich_failure_records(
enrichment_candidates[:FAILURE_ENRICHMENT_LIMIT]
)
if updated_count or enrichment_attempts:
self._last_sync_time = end
self._rebuild_failed_index()
self.save()
return updated_count
def _enrich_failure_records(self, records: list[dict[str, Any]]) -> int:
if not records:
return 0
def inspect(record: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any] | None, str | None]:
try:
result = fetch_and_classify_failure_log(
str(record["logCosUrl"]),
task_context=record,
llm_classifier=self._failure_llm_classifier,
)
return record, result, None
except Exception as exc:
return record, None, f"{type(exc).__name__}: {exc}"
attempted = 0
with ThreadPoolExecutor(max_workers=min(FAILURE_ENRICHMENT_WORKERS, len(records))) as executor:
futures = [executor.submit(inspect, record) for record in records]
for future in as_completed(futures):
record, result, error = future.result()
attempted += 1
record["failureEnrichmentAttempts"] = int(record.get("failureEnrichmentAttempts") or 0) + 1
if result is None:
record["failureEnrichmentError"] = error
continue
record.update(result)
record["failReason"] = result.get("failureCategory") or record.get("failReason")
record["failureEnrichmentError"] = None
record.pop("logCosUrl", None)
return attempted
def is_model_gpu_failed(
self,
model_id: str,
target_gpu: str,
*,
cooldown_hours: int = 24,
now: datetime | None = None,
) -> bool:
failed_at = self._failed_model_gpus.get((model_id, target_gpu))
if failed_at is None:
return False
now = now or utc_now()
return failed_at >= now - timedelta(hours=max(0, int(cooldown_hours)))
def get_stats_report(self) -> dict[str, Any]:
now_datetime = utc_now()
now = now_datetime.isoformat()
terminal = [r for r in self._records if r.get("outcome") in {"success", "failed"}]
gpu_groups: dict[str, list[dict[str, Any]]] = defaultdict(list)
framework_groups: dict[str, list[dict[str, Any]]] = defaultdict(list)
combo_groups: dict[tuple[str, str, str], list[dict[str, Any]]] = defaultdict(list)
profile_groups: dict[tuple[str, str, str, str, str], list[dict[str, Any]]] = defaultdict(list)
for record in terminal:
gpu = record.get("targetGpu") or "unknown"
fw = record.get("framework") or "unknown"
tt = record.get("taskType") or "unknown"
gpu_groups[gpu].append(record)
framework_groups[f"{fw}"].append(record)
combo_groups[(gpu, fw, tt)].append(record)
profile = record.get("modelProfile") or {}
model_type = str(profile.get("modelType") or "").strip()
if model_type:
quantization = str(profile.get("quantizationMethod") or "none").strip()
profile_groups[(gpu, fw, tt, model_type, quantization)].append(record)
gpu_summaries = {gpu: _summarize(records) for gpu, records in gpu_groups.items()}
framework_summaries = {fw: _summarize(records) for fw, records in framework_groups.items()}
combination_stats = {
f"{gpu}|{fw}|{tt}": {"targetGpu": gpu, "framework": fw, "taskType": tt, **_summarize(records)}
for (gpu, fw, tt), records in combo_groups.items()
}
recent_combination_stats: dict[str, dict[str, Any]] = {}
for (gpu, fw, tt), records in combo_groups.items():
recent = sorted(records, key=_outcome_record_timestamp, reverse=True)[:20]
consecutive_failures = _consecutive_attributable_failures(recent)
consecutive_platform_failures = _consecutive_platform_failures(recent)
last_terminal_at = None
if recent:
last_terminal_at = (
parse_datetime(recent[0].get("lastSyncTime"))
or parse_datetime(recent[0].get("submitTime"))
)
recent_combination_stats[f"{gpu}|{fw}|{tt}"] = {
"targetGpu": gpu,
"framework": fw,
"taskType": tt,
**_summarize(recent),
"consecutiveFailures": consecutive_failures,
"consecutivePlatformFailures": consecutive_platform_failures,
"lastPlatformFailureAt": _latest_platform_failure_at(recent),
"lastTerminalAt": last_terminal_at.isoformat() if last_terminal_at else None,
}
profile_combination_stats: dict[str, dict[str, Any]] = {}
recent_profile_combination_stats: dict[str, dict[str, Any]] = {}
for (gpu, fw, tt, model_type, quantization), records in profile_groups.items():
key = f"{gpu}|{fw}|{tt}|{model_type}|{quantization}"
profile_combination_stats[key] = {
"targetGpu": gpu,
"framework": fw,
"taskType": tt,
"modelType": model_type,
"quantizationMethod": quantization,
**_summarize(records),
}
recent = sorted(records, key=_outcome_record_timestamp, reverse=True)[:20]
consecutive_failures = _consecutive_attributable_failures(recent)
last_terminal_at = None
if recent:
last_terminal_at = (
parse_datetime(recent[0].get("lastSyncTime"))
or parse_datetime(recent[0].get("submitTime"))
)
recent_profile_combination_stats[key] = {
**profile_combination_stats[key],
**_summarize(recent),
"consecutiveFailures": consecutive_failures,
"lastTerminalAt": last_terminal_at.isoformat() if last_terminal_at else None,
}
warnings: list[str] = []
for gpu, summary in gpu_summaries.items():
if summary["decisionTotal"] >= 4 and summary["decisionFailureRate"] >= 0.5:
warnings.append(f"GPU {gpu} 本地统计失败率偏高≥50%),建议重点关注。")
for key, stat in combination_stats.items():
if stat["decisionTotal"] >= 3 and stat["decisionFailureRate"] >= 0.6:
warnings.append(f"组合 {key} 近期失败集中,建议降低该 GPU+框架的提交优先级。")
pending_count = sum(1 for r in self._records if r.get("outcome") == "pending")
policy_cancelled_count = sum(
1 for record in self._records if record.get("outcome") == "policy_cancelled"
)
observed_gpu_memory: dict[str, float] = {}
for record in self._records:
gpu = str(record.get("targetGpu") or "")
try:
memory_gib = float(record.get("failureObservedGpuMemoryGiB"))
except (TypeError, ValueError):
continue
if gpu and 0 < memory_gib <= 1024:
previous = observed_gpu_memory.get(gpu)
observed_gpu_memory[gpu] = min(previous, memory_gib) if previous else memory_gib
architecture_block_ttl_days = _architecture_block_ttl_days()
architecture_blocks = _build_architecture_compatibility_blocks(
terminal,
now=now_datetime,
ttl_days=architecture_block_ttl_days,
)
architecture_blocks_by_gpu_framework: dict[str, int] = defaultdict(int)
for block in architecture_blocks.values():
combination = f"{block['targetGpu']}|{block['framework']}"
architecture_blocks_by_gpu_framework[combination] += 1
return {
"generatedAt": now,
"totalRecords": len(self._records),
"pendingRecords": pending_count,
"policyCancelledRecords": policy_cancelled_count,
"terminalRecords": len(terminal),
"gpuSummaries": gpu_summaries,
"frameworkSummaries": framework_summaries,
"combinationStats": combination_stats,
"recentCombinationStats": recent_combination_stats,
"profileCombinationStats": profile_combination_stats,
"recentProfileCombinationStats": recent_profile_combination_stats,
"architectureCompatibilityBlocks": architecture_blocks,
"architectureCompatibilitySummary": {
"activeBlockCount": len(architecture_blocks),
"ttlDays": architecture_block_ttl_days,
"byGpuFramework": dict(architecture_blocks_by_gpu_framework),
},
"observedGpuMemoryGiB": observed_gpu_memory,
"totals": _summarize(terminal),
"warnings": warnings,
}
def save(self) -> None:
local_records = list(self._records)
def merge(existing: list[dict[str, Any]]) -> list[dict[str, Any]]:
merged = list(existing)
index_by_key = {_outcome_record_key(record): index for index, record in enumerate(merged)}
for record in local_records:
key = _outcome_record_key(record)
existing_index = index_by_key.get(key)
if existing_index is None:
index_by_key[key] = len(merged)
merged.append(record)
continue
merged[existing_index] = _prefer_newer_outcome(merged[existing_index], record)
return merged
self._records = update_jsonl(self.path, merge)
self._rebuild_indexes()
def _rebuild_failed_index(self) -> None:
self._failed_model_gpus.clear()
latest_by_combo: dict[tuple[str, str], tuple[datetime, dict[str, Any]]] = {}
for record in self._records:
# Infrastructure failures and our own policy cancellations neither
# clear nor create a model/GPU cooldown. Look through them to the
# latest attributable outcome.
if _is_platform_failure(record) or _is_policy_cancelled(record):
continue
model_id = record.get("modelId") or ""
target_gpu = record.get("targetGpu") or ""
event_time = parse_datetime(record.get("lastSyncTime")) or parse_datetime(record.get("submitTime"))
if not model_id or not target_gpu or event_time is None:
continue
key = (model_id, target_gpu)
current = latest_by_combo.get(key)
if current is None or event_time >= current[0]:
latest_by_combo[key] = (event_time, record)
for key, (event_time, record) in latest_by_combo.items():
if record.get("outcome") == "failed":
self._failed_model_gpus[key] = event_time
@staticmethod
def _update_record_from_task(record: dict[str, Any], task: dict[str, Any]) -> None:
record["status"] = task.get("status")
record["verifyResult"] = task.get("verifyResult")
record["lastSyncTime"] = _now_iso()
if task.get("logCosUrl"):
record["logCosUrl"] = task.get("logCosUrl")
status = str(task.get("status") or "").strip().lower()
if is_success(task):
record["outcome"] = "success"
record["failReason"] = None
if record.get("policyCancelled"):
record["policyCancellationResolvedAsSuccess"] = True
record["policyCancelled"] = False
elif record.get("policyCancelled"):
# A successful stop may be observed as waiting/running briefly before
# the platform publishes its terminal cancellation state. None of
# those intermediate states should become failure evidence.
record["outcome"] = "policy_cancelled"
record["failReason"] = None
elif is_failure(task):
record["outcome"] = "failed"
record["failReason"] = classify_failure(task)
else:
record["outcome"] = "pending"
@staticmethod
def _create_record_from_task(task: dict[str, Any]) -> dict[str, Any]:
create_time = parse_datetime(task.get("createTime"))
record: dict[str, Any] = {
"modelId": task.get("modelId") or task.get("model_id") or "",
"targetGpu": task.get("gpuType") or task.get("targetGpu") or "",
"framework": task.get("framework") or "",
"taskType": task.get("taskType") or "",
"taskId": str(task.get("taskId")) if task.get("taskId") is not None else None,
"submitTime": create_time.isoformat() if create_time else _now_iso(),
"lastSyncTime": _now_iso(),
"status": task.get("status"),
"verifyResult": task.get("verifyResult"),
"outcome": "pending",
"failReason": None,
"logCosUrl": task.get("logCosUrl"),
}
if is_success(task):
record["outcome"] = "success"
elif is_failure(task):
record["outcome"] = "failed"
record["failReason"] = classify_failure(task)
return record
def _summarize(records: list[dict[str, Any]]) -> dict[str, Any]:
total = len(records)
success_count = sum(1 for r in records if r.get("outcome") == "success")
failure_count = sum(1 for r in records if r.get("outcome") == "failed")
pending_count = sum(1 for r in records if r.get("outcome") == "pending")
attributable_failure_count = sum(
1 for record in records
if record.get("outcome") == "failed" and not _is_platform_failure(record)
)
platform_failure_count = failure_count - attributable_failure_count
decision_total = success_count + attributable_failure_count
failure_breakdown: dict[str, int] = defaultdict(int)
for r in records:
reason = r.get("failureCategory") or r.get("failReason")
if reason:
failure_breakdown[reason] += 1
return {
"total": total,
"successCount": success_count,
"failureCount": failure_count,
"attributableFailureCount": attributable_failure_count,
"platformFailureCount": platform_failure_count,
"decisionTotal": decision_total,
"pendingCount": pending_count,
"successRate": round(success_count / total, 4) if total > 0 else 0.0,
"failureRate": round(failure_count / total, 4) if total > 0 else 0.0,
"decisionSuccessRate": round(success_count / decision_total, 4) if decision_total > 0 else 0.0,
"decisionFailureRate": round(attributable_failure_count / decision_total, 4) if decision_total > 0 else 0.0,
"pendingRate": round(pending_count / total, 4) if total > 0 else 0.0,
"failureBreakdown": dict(failure_breakdown),
}
def _architecture_block_ttl_days() -> int:
raw = os.getenv("MODELHUB_ARCHITECTURE_BLOCK_TTL_DAYS")
if raw is None:
return DEFAULT_ARCHITECTURE_BLOCK_TTL_DAYS
try:
value = int(raw)
except ValueError:
return DEFAULT_ARCHITECTURE_BLOCK_TTL_DAYS
return min(365, max(1, value))
def _build_architecture_compatibility_blocks(
records: list[dict[str, Any]],
*,
now: datetime,
ttl_days: int,
) -> dict[str, dict[str, Any]]:
successes: dict[str, list[tuple[datetime, dict[str, Any]]]] = defaultdict(list)
failures: dict[str, list[tuple[datetime, dict[str, Any], dict[str, Any]]]] = defaultdict(list)
cutoff = now - timedelta(days=max(1, int(ttl_days)))
for record in records:
profile_data = record.get("modelProfile")
if not isinstance(profile_data, dict):
continue
profile = architecture_profile(
profile_data.get("modelType"),
profile_data.get("architectures"),
)
if profile is None:
continue
target_gpu = str(record.get("targetGpu") or "").strip()
framework = str(record.get("framework") or "").strip()
task_type = str(record.get("taskType") or "").strip()
key = architecture_compatibility_key(
target_gpu,
framework,
task_type,
profile["signature"],
)
event_time = (
parse_datetime(record.get("submitTime"))
or parse_datetime(record.get("lastSyncTime"))
)
if key is None or event_time is None:
continue
if record.get("outcome") == "success":
successes[key].append((event_time, record))
continue
if not _is_explicit_architecture_failure(record) or event_time < cutoff:
continue
failures[key].append((event_time, record, profile))
blocks: dict[str, dict[str, Any]] = {}
for key, failure_events in failures.items():
failure_events.sort(key=lambda item: item[0])
latest_failure_at, latest_failure, latest_profile = failure_events[-1]
success_events = successes.get(key) or []
latest_success_at = max((item[0] for item in success_events), default=None)
if latest_success_at is not None and latest_success_at >= latest_failure_at:
continue
effective_failures = [
item
for item in failure_events
if latest_success_at is None or item[0] > latest_success_at
]
expires_at = latest_failure_at + timedelta(days=max(1, int(ttl_days)))
if expires_at <= now:
continue
blocks[key] = {
"targetGpu": str(latest_failure.get("targetGpu") or ""),
"framework": str(latest_failure.get("framework") or ""),
"taskType": str(latest_failure.get("taskType") or ""),
"matchType": latest_profile["matchType"],
"architectureSignature": latest_profile["signature"],
"architectures": latest_profile["architectures"],
"modelType": latest_profile["modelType"],
"evidenceCount": len(effective_failures),
"latestFailureAt": latest_failure_at.isoformat(),
"latestSuccessfulAt": latest_success_at.isoformat() if latest_success_at else None,
"expiresAt": expires_at.isoformat(),
"sourceTaskIds": _bounded_unique(
record.get("taskId") for _, record, _ in reversed(effective_failures)
),
"sourceModelIds": _bounded_unique(
record.get("modelId") for _, record, _ in reversed(effective_failures)
),
}
return blocks
def _is_explicit_architecture_failure(record: dict[str, Any]) -> bool:
return bool(
record.get("outcome") == "failed"
and record.get("failureCategory") == EXPLICIT_ARCHITECTURE_FAILURE_CATEGORY
and record.get("failureDeterministic") is True
and record.get("failureClassificationReason") == EXPLICIT_ARCHITECTURE_FAILURE_REASON
)
def _bounded_unique(values: Any, limit: int = 5) -> list[str]:
result: list[str] = []
for value in values:
rendered = str(value or "").strip()
if not rendered or rendered in result:
continue
result.append(rendered)
if len(result) >= limit:
break
return result
def _is_platform_failure(record: dict[str, Any]) -> bool:
if record.get("outcome") != "failed":
return False
category = str(record.get("failureCategory") or "").lower()
scope = str(record.get("failureScope") or "").lower()
return scope == "platform" or category.startswith("platform_")
def _is_policy_cancelled(record: dict[str, Any]) -> bool:
return bool(record.get("policyCancelled")) or record.get("outcome") == "policy_cancelled"
def _consecutive_attributable_failures(records: list[dict[str, Any]]) -> int:
count = 0
for record in records:
if record.get("outcome") == "success":
break
if record.get("outcome") == "failed" and not _is_platform_failure(record):
count += 1
return count
def _consecutive_platform_failures(records: list[dict[str, Any]]) -> int:
count = 0
for record in records:
if not _is_platform_failure(record):
break
count += 1
return count
def _latest_platform_failure_at(records: list[dict[str, Any]]) -> str | None:
for record in records:
if not _is_platform_failure(record):
continue
timestamp = parse_datetime(record.get("lastSyncTime")) or parse_datetime(record.get("submitTime"))
return timestamp.isoformat() if timestamp else None
return None
def _outcome_record_timestamp(record: dict[str, Any]) -> float:
timestamp = parse_datetime(record.get("lastSyncTime")) or parse_datetime(record.get("submitTime"))
return timestamp.timestamp() if timestamp else 0.0
def _outcome_record_key(record: dict[str, Any]) -> str:
task_id = record.get("taskId")
if task_id is not None:
return f"task:{task_id}"
return "fallback:{model}|{gpu}|{time}".format(
model=record.get("modelId") or "",
gpu=record.get("targetGpu") or "",
time=record.get("submitTime") or "",
)
def _outcome_version(record: dict[str, Any]) -> tuple[int, float, float]:
last_sync = parse_datetime(record.get("lastSyncTime"))
submit_time = parse_datetime(record.get("submitTime"))
outcome_rank = 1 if record.get("outcome") in {"success", "failed", "policy_cancelled"} else 0
return (
outcome_rank,
last_sync.timestamp() if last_sync else 0.0,
submit_time.timestamp() if submit_time else 0.0,
)
def _prefer_newer_outcome(existing: dict[str, Any], candidate: dict[str, Any]) -> dict[str, Any]:
if _outcome_version(candidate) >= _outcome_version(existing):
return candidate
return existing