fix(settings/agents): exclude noise from drift detection
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:
2026-05-04 17:39:17 +00:00
parent 702c01d678
commit 69e153b3db
2 changed files with 32 additions and 11 deletions

View File

@@ -336,6 +336,7 @@ export function AgentsTab() {
<div className="space-y-4"> <div className="space-y-4">
<Card className="bg-surface border-rule shadow-sm"> <Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-5 py-4 flex items-center justify-between gap-3 flex-wrap"> <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"> <div className="text-[0.85rem] text-ink-muted">
{data.pairs.length} סוכנים × 2 חברות (CMP master / CMPA mirror) {data.pairs.length} סוכנים × 2 חברות (CMP master / CMPA mirror)
{totalDrift > 0 && ( {totalDrift > 0 && (
@@ -347,6 +348,10 @@ export function AgentsTab() {
<span className="text-danger ms-2">· {missingCount} זוגות לא שלמים</span> <span className="text-danger ms-2">· {missingCount} זוגות לא שלמים</span>
)} )}
</div> </div>
<div className="text-[0.7rem] text-ink-light">
פערי skills מחושבים על paperclipai/* בלבד. local/* ו-company/* מסוננים שם שונה בין החברות הוא צפוי.
</div>
</div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"

View File

@@ -3191,6 +3191,8 @@ _AGENT_NAME_ORDER = {
} }
# Fields that should match between master (CMP) and mirror (CMPA). Drift = bug. # 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 = ( _DRIFT_FIELDS = (
"model", "model",
"effort", "effort",
@@ -3204,10 +3206,21 @@ _DRIFT_FIELDS = (
"wakeOnDemand", "wakeOnDemand",
"maxConcurrentRuns", "maxConcurrentRuns",
"budget_monthly_cents", "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: def _shape_paperclip_agent(raw: dict, company_id: str, company_name: str) -> dict:
"""Flatten a Paperclip agent row into the shape the UI consumes.""" """Flatten a Paperclip agent row into the shape the UI consumes."""
ac = raw.get("adapterConfig") or {} 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: for field in _DRIFT_FIELDS:
m_val = master.get(field) m_val = master.get(field)
i_val = mirror.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: if m_val != i_val:
drift.append({"field": field, "master": m_val, "mirror": i_val}) drift.append({"field": field, "master": m_val, "mirror": i_val})
return drift return drift