diff --git a/bot_bottle/backend/__init__.py b/bot_bottle/backend/__init__.py index 6fc9aa9..b519246 100644 --- a/bot_bottle/backend/__init__.py +++ b/bot_bottle/backend/__init__.py @@ -466,14 +466,20 @@ def enumerate_active_agents() -> list[ActiveAgent]: """All currently-running agents, across every available backend. Used by CLI `list active` and the dashboard's agents pane so neither has to know which backends exist. Skips - backends whose `is_available()` reports False. Ordered by - backend name, then by whatever each backend's - `enumerate_active` returns.""" + backends whose `is_available()` reports False. + + Sorted by `(started_at, slug)` so the list is stable across + dashboard refresh ticks — agents don't shift position while + the operator navigates with arrow keys. ISO 8601 timestamps + sort lexicographically in chronological order; `slug` is the + deterministic tiebreaker. Agents with missing metadata + (`started_at == ""`) sort first.""" out: list[ActiveAgent] = [] for name in known_backend_names(): if not has_backend(name): continue out.extend(_BACKENDS[name].enumerate_active()) + out.sort(key=lambda a: (a.started_at, a.slug)) return out diff --git a/tests/unit/test_backend_selection.py b/tests/unit/test_backend_selection.py index ef16696..1b9d076 100644 --- a/tests/unit/test_backend_selection.py +++ b/tests/unit/test_backend_selection.py @@ -81,6 +81,46 @@ class TestEnumerateActiveAgents(unittest.TestCase): ): self.assertEqual([a, b], enumerate_active_agents()) + def test_sorts_by_started_at_then_slug_across_backends(self): + newer = ActiveAgent( + backend_name="docker", slug="docker-new", agent_name="impl", + started_at="2026-06-02T12:00:00Z", services=(), + ) + tie_b = ActiveAgent( + backend_name="docker", slug="b-slug", agent_name="review", + started_at="2026-06-02T11:00:00Z", services=(), + ) + missing_metadata = ActiveAgent( + backend_name="smolmachines", slug="missing-metadata", + agent_name="?", started_at="", services=(), + ) + tie_a = ActiveAgent( + backend_name="smolmachines", slug="a-slug", agent_name="research", + started_at="2026-06-02T11:00:00Z", services=(), + ) + + class _FakeBackend: + def __init__(self, items): + self._items = items + + def is_available(self): + return True + + def enumerate_active(self): + return self._items + + with patch.object( + backend_mod, "_BACKENDS", + { + "docker": _FakeBackend([newer, tie_b]), + "smolmachines": _FakeBackend([missing_metadata, tie_a]), + }, + ): + self.assertEqual( + [missing_metadata, tie_a, tie_b, newer], + enumerate_active_agents(), + ) + def test_empty_when_no_backends_have_active(self): class _FakeBackend: def is_available(self):