Skip to content

feat(sync-health): divergence age is the signal — red and frozen at >24h, persisted on the row (trinity-enterprise#706) - #3035

Merged
vybe merged 15 commits into
devfrom
AndriiPasternak31/ent706-707
Sep 29, 2026
Merged

vybe merged 15 commits into
devfrom
AndriiPasternak31/ent706-707

Conversation

@AndriiPasternak31

@AndriiPasternak31 AndriiPasternak31 commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Sync health measures whether the agent and its repository agree, and the freeze keys on divergence age

Fixes abilityai/trinity-enterprise#706 (PR-A of two; the fleet-health, fleet-audit and MCP surfaces are trinity-enterprise#707, stacked on this one).

Problem

Sync health measured whether the last push succeeded. An agent 15 commits ahead with auto-sync off never ran a heartbeat, so it never failed. The dashboard dot rendered it gray ("never synced"), not the green the issue describes. The schedule freeze (#1808) fired only after three failed syncs, so a work agent whose repository had stopped receiving its work kept running on cron indefinitely (operator ruling 2, invariants G4/G6).

What changes

  • Four persisted columns on agent_sync_state, on both tracks: SQLite agent_sync_state_divergence and Alembic 0081_agent_sync_state_divergence (parent 0080_agent_skill_sets, the live head since feat(skills): skill sets — assign a named slice of the library as a unit (ent#530) #3005 merged; re-chained 2026-09-28), plus schema.py and tables.py.
  • One policy module. services/sync_freeze_policy.py::classify decides state, reason, recommendation, binding and freeze.
    • It is stdlib-only, never raises, and is vendored byte-identically to src/scheduler/sync_freeze_policy.py (the refactor: unify the failure classifier into one shared package #1088 precedent, with a parity test).
    • services/sync_health_view.py::sync_view is the backend's one call site.
    • The scheduler's SYNC_FAILURE_FREEZE_THRESHOLD now comes from the mirror, so there is one number.
  • The freeze = freeze_schedules_if_sync_failing AND (sync_failing OR (work agent AND diverged > 24 h AND last_check_at ≤ 15 min old)).
    • SchedulerDatabase.sync_freeze_reason returns a reason string, never a (bool, reason) tuple. A tuple is truthy, so a caller left on the old shape would freeze the fleet. should_freeze_schedules is its bool view.
    • The skipped row's skip_reason is Git sync frozen: <reason>.
    • The gate stays fail-open.
  • One sync_diverged operator-queue item per episode, raised while a divergence freeze is in force, under the poller's lease.
    • Its id is sync-diverged-{agent}-{diverged_since}, and the prefix is reserved.
    • It carries counts and the recommendation, and no agent-authored error text.
  • Surfaces. GET /api/agents/sync-health, GET /api/agents/{name}/git/sync-state (and so MCP get_git_sync_state, a pass-through) and GET /api/internal/agents/{name}/sync-health-status add the verdict. Every pre-existing key is unchanged.
  • The dot. utils/syncHealth.js renders the backend's state and shows reason — recommendation on hover. It holds no threshold.

Decisions (posted on the issue 2026-09-27 for @vybe / @dolho to overrule; none overruled at the time of writing)

# Decision Taken
D1 One PR or two Two, stacked. This is PR-A; PR-B stays draft until this merges
D3 dirty_since column (not in the AC's list) Added. Dirt is its own episode, with no carry-over into divergence
D4 Divergence basis The own-branch working tuple. behind_main is information in the reason only
D5 Work agent source_mode = 0 OR auto_sync_enabled = 1. A fork-to-own or bound agent with auto-sync off reads as a deployment. #3022's badge should use the same predicate
D6 Freeze needs a fresh observation Yes: last_check_at ≤ 15 min, failing open
D7 Deployments Never red on age and never freeze on divergence. sync_failing still freezes any binding (#1808 unchanged)
D8 Skip-row volume One per cron tick while frozen (the operator's signal; retention prunes it)
D9 Thresholds 24 h / 24 h / 7 d / 15 min as constants. No env knob and no fleet kill switch; the per-agent flag is the switch
D10 Episode item Only when a freeze fires; one per episode; not auto-resolved
D14 ent#142 Close as superseded after this merges

Behaviour changes to know before merging

  • An agent ahead of origin with auto-sync off rendered gray and now renders yellow, or red after 24 h.
  • behind_working > 0 is no longer an automatic red. A deployment that is behind is yellow.
  • GET /api/agents/sync-health now reads one shared query (db.list_sync_health_rows). An agent whose git sync is disabled (sync_enabled = 0) shows unknown, not its last stale row.
  • Nothing can divergence-freeze for 24 h after deploy. A divergence freeze also needs the per-agent opt-in, which defaults to 0. Pre-release sizing query for the operator:
    SELECT count(*) FROM agent_git_config gc JOIN agent_sync_state ss USING (agent_name)
    WHERE gc.freeze_schedules_if_sync_failing = 1
      AND (gc.source_mode = 0 OR gc.auto_sync_enabled = 1)
      AND (ss.ahead_working > 0 OR ss.behind_working > 0);
  • The reason for a failed sync carries at most 120 characters of the agent's last_error_summary. That text was already on both endpoints; bug: workspace canvas panel flickers + charts broken in update_panel #707's fleet surfaces will need to decide whether to redact it (D12).

Migration coordination

Evidence

Check Result
Failing tests first Every new suite was run red before its code (collection error / 3 fail / 8 fail / 13 fail / 4 fail / 16 fail / 4 fail / 11 fail), then green
Per commit Each of the 9 commits exported with git archive and run through 16–22 related suite files: 310 → 406 passed, 0 failed at every commit
Full unit suite, tip pytest unit/ -n 8 --dist loadfile -p randomly: seed 12345 → 18645 passed, 0 failed. Seed 99999 → 1 failure in test_ent551_voice_background_tasks (a timing test in untouched voice code); the same-seed replay was 18646 passed, 0 failed; the test passes 5/5 alone and 36/36 under 12× concurrency
Frontend vitest run → 176 files, 3693 passed (the raw-colour and loading-gate ratchets included)
MCP server tsc --noEmit clean; npm test → 550 pass, 0 fail
Scheduler suite tests/scheduler_tests → 243 passed
Guards check_alembic_heads.py → 1 head (0081 after the re-chain on #3005); check_alembic_parity.py origin/dev HEAD → red with only the SQLite track, PASS with both; lint_sys_modules.py / lint_root_test_placement.py OK; no new ruff finding in any changed file
Control arm: episode item The same aged episode with the freeze flag off, or as a deployment → 0 items. A mutant that raises on red rather than on freeze makes the flag-off arm fail
Control arm: scheduler gate The same row with diverged_since = NULL fires; 24 h − 1 s fires; 24 h + 1 s freezes
/verify-local (global, VERIFY_PYTHON=python3.13, foreground, --skip-unit --skip-agent) pass: build + import-smoke OK, boot + health OK, integration 70 passed / 13 skipped / 2 deselected (registry false-fails)
In-image The scheduler image imports scheduler.sync_freeze_policy, and its sha256 prefix 0f0815349a2c162a equals the backend image copy and the source. Control: a pre-change scheduler image lacks the module
Live, real backend + scheduler images (a kept verify stack, DB-seeded agents) live-frozen (flag on, diverged 24 h + 60 s, fresh) → skipped, Git sync frozen: diverged 0 behind / 7 ahead for 24h. Control live-control (flag off, same row) → fired at the same tick. After clearing diverged_since the next tick fired (automatic un-freeze). /git/sync-state returned state=red, freeze=true vs false
PostgreSQL 16 (local; CI runs no PG pytest) A DB built by dev's tree has none of the columns (control); alembic upgrade head → 0080 (this revision's number before the re-chain), 4 columns added, the existing row kept with NULLs; downgrade -1 → 0079, 0 columns; re-upgrade OK. SchedulerDatabase.sync_freeze_reason over PG RealDictCursor rows: diverged 24 h + 60 s → reason; control NULL → None; stale 16 min → None; deployment → None. TEST_POSTGRES_URL db_harness run of the 7 sync suites → 185 passed ([postgres] variants included)
Gitlinks git ls-tree HEAD .claude src/backend/enterprise equals origin/dev

Not verified

  • A real github: agent end to end (plan §8 runbook): the in-container ahead/behind/porcelain numbers matching the stored row, and one sync_diverged item raised by a live poll. No GitHub PAT was available locally, and creating a repository is an external effect. Covered by unit tests through the real _poll_cycle only.
  • The UI in a real browser (both themes). Only the util mapping changed; no screenshot was taken.
  • The agent stage of /verify-local (--skip-agent): there is no docker/base-image change.

Known limitations (documented in the flow)

  • A failed git status inside the agent reads as a clean tree. This is agent-side and needs a base-image change.
  • The D5 gap: a fork-to-own or bound agent with auto-sync off never divergence-freezes.
  • SYNC_HEALTH_POLL_INTERVAL_SECONDS above 15 min disables the divergence freeze, because every observation is stale.
  • A starved pull (feat(voice): agent workspace page with canvas panel and Gemini panel tools (#699) #703) can cycle one short freeze window a day, and it self-heals.

🤖 Generated with Claude Code

🤖 Generated with Claude Code

…-enterprise#706)

Amends §11.8 (health now measures agreement with the repository, not only
push success) and adds §11.19: the four persisted columns, the one policy
module and its scheduler mirror, the work-agent predicate, the state rules,
the freshness-guarded freeze and the one-item-per-episode alert.

Refs Abilityai/trinity-enterprise#706
…ty-enterprise#706)

services/sync_freeze_policy.py::classify decides an agent's sync state
(green / yellow / red / unknown), the operator-readable reason, the
recommendation and whether its schedules freeze. Stdlib only, never raises,
vendored byte-identically to src/scheduler/ (the #1088 precedent) with a
parity test. The scheduler's SYNC_FAILURE_FREEZE_THRESHOLD now comes from
the mirror, so there is one number.

Nothing calls classify yet; the poller, the gates and the surfaces follow.

Refs Abilityai/trinity-enterprise#706
…count, last push (trinity-enterprise#706)

agent_sync_state gains diverged_since, dirty_files, dirty_since and
last_successful_push_at, on both tracks: the SQLite migration
agent_sync_state_divergence and Alembic 0080_agent_sync_state_divergence
(parent 0079_telegram_group_context, the live head), plus schema.py and
tables.py. All nullable, no backfill: the clocks start at the first poll
after upgrade, which is the 24 h soak before any divergence freeze.

The upsert takes a KEEP sentinel for the two clocks so a caller can say
clear (None) as distinct from unchanged, which _merged() cannot express.

Refs Abilityai/trinity-enterprise#706
…d the last push (trinity-enterprise#706)

On every poll, inside the leased cycle: diverged_since is set once when the
working tuple (origin on the agent's own branch) shows ahead or behind,
cleared when both are known 0 (or ahead is 0 with no upstream), and kept
when ahead could not be computed. dirty_files comes from the changes_count
the status call already makes, with dirty_since as its own episode clock.
last_successful_push_at is the later of the heartbeat's #3011 record, an
operator Push and the stored value; an image older than #3011 falls back to
its last success cycle. Agent timestamps are validated (explicit offset,
not in the future) before they can win a max().

Refs Abilityai/trinity-enterprise#706
…ce freeze (trinity-enterprise#706)

When a divergence freeze is in force, the poller (inside its leader lease)
raises one sync_diverged item with the deterministic id
sync-diverged-{agent}-{diverged_since}. create_item's (agent_name,
request_id) conflict target makes repeat polls, a restart and a fail-open
double leader converge on one row; a new episode has a new id. The prefix
is platform-reserved so an agent cannot pre-create the id and silence the
alarm about itself. No item for red-but-not-frozen; not auto-resolved. The
context carries counts and the recommendation, no agent-authored error text.

Refs Abilityai/trinity-enterprise#706
… agents (trinity-enterprise#706)

Both enforcement points now decide with the one policy module:

- src/scheduler/database.py: sync_freeze_reason() returns why an agent's
  cron firing is frozen (or None), reading the divergence inputs in the same
  fail-open query; should_freeze_schedules() is its bool view. A reason
  string, not a (bool, reason) tuple, because a tuple is truthy and a caller
  on the old shape would freeze the fleet.
- src/scheduler/service.py: the skipped row's skip_reason carries the
  reason ("Git sync frozen: diverged 0 behind / 7 ahead for 26h"), and the
  comment no longer calls the row count bounded.
- routers/internal.py: sync-health-status keeps its keys and adds
  freeze_reason, divergence_age_s and work_agent via the new
  services/sync_health_view.py, which the poller now uses too.

Refs Abilityai/trinity-enterprise#706
…verdict (trinity-enterprise#706)

GET /api/agents/sync-health and GET /api/agents/{name}/git/sync-state add
state, reason, recommendation, binding, freeze, the divergence age and the
new columns; every pre-existing key is unchanged. MCP get_git_sync_state
passes the backend JSON through, so it carries them too with no TS change.

The batch now reads through one shared query, db.list_sync_health_rows
(agent_git_config joined to live ownership, left-joined to
agent_sync_state), so the dot and the poller see the same set of agents.

Refs Abilityai/trinity-enterprise#706
…(trinity-enterprise#706)

utils/syncHealth.js no longer owns a threshold: classifySyncHealth maps the
backend's state (unknown -> gray) and syncHealthLabel shows its reason and
recommendation on hover. The 24 h / 7 d / behind-is-red rules are gone from
the frontend, so the dot cannot disagree with the scheduler's freeze.

Visible change: an agent ahead of origin with auto-sync off used to render
gray (it had never synced); it now renders the backend's yellow or red.

Refs Abilityai/trinity-enterprise#706
…freeze (trinity-enterprise#706)

Architecture (agent-lifecycle Git Sync Health, database DDL, background
services, backend catalog, api-endpoints), the git-sync-health flow (data
model, poller, the new §2b policy and freeze, the dot, tests, controls,
known limitations), the github-sync journey note, the feature-flows index
row, the RD-03 invariant note, and the user docs: what the sync state
means, when schedules pause and resume, the new Operating Room entry.
The MCP get_git_sync_state description names the new fields (the tool is
a pass-through; no behaviour change).

Refs Abilityai/trinity-enterprise#706
@AndriiPasternak31 AndriiPasternak31 changed the title fix(sync-health): divergence age is the signal — red and frozen at >24h, persisted on the row (trinity-enterprise#706) feat(sync-health): divergence age is the signal — red and frozen at >24h, persisted on the row (trinity-enterprise#706) Sep 27, 2026
…nterprise#706)

When the agent cannot compute its working tuple (a git timeout sends
ahead_working/behind_working as null), the poller correctly KEEPS
diverged_since, but _coerce_counter still stored the unknown counts as 0.
The row then read "diverged 25 h" next to 0/0: the freeze fired with the
self-contradicting reason "diverged 0 behind / 0 ahead" and the episode's
one sync_diverged item carried ahead 0 / behind 0.

Pass None (upsert's "keep the prior value") for the two working counts
exactly when the clock is kept on an unknown tuple. Scoped to that case:
with no episode to keep, an unknown tuple is still stored as 0 (#2827).

Found by /review on #3035. The new test was red before the fix
(assert (0, 0) == (7, 31)); the control arm is green before and after.
@AndriiPasternak31

Copy link
Copy Markdown
Contributor Author

/review Report

Branch: AndriiPasternak31/ent706-707 → dev (merge-base ebc2bfb4f)
Files Changed: 43 (+3091/−152) at review time; +2 commits since (below)
Scope: CLEAN. The diff is the trinity-enterprise#706 PR-A slice (columns, one policy, freeze, episode item, verdict on the three read surfaces, dot). The fleet surfaces are left to #707 as the body says.
Plan Completion: the AC items the body lists are all present in the diff. Not verifiable from the diff: a real github: agent end to end, and the UI in a browser. The body already lists both under "Not verified".

Execution coverage (Step 2.5)

changed symbol / test file executed by live consumer verdict
sync_freeze_policy.classify test_ent706_sync_policy.py sync_health_view.sync_view; scheduler database.py::sync_freeze_reason ✅ executed
scheduler sync_freeze_reason / should_freeze_schedules test_1808_sync_freeze_enforcement.py (real SQLite + mapping-only rows) scheduler/service.py fire path ✅ executed
SyncHealthService._sync_agent clocks, _last_successful_push_at, _emit_sync_diverged_alert test_ent706_divergence_tracking.py through the real _poll_cycle the poller loop ✅ executed
SyncStateOperations.list_health_rows / upsert KEEP test_ent706_sync_state_columns.py, test_ent706_sync_health_surfaces.py (db_harness) routers/agents.py::get_all_sync_health ✅ executed
routers/internal.py::internal_agent_sync_health test_ent706_internal_sync_health.py backend-internal read (the scheduler reads the DB directly) ✅ executed
scheduler mirror parity test_ent706_sync_policy_parity.py CI (vendored copy) 🛡 guard
syncHealth.spec.js readFileSync (no threshold in the util) vitest CI; behaviour cases sit beside it in the same spec 🛡 guard
test_threshold_matches_the_backend (reads internal.py text) pytest CI; the endpoint's behaviour is covered separately 🛡 guard
Alembic 0080 text asserts pytest CI; plus a local PG 16 upgrade/downgrade (PR body) 🛡 guard

Fix mutation: the fix in 2578a7151 (below) has a new test that was red before the fix (assert (0, 0) == (7, 31)) and is green after. Its control arm (an unknown tuple with no episode is still stored as 0) is green both before and after.

Critical Findings (block merge)

None.

Checked specifically, because this PR touches the scheduler fire path and routers/internal.py:

  • SQL safety. The scheduler query is parameterised (WHERE gc.agent_name = ?). list_health_rows is SQLAlchemy Core. The SQLite migration's f-string only takes the four constant column names from its own tuple. The Alembic revision is ADD/DROP COLUMN IF [NOT] EXISTS, nullable, with no backfill, so no data can be lost.
  • Fire-path safety. sync_freeze_reason returns Optional[str] and the caller tests it for truthiness. No tuple can reach it, and both freeze reasons are non-empty strings. A missing column on a DB that has not migrated yet raises inside the try and fails open (return None), which matches freeze_schedules_if_sync_failing is never enforced — schedules keep firing while sync is failing (safety toggle is inert) #1808's posture. All eight selected columns are read by name, so the PG RealDictCursor still works (test_mapping_only_rows_still_freeze).
  • Auth. /api/internal/* is behind verify_internal_secret (C-003). GET /{agent_name}/git/sync-state keeps AuthorizedAgentByName with an {agent_name} path param, so the bug: schedule enable/disable/trigger always 422 — OwnedAgentByName on {name} routes (regression from #2081 CSO M1) #2094 pairing is correct. The batch endpoint builds its set from get_accessible_agents(current_user) and drops out-of-scope rows before building any entry. The only agent-authored text in the new reason is the 120-character last_error_summary excerpt, and that text was already on both endpoints.
  • Operator-queue abuse. sync-diverged- is in _RESERVED_ID_PREFIXES, so an agent cannot pre-create the deterministic id to silence its own alarm. create_item is on_conflict_do_nothing on (agent_name, request_id), so a repeat or a double leader cannot reset an item the operator already answered back to pending.
  • Races. Only the leader-leased poller writes agent_sync_state, and no other upsert_sync_state call exists. Under a fail-open double leader, the first-poll diverged_since is last-write-wins by a few ms, and every later poll reads the one persisted value. test_two_pollers_converge_on_one_item covers this.

Informational Findings (review required)

[I1] Consistency: a kept divergence clock sat next to 0/0 counts. FIXED in 2578a7151 (Confidence: 8/10)
File: src/backend/services/sync_health_service.py (_sync_agent)
Evidence: the clock is decided on the raw value (known=ahead_raw is not None), but the counts went through ahead_working=_coerce_counter(payload, "ahead_working"), and _coerce_counter returns 0 for None.
Issue: when the agent's working tuple is uncomputable, the clock is correctly kept but the counts were overwritten with 0. This happens on a git rev-list failure or timeout: _ahead_behind_vs and _count_on_no_remote both return None. After 24 h the freeze fired with the reason diverged 0 behind / 0 ahead for 25h. The episode's one sync_diverged item then carried ahead 0 / behind 0 and no recommendation, a self-contradicting alarm that is never re-raised for that episode. The dirt clock was already consistent, because dirty_files goes through _merged, which keeps the prior value.
Fix: when the clock is kept on an unknown tuple, pass None, which is upsert's "keep prior", for both working counts. The change is scoped to that case, so #2827's default-0 is unchanged otherwise. Test: test_unknown_tuple_keeps_the_counts_with_the_clock plus the control test_unknown_tuple_without_an_episode_still_reads_zero. Flow doc updated, and a learnings fragment was added in f800e91b1.

[I2] Grid-mode tiles don't show the new verdict. Not a defect of this PR (Confidence: 7/10)
File: src/frontend/src/components/AgentTile.vue:342
The tile chip still keys on last_sync_status === 'failed' only, so a divergence-frozen agent shows no chip in grid mode. The list-mode dot is correct. This is not a regression: the tile never rendered sync health beyond failures, and every pre-existing key it reads is unchanged. It is a candidate for #707's fleet surfaces or a follow-up.

[I3] Scheduler fail-open during the migration window. Not a defect (Confidence: 6/10)
File: src/scheduler/database.py::sync_freeze_reason
The scheduler depends on backend: service_started, not on migrations having run. If it fires before 0080 / agent_sync_state_divergence has applied, the widened SELECT raises and the gate fails open. This also briefly suspends the #1808 failing-sync freeze, which the old two-column query would still have enforced. This is the documented fail-open posture, and the window is one boot. A warning is logged on each skipped check.

[I4] Batch vs per-agent read of a sync_enabled = 0 agent. Not a defect (Confidence: 6/10)
GET /api/agents/sync-health shows unknown for such an agent, because list_health_rows filters on sync_enabled = 1. GET /{name}/git/sync-state still classifies its last row, with a (last observed …) suffix. The divergence freeze cannot fire on it, because the stale observation fails open. The body already names the batch behaviour change. The two surfaces answer different questions (the fleet dot vs this agent's last row).

Clean Categories

  • Credential exposure: no new value is logged. The episode item's context holds counts and the recommendation, and last_error_summary is not in it (asserted).
  • Enterprise disclosure (4.5): the added public-doc lines were grepped against the guard pattern with no hit, and there is no private schema.
  • Enum completeness: sync_diverged is a new operator-queue type. Types are free-form here (no allowlist), and the #1677 emitter allowlist was updated.
  • Dead code / magic numbers: thresholds are named constants in the one policy module, and the scheduler re-exports SYNC_FAILURE_FREEZE_THRESHOLD from it.
  • Performance: the batch endpoint is one query, with the scope applied in Python (fleet-sized), so there is no N+1.

Summary

🤖 Generated with Claude Code

@AndriiPasternak31

Copy link
Copy Markdown
Contributor Author

PR Validation Report (re-run on f800e91b1)

PR: #3035, feat(sync-health): divergence age is the signal (trinity-enterprise#706)
Author: AndriiPasternak31
Branch: AndriiPasternak31/ent706-707 → dev
Files Changed: 44 (+3151/−154)
Lane: C + schema. The lane-C trigger is src/backend/routers/internal.py. The schema triggers are db/{schema,migrations,tables}.py and 0080_agent_sync_state_divergence.py.

Summary

Category Status Notes
Commit Messages ✅ 11 conventional commits. The last two are the /review fix (2578a7151) and its learnings fragment (f800e91b1)
Base Branch ✅ dev
PR Size ✅ 44 files, under the 50-file split threshold
Status Auto-Promotion ⚠️ Refs abilityai/trinity-enterprise#706, no closing keyword. A cross-repo keyword would not fire anyway, so bump ent#706 to status-in-dev by hand after merge
Roadmap ✅ ent#706 exists: P1, type-feature, theme-reliability, status-in-progress
Requirements ✅ docs/memory/requirements/github.md §11.19
Architecture ✅ architecture/{agent-lifecycle,api-endpoints,backend,background-services,database}.md
Feature Flows ✅ git-sync-health.md (updated again for the /review fix), github-sync.md, mcp-git-tools.md; index row + changelog row
Feature Flow Format ➖ These are existing flows that keep their established structure. No new flow was added
Security Check ✅ §4.1–4.6 clean. The only "email" hits are @pytest.* decorators
Infrastructure ➖ No compose, Dockerfile or nginx change
Build Packaging ✅ No new top-level src/backend/ directory
Config Packaging ➖ No new backend env var
File Modes / Host Paths ✅ No mode flip or symlink, and no /Users/ or /home/ path
Enterprise disclosure ✅ The guard pattern has 0 hits on added doc lines
Schema: dual track ✅ SQLite agent_sync_state_divergence plus Alembic 0080, both nullable with no backfill
Schema: single head ✅ / ⚠️ check_alembic_heads.py → 81 revisions, 1 head (0080_agent_sync_state_divergence). The branch is 0 commits behind dev and merge-tree is clean. Open #2984 also claims 0080 off 0079. Their table sets are disjoint (agent_sync_state vs seat_ask_class_state / agent_event_subscriptions), so this is mechanical: whichever lands second re-parents
Structural Review ✅ /review posted on this PR: 0 critical, 1 informational fixed test-first (2578a7151), 3 judged not defects
Merge Gate ✅ Tier 1 on f800e91b1 is green (pytest (head, seed 12345), regression diff, schema-parity, pg-migrations, CodeQL ×2, verify-non-root, Build (typecheck) + unit, gitleaks, lints). The lane-C PR-side Tier 2 checks are green too (journey-smoke, e2e, plus prod-image-smoke). dev-ci = success (all Tier 2 green) and there is no open dev-ci red issue
Test Adequacy ✅ Non-happy paths are covered: an unknown tuple, an unreachable agent, garbage values, the fail-open scheduler gate on a missing table and on PG mapping rows, a stale observation, a double leader, and the flag-off / deployment control arms
Code Quality ✅ Focused. The one policy module is vendored with a parity guard. The tests/ deletions (9 lines) widen the #1808 seed schema and replace the inline-threshold text pin with the policy-module guard. No guard was loosened
Requirements Trace ✅ Every AC item traces to §11.19 and the diff

Execution coverage (§5.4)

changed symbol / test file executed by live consumer verdict
sync_freeze_policy.classify test_ent706_sync_policy.py sync_health_view.py::sync_view; scheduler/database.py::sync_freeze_reason ✅
SchedulerDatabase.sync_freeze_reason test_1808_sync_freeze_enforcement.py scheduler/service.py cron fire path ✅
SyncHealthService._sync_agent (clocks, kept counts, push time, episode item) test_ent706_divergence_tracking.py via the real _poll_cycle the poller loop ✅
SyncStateOperations.list_health_rows / upsert KEEP test_ent706_sync_state_columns.py, test_ent706_sync_health_surfaces.py routers/agents.py::get_all_sync_health, routers/git.py::get_agent_sync_state ✅
internal_agent_sync_health test_ent706_internal_sync_health.py backend-internal read ✅
utils/syncHealth.js syncHealth.spec.js (behaviour cases + a no-threshold source pin) AgentListPanel.vue:864 ✅ (the pin is a 🛡 guard)
parity / Alembic-text / threshold pins pytest CI 🛡 guard

The /review fix was mutation-checked: test_unknown_tuple_keeps_the_counts_with_the_clock was red before 2578a7151 ((0, 0) == (7, 31)) and is green after.

Issues Found

Critical (Block Merge)

None.

Warnings (Review Required, non-blocking)

Suggestions (Optional)

  • Lane C lists /cso --diff as a companion pass. It has not been run. The lane-C surface here is a read-only, shared-secret-gated internal endpoint, and /review covered its auth boundary.

Recommendation

APPROVE. All gates are green and /review has run with nothing critical outstanding. The two warnings are the non-blocking kind described above.

🤖 Generated with Claude Code

@dolho dolho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Validation Report (/validate-pr)

Lane C + schema. routers/internal.py puts it in Lane C, and the migration files add the schema flag. 44 files, +3151/−154, AndriiPasternak31/ent706-707 → dev.

Category Status Notes
Commit messages ✅ 11 conventional commits, each with a clear scope
Base branch ✅ dev
PR size ✅ 44 files; ~half are docs and tests
Status auto-promotion ⚠️ Refs abilityai/trinity-enterprise#706 has no closing keyword. It's cross-tracker anyway, so bump status-in-dev on ent#706 by hand after merge
Requirements / Architecture / Flows ✅ requirements/github.md, 5 architecture area files, git-sync-health.md + github-sync.md + mcp-git-tools.md, user docs
Security (§4.1–4.10) ✅ No keys, emails, IPs, env vars, symlinks or host paths. The enterprise-docs guard grep is clean
Schema dual-track ✅ SQLite agent_sync_state_divergence + Alembic 0080 + schema.py/tables.py. check_alembic_heads.py on PR merged with current dev → 1 head, PASS
Build / config packaging ✅ No new top-level package and no new os.getenv
Merge gate (Tier 1 + lane C Tier 2) ✅ All checks green, including regression diff, pg-migrations, schema-parity, journey-smoke, e2e, prod-image-smoke and alembic-head-watch. dev-ci = success
Test adequacy ✅ Control arms for the freeze flag, deployments, the 24 h ± 1 s boundary, stale observations and PG RealDictCursor rows. The policy mirror has a byte-parity test

Execution coverage (§5.4)

Changed symbol Executed by Live consumer Verdict
sync_freeze_policy.classify test_ent706_sync_policy.py, test_1808_* sync_health_view.sync_view, scheduler/database.py::sync_freeze_reason ✅
SchedulerDatabase.sync_freeze_reason test_1808_sync_freeze_enforcement.py scheduler/service.py fire path ✅
episode clocks / KEEP / list_health_rows test_ent706_divergence_tracking.py, test_ent706_sync_state_columns.py SyncHealthService._sync_agent, GET /api/agents/sync-health ✅
_emit_sync_diverged_alert test_1677_*, divergence suite poller _poll_cycle ✅
utils/syncHealth.js syncHealth.spec.js (pure util, executed) dashboard dot ✅

Local verification. On the PR head I ran test_ent706_*, test_1808_* and test_1677_*: 118 passed. Then a mutation test: I removed the not stale freshness guard from classify in both copies, and two tests went red (test_stale_observation_stays_red_but_does_not_freeze, test_a_stale_observation_does_not_divergence_freeze). So that guard is really pinned.

Design notes (no action needed)

  • sync_freeze_reason returns Optional[str] rather than a tuple, and the docstring explains why. It still fails open on a pre-migration DB, because the missing columns make the query throw and it returns None.
  • The sync-diverged- prefix is reserved, and the id is deterministic per episode, with (agent_name, request_id) ON CONFLICT DO NOTHING. So an agent can't pre-empt or silence the item, and a double leader converges on one row.
  • The divergence item's question carries only the divergence clause (counts/age). No agent-authored error text reaches it. 👍

Suggestions (non-blocking)

  • Migration coordination. #2984 also claims 0080 with parent 0079. The table sets don't overlap (agent_sync_state vs its own table), so re-parenting is mechanical for whichever lands second. #3021 needs the same re-parent onto 0080_agent_sync_state_divergence if this lands first. The PR body already covers both.
  • Small edge. The sync_failing freeze now also requires a non-null last_check_at: with observed_age is None, classify returns unknown before it evaluates the freeze. upsert always stamps last_check_at, so I don't see a real regression. Mentioning it because the #1808 path used to ignore that column.
  • The UI wasn't checked in a browser (the PR says so). The change is just the util mapping, so the risk is low.

Recommendation: APPROVE ✅

The policy lives in one place, both migration tracks are there, the gate still fails open and CI is fully green. The tests execute the changed paths, and a mutation of the freeze guard turns them red.

#3005 (skill sets) landed 0080_agent_skill_sets on 0079. This branch's
Alembic revision becomes 0081_agent_sync_state_divergence on top of it,
and the SQLite entry follows agent_skill_sets in MIGRATIONS. No change
to what the migration does; docs updated to the new revision id.
AndriiPasternak31 added a commit that referenced this pull request Sep 28, 2026
#3005 (skill sets) landed 0080_agent_skill_sets on 0079. The ent#706
Alembic revision this branch carries becomes 0081_agent_sync_state_divergence
on top of it, and the SQLite entry follows agent_skill_sets in MIGRATIONS --
the same resolution as #3035's merge 9229f08. Docs name the new revision id.

@dolho dolho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/review — #3035, changes since my approval (f800e91b18 → 9229f08f39)

Re-approving at the new head. Approval still needs the dev conflict resolved, see below.

The delta. The only new commit is the dev merge. It moves the migration on top of #3005: the Alembic revision becomes 0081_agent_sync_state_divergence with down_revision = "0080_agent_skill_sets", and the SQLite entry follows agent_skill_sets. What the migration does is unchanged, the SQLite name is unchanged, and no reference to the old 0080 id is left (git show --remerge-diff shows only the rename, the list order and three docs lines).

Tests run

  • At the PR head: the 8 PR test files (ent706 ×6, 1808, 1677) gave 118 passed. Schema parity, the Alembic parity guard, revision-id length and the heads guard gave 146 passed, 2 skipped.
  • On a local merge preview with current dev: the PR tests plus dev's operator-queue suites (1525, 1631, 1632, 2915, ent611 native-ask and endings) and the parity suites gave 653 passed. So #3028's operator-queue rework still works with the sync-diverged- reserved prefix and the create_operator_queue_item emitter.

Still blocking the merge: CONFLICTING with dev again. Please merge dev once more. Both conflicts are docs and registry only:

  • docs/memory/architecture/background-services.md: take dev's Operator Queue Sync row (it gains the #611 "Native asks" sentence) and keep this PR's Sync Health Service row.
  • tests/registry.json: keep both sides' new entries (the ent706 ones and dev's test_human_only_principals.py / test_mcp_key_creation_requires_session.py).

There is no Alembic fork: on the merge preview, check_alembic_heads.py reports 82 revisions and 1 head.

Informational: a PostgreSQL instance that booted an earlier build of this branch is stamped at the old 0080_agent_sync_state_divergence and would need a manual alembic stamp. This only matters if the branch was deployed on PostgreSQL.

@github-actions

Copy link
Copy Markdown

⚠️ Alembic head fork if this PR is merged into dev.

dev has advanced since this PR's checks last ran. GitHub recomputes the merge ref when the base moves but does not re-trigger workflows, so a green schema-parity here can describe a base that no longer exists (#2533).

scripts/ci/check_alembic_heads.py against dev + this PR, merged in memory
alembic-heads: FAIL — src/backend/migrations/versions resolves to 2 heads across 83 revision(s); exactly 1 is required.
  • 0081_agent_sync_state_divergence  (0081_agent_sync_state_divergence.py)
  • 0081_portal_messages_unread_idx  (0081_portal_messages_unread_idx.py)

They fork at: 0080_agent_skill_sets  (0080_agent_skill_sets.py)

`alembic upgrade head` is singular and resolves its target BEFORE applying anything,
so this graph applies ZERO revisions — every revision since the fork stops arriving,
not only the one that forked. Fix by chaining the newer revision off the real head,
or — if the forked revision may already be applied somewhere — by adding a merge
revision (`alembic merge -m "…" <head-a> <head-b>`), whose tuple `down_revision`
converges the line from any starting state. See Architectural Invariant #3.
alembic-heads: src/backend/enterprise/backend/migrations/versions — version directory absent (submodule not initialised) — skipped.

Evaluated on the version line only — this PR also conflicts with dev in 3 unrelated file(s), which do not change this verdict but must be resolved before merge:

docs/memory/architecture/background-services.md
src/backend/db/migrations.py
tests/registry.json

Fix: rechain this PR's revision off 0081_portal_messages_unread_idx (the current dev head), or — if the forked revision may already be applied somewhere — add a merge revision (alembic merge -m "…" 0081_agent_sync_state_divergence 0081_portal_messages_unread_idx), whose tuple down_revision converges the line from any starting state. See Architectural Invariant #3.

Push the fix and schema-parity re-checks it against the merge ref immediately; this comment clears on the next push to dev touching src/backend/migrations/versions/**.

Advisory — this check does not block merge. · head_sha: 9229f08f391b9af378f52ad2cd8271b6361219c5 · run

@github-actions

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

@github-actions

Copy link
Copy Markdown

⚠️ Live-instance suite skipped — merge conflict against dev.

Resolve by merging dev locally and pushing the result; the next nightly re-tests.

Alembic 0081_agent_sync_state_divergence -> 0082, down_revision
0081_portal_messages_unread_idx; SQLite entry after
portal_messages_unread_index. Docs/registry conflicts keep both sides.
@vybe

vybe commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

merge-train: not on this train — conflicts with dev and wasn't validated this run. Note: #3021 also adds an 0082_* Alembic revision off 0081_portal_messages_unread_idx; whichever of the two lands second must re-parent to avoid the #2068 two-heads fork.

AndriiPasternak31 and others added 2 commits September 29, 2026 19:25
…06-707

# Conflicts:
#	docs/memory/feature-flows.md
@vybe vybe added the ui PR touches the frontend UI — triggers Playwright e2e tests label Sep 29, 2026
@vybe

vybe commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

merge-train: on this train. Mechanical changes pushed to your branch / PR:

Non-blocking notes: routers/git.py sync-state reports binding: "agent" for an agent with no git config (agents.py guards with if config else None, git.py doesn't); the new fields on GET /api/internal/agents/{name}/sync-health-status have no live caller (the scheduler reads the DB directly). The red journey-smoke was the /start ReadTimeout seen on unrelated PRs today, not this change. Heads-up: #3085 also adds an 0082 off the same parent and will re-parent after this lands.

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merge-train: batch validated on train/20260929-1926 (#3095)

@vybe
vybe merged commit 9690bc8 into dev Sep 29, 2026
28 of 33 checks passed
AndriiPasternak31 added a commit that referenced this pull request Sep 29, 2026
# Conflicts:
#	docs/memory/feature-flows.md
dolho added a commit that referenced this pull request Sep 30, 2026
…sion to 0083

dev gained 0082_agent_sync_state_divergence (#3035), forking the Alembic
head. Chain 0083_ent720_email_identity off it and order the SQLite entry
after agent_sync_state_divergence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dolho added a commit that referenced this pull request Sep 30, 2026
…revision to 0083

dev gained 0082_agent_sync_state_divergence (#3035), forking the Alembic
head. Chain 0083_seat_ask_class_state off it and order the SQLite entry
after agent_sync_state_divergence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dolho added a commit that referenced this pull request Sep 30, 2026
… revision to 0083

dev gained 0082_agent_sync_state_divergence (#3035), which adds its own
agent_sync_state columns. Union both column sets across schema/tables/
sync_state/sync_health_service, chain 0083_pull_sync off dev's head, and
order the SQLite entry after agent_sync_state_divergence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vybe added a commit that referenced this pull request Sep 30, 2026
…MCP, and the agent card (trinity-enterprise#707) (#3036)

* docs(requirements): divergence age and the divergence freeze (trinity-enterprise#706)

Amends §11.8 (health now measures agreement with the repository, not only
push success) and adds §11.19: the four persisted columns, the one policy
module and its scheduler mirror, the work-agent predicate, the state rules,
the freshness-guarded freeze and the one-item-per-episode alert.

Refs Abilityai/trinity-enterprise#706

* feat(sync-health): one policy for state, reason and the freeze (trinity-enterprise#706)

services/sync_freeze_policy.py::classify decides an agent's sync state
(green / yellow / red / unknown), the operator-readable reason, the
recommendation and whether its schedules freeze. Stdlib only, never raises,
vendored byte-identically to src/scheduler/ (the #1088 precedent) with a
parity test. The scheduler's SYNC_FAILURE_FREEZE_THRESHOLD now comes from
the mirror, so there is one number.

Nothing calls classify yet; the poller, the gates and the surfaces follow.

Refs Abilityai/trinity-enterprise#706

* feat(sync-health): persist divergence and dirt episode clocks, dirty count, last push (trinity-enterprise#706)

agent_sync_state gains diverged_since, dirty_files, dirty_since and
last_successful_push_at, on both tracks: the SQLite migration
agent_sync_state_divergence and Alembic 0080_agent_sync_state_divergence
(parent 0079_telegram_group_context, the live head), plus schema.py and
tables.py. All nullable, no backfill: the clocks start at the first poll
after upgrade, which is the 24 h soak before any divergence freeze.

The upsert takes a KEEP sentinel for the two clocks so a caller can say
clear (None) as distinct from unchanged, which _merged() cannot express.

Refs Abilityai/trinity-enterprise#706

* feat(sync-health): the poller keeps the divergence and dirt clocks and the last push (trinity-enterprise#706)

On every poll, inside the leased cycle: diverged_since is set once when the
working tuple (origin on the agent's own branch) shows ahead or behind,
cleared when both are known 0 (or ahead is 0 with no upstream), and kept
when ahead could not be computed. dirty_files comes from the changes_count
the status call already makes, with dirty_since as its own episode clock.
last_successful_push_at is the later of the heartbeat's #3011 record, an
operator Push and the stored value; an image older than #3011 falls back to
its last success cycle. Agent timestamps are validated (explicit offset,
not in the future) before they can win a max().

Refs Abilityai/trinity-enterprise#706

* feat(sync-health): one sync_diverged operator-queue item per divergence freeze (trinity-enterprise#706)

When a divergence freeze is in force, the poller (inside its leader lease)
raises one sync_diverged item with the deterministic id
sync-diverged-{agent}-{diverged_since}. create_item's (agent_name,
request_id) conflict target makes repeat polls, a restart and a fail-open
double leader converge on one row; a new episode has a new id. The prefix
is platform-reserved so an agent cannot pre-create the id and silence the
alarm about itself. No item for red-but-not-frozen; not auto-resolved. The
context carries counts and the recommendation, no agent-authored error text.

Refs Abilityai/trinity-enterprise#706

* feat(scheduling): the git-sync freeze keys on divergence age for work agents (trinity-enterprise#706)

Both enforcement points now decide with the one policy module:

- src/scheduler/database.py: sync_freeze_reason() returns why an agent's
  cron firing is frozen (or None), reading the divergence inputs in the same
  fail-open query; should_freeze_schedules() is its bool view. A reason
  string, not a (bool, reason) tuple, because a tuple is truthy and a caller
  on the old shape would freeze the fleet.
- src/scheduler/service.py: the skipped row's skip_reason carries the
  reason ("Git sync frozen: diverged 0 behind / 7 ahead for 26h"), and the
  comment no longer calls the row count bounded.
- routers/internal.py: sync-health-status keeps its keys and adds
  freeze_reason, divergence_age_s and work_agent via the new
  services/sync_health_view.py, which the poller now uses too.

Refs Abilityai/trinity-enterprise#706

* feat(sync-health): the batch and per-agent reads carry the backend's verdict (trinity-enterprise#706)

GET /api/agents/sync-health and GET /api/agents/{name}/git/sync-state add
state, reason, recommendation, binding, freeze, the divergence age and the
new columns; every pre-existing key is unchanged. MCP get_git_sync_state
passes the backend JSON through, so it carries them too with no TS change.

The batch now reads through one shared query, db.list_sync_health_rows
(agent_git_config joined to live ownership, left-joined to
agent_sync_state), so the dot and the poller see the same set of agents.

Refs Abilityai/trinity-enterprise#706

* feat(dashboard): the sync dot renders the backend's state and reason (trinity-enterprise#706)

utils/syncHealth.js no longer owns a threshold: classifySyncHealth maps the
backend's state (unknown -> gray) and syncHealthLabel shows its reason and
recommendation on hover. The 24 h / 7 d / behind-is-red rules are gone from
the frontend, so the dot cannot disagree with the scheduler's freeze.

Visible change: an agent ahead of origin with auto-sync off used to render
gray (it had never synced); it now renders the backend's yellow or red.

Refs Abilityai/trinity-enterprise#706

* docs(sync-health): divergence age, the one policy and the divergence freeze (trinity-enterprise#706)

Architecture (agent-lifecycle Git Sync Health, database DDL, background
services, backend catalog, api-endpoints), the git-sync-health flow (data
model, poller, the new §2b policy and freeze, the dot, tests, controls,
known limitations), the github-sync journey note, the feature-flows index
row, the RD-03 invariant note, and the user docs: what the sync state
means, when schedules pause and resume, the new Operating Room entry.
The MCP get_git_sync_state description names the new fields (the tool is
a pass-through; no behaviour change).

Refs Abilityai/trinity-enterprise#706

* fix(sync-health): a kept divergence clock keeps its counts (trinity-enterprise#706)

When the agent cannot compute its working tuple (a git timeout sends
ahead_working/behind_working as null), the poller correctly KEEPS
diverged_since, but _coerce_counter still stored the unknown counts as 0.
The row then read "diverged 25 h" next to 0/0: the freeze fired with the
self-contradicting reason "diverged 0 behind / 0 ahead" and the episode's
one sync_diverged item carried ahead 0 / behind 0.

Pass None (upsert's "keep the prior value") for the two working counts
exactly when the clock is kept on an unknown tuple. Scoped to that case:
with no episode to keep, an unknown tuple is still stored as 0 (#2827).

Found by /review on #3035. The new test was red before the fix
(assert (0, 0) == (7, 31)); the control arm is green before and after.

* docs(learnings): a value kept on unknown keeps its companion values (trinity-enterprise#706)

* docs(requirements): sync health on fleet health, the fleet audit and the agent card (trinity-enterprise#707)

The requirement for the #707 read surfaces, written before the code: a
per-agent sync block and fleet totals on /api/monitoring/status, the
recommendation in issues[], the fleet sync audit's new columns and its MCP
tool, the agent-card sync chip, access scoping, and no raw git error text
in any surface's reason.

Refs Abilityai/trinity-enterprise#707

* feat(monitoring): fleet health carries each agent's git sync health (trinity-enterprise#707)

GET /api/monitoring/status annotates every agent with a sync block read
through the shared reader and the one policy: binding, auto_sync_enabled,
ahead/behind on the agent's own branch, dirty_files, last_successful_push_at,
divergence_age_s, state, reason, recommendation and frozen, or null for an
agent with no git binding. The response gains fleet totals in sync_summary,
and a red agent gains one 'sync: <reason> — <recommendation>' issue.

Sync is an annotation like the heartbeat layer: status, the sort and the
health counts are unchanged. The merge is computed before it is applied and
degrades on its own to null on a fault, never into the aggregation-failed
payload.

sync_view now takes reason and freeze_reason from the verdict computed with
the git error withheld, so no surface's reason carries the agent-written
stderr (this also covers the sync_diverged item's question). The
recommendation still reads the error. The scheduler's skip row, which calls
the policy directly, is unchanged.

Refs Abilityai/trinity-enterprise#707

* feat(fleet): the sync audit carries the divergence columns and the verdict (trinity-enterprise#707)

build_fleet_sync_audit keeps every pre-#707 key and adds, per agent, ahead,
behind, dirty_files, diverged_since, divergence_age_s,
last_successful_push_at, state, reason, recommendation, binding,
auto_sync_enabled and frozen from the same sync_view call every other
surface uses. dirty_tree is now real (dirty_files > 0) instead of a
hard-coded false, so the summary's in_sync and dirty are real too; the
summary adds diverged, frozen, auto_sync_off and red.

Refs Abilityai/trinity-enterprise#707

* feat(mcp): get_fleet_health carries sync health; get_fleet_sync_audit (trinity-enterprise#707)

get_fleet_health passes each agent's sync block (null without a git
binding), the fleet sync_summary and the issues strings through, next to the
kept issues_count, so an orchestrator sees the red-sync recommendation.

New get_fleet_sync_audit reads GET /api/fleet/sync-audit with the caller's
key and returns the audit unchanged. No agent target, so its policy row is
'none': the backend scopes the rows through accessible_agent_names, and an
agent key sees its owner's set (decision D12), as get_fleet_health already
does. routers/fleet.py's '# mcp:' header now names it (Invariant #13).

Refs Abilityai/trinity-enterprise#707

* feat(dashboard): the agent card shows ahead, behind, dirty and the last push (trinity-enterprise#707)

The Fleet tile carried 'sync failing ×N' (with the raw git error as its
tooltip) and a bare 'git ✓'; an agent 15 commits ahead with auto-sync off
showed neither. It now carries one sync chip: the kind follows the backend's
state (red crit, yellow warn, green calm, nothing for unknown), the text is
'↑7 ↓0 · 12 dirty · pushed 3h ago', and the tooltip is the reason and the
recommendation plus the absolute push time. One chip replaces two, so the
strip does not grow; a problem leads it and a calm fact trails it.

Agent Detail → Overview kept only consecutive_failures and said 'ok' for
every agent that had not failed a push, including agents with no git binding.
It now keeps the whole /git/sync-state payload and renders the same numbers
in the state colour; it sits on a chrome fill, so status ink steps up one tier (800 light / 300 dark: the 700/400 tier measured 4.47:1 and 3.73:1 there in the browser), '—' with no
observation or after a failed read. The attention count still counts failed
syncs (D13).

The formatter is its own module, utils/syncSummary.js, taking 'now' from the
caller: syncHealth.js keeps its #706 guarantee of no clock and no counts.
Semantic tokens only; per-file raw-colour and loading-gate counts unchanged.
Specs mount AgentTile and OverviewPanel.

Refs Abilityai/trinity-enterprise#707

* docs(sync-health): fleet health, the sync audit, get_fleet_sync_audit and the agent card (trinity-enterprise#707)

Architecture: the monitoring.ts row gains get_fleet_sync_audit and the
sync pass-through (130 tools); api-endpoints gains the /api/monitoring/status
row and the sync-audit additions. Feature flows: git-sync-health (§4 audit,
§5 the card, files, tests, the dirty_tree limitation closed, the scheduler
skip row's error excerpt recorded), agent-monitoring (the sync merge),
dashboard-grid-view (the chip) and a dated index row. User docs: monitoring,
managing agents, GitHub sync.

Refs Abilityai/trinity-enterprise#707

* fix(sync-health): a 2x sync glyph, centred; the Overview sync pill takes the state tint (trinity-enterprise#707)

From manual sign-off on the sign-off stack:
- The tile chip's ⟳ read as a dot at 9.5px. It is now its own aria-hidden
  element at 2x, in a fixed 11px box so the chip keeps its 12.22px height,
  and nudged up 0.09em: the fallback font draws its ink 1.5px below the box
  centre (measured in the browser; 0.12px after the nudge).
- The Overview 'Sync:' pill takes the state tint as a whole on red and
  yellow (status-* 50 / 900/30 fill, inset ring so it keeps its siblings'
  height). Green and unknown keep the neutral chrome of the sibling pills.

---------

Co-authored-by: Eugene Vyborov <1073874+vybe@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ui PR touches the frontend UI — triggers Playwright e2e tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants