fix(settings/agents): exclude noise from drift detection
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 32s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 32s
Two false positives surfaced after the Agents tab went live: 1. status (running/idle/paused) is runtime state, not config — drops in and out as agents pick up issues. Removed from _DRIFT_FIELDS. 2. desiredSkills compared raw, but local/* and company/* skills carry per-company hashes/scopes by design (sync_agents_across_companies.py filters local skills with a warning). Comparing them flags every master+mirror pair that has any local skill on master. Now compares only paperclipai/* skills (vendor-shipped, must match). UI shows an inline note explaining the filter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -336,6 +336,7 @@ export function AgentsTab() {
|
||||
<div className="space-y-4">
|
||||
<Card className="bg-surface border-rule shadow-sm">
|
||||
<CardContent className="px-5 py-4 flex items-center justify-between gap-3 flex-wrap">
|
||||
<div className="space-y-1">
|
||||
<div className="text-[0.85rem] text-ink-muted">
|
||||
{data.pairs.length} סוכנים × 2 חברות (CMP master / CMPA mirror)
|
||||
{totalDrift > 0 && (
|
||||
@@ -347,6 +348,10 @@ export function AgentsTab() {
|
||||
<span className="text-danger ms-2">· {missingCount} זוגות לא שלמים</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[0.7rem] text-ink-light">
|
||||
פערי skills מחושבים על paperclipai/* בלבד. local/* ו-company/* מסוננים — שם שונה בין החברות הוא צפוי.
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
||||
18
web/app.py
18
web/app.py
@@ -3191,6 +3191,8 @@ _AGENT_NAME_ORDER = {
|
||||
}
|
||||
|
||||
# Fields that should match between master (CMP) and mirror (CMPA). Drift = bug.
|
||||
# `status` is intentionally excluded — it's runtime state (running/idle/paused),
|
||||
# not config, and changes constantly.
|
||||
_DRIFT_FIELDS = (
|
||||
"model",
|
||||
"effort",
|
||||
@@ -3204,10 +3206,21 @@ _DRIFT_FIELDS = (
|
||||
"wakeOnDemand",
|
||||
"maxConcurrentRuns",
|
||||
"budget_monthly_cents",
|
||||
"status",
|
||||
)
|
||||
|
||||
|
||||
def _portable_skills(skills: list[str]) -> list[str]:
|
||||
"""Return only the skills whose drift across companies is meaningful.
|
||||
|
||||
`local/*` skills carry per-install hashes (different IDs per company even
|
||||
when the underlying skill is identical); `company/{cid}/*` skills are
|
||||
scoped to a single company by construction. Both are expected to differ
|
||||
between master and mirror — comparing them produces noise. Only
|
||||
`paperclipai/*` (vendor-shipped) skills should match exactly.
|
||||
"""
|
||||
return sorted(s for s in skills if s.startswith("paperclipai/"))
|
||||
|
||||
|
||||
def _shape_paperclip_agent(raw: dict, company_id: str, company_name: str) -> dict:
|
||||
"""Flatten a Paperclip agent row into the shape the UI consumes."""
|
||||
ac = raw.get("adapterConfig") or {}
|
||||
@@ -3252,6 +3265,9 @@ def _compute_drift(master: dict | None, mirror: dict | None) -> list[dict]:
|
||||
for field in _DRIFT_FIELDS:
|
||||
m_val = master.get(field)
|
||||
i_val = mirror.get(field)
|
||||
if field == "desiredSkills":
|
||||
m_val = _portable_skills(m_val or [])
|
||||
i_val = _portable_skills(i_val or [])
|
||||
if m_val != i_val:
|
||||
drift.append({"field": field, "master": m_val, "mirror": i_val})
|
||||
return drift
|
||||
|
||||
Reference in New Issue
Block a user