From c6c99c7b9fc6c1fc76cdb6c05b8687568cc51bb0 Mon Sep 17 00:00:00 2001 From: Alexander Ramirez Date: Sun, 15 Mar 2026 17:12:11 -0600 Subject: [PATCH 1/2] fix: preserve custom lead agent names during reprovisioning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ensure_board_lead_agent()` unconditionally reset the agent name to "Lead Agent" on every lifecycle reconciliation because `config_options .agent_name` is None during automated reprovisions, falling through to the static default. This meant any custom name set via PATCH (e.g. "Ferris", "Sentinel") was silently overwritten on the next gateway restart or heartbeat config patch — sometimes within minutes. Fix: prefer the agent's existing name over the static default. The fallback chain is now: explicit config name → existing DB name → "Lead Agent" (only for brand-new agents with no name). Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/services/openclaw/provisioning_db.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/app/services/openclaw/provisioning_db.py b/backend/app/services/openclaw/provisioning_db.py index 15f8e6849..3d910733f 100644 --- a/backend/app/services/openclaw/provisioning_db.py +++ b/backend/app/services/openclaw/provisioning_db.py @@ -168,7 +168,8 @@ async def ensure_board_lead_agent( ) ).first() if existing: - desired_name = config_options.agent_name or self.lead_agent_name(board) + default_name = self.lead_agent_name(board) + desired_name = config_options.agent_name or existing.name or default_name changed = False if existing.name != desired_name: existing.name = desired_name From 316724ef747249ac03070d42e0ada194d0d4ea20 Mon Sep 17 00:00:00 2001 From: Alexander Ramirez Date: Mon, 16 Mar 2026 11:43:26 -0600 Subject: [PATCH 2/2] test: cover lead agent name preservation --- .../tests/test_provisioning_db_lead_agent.py | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 backend/tests/test_provisioning_db_lead_agent.py diff --git a/backend/tests/test_provisioning_db_lead_agent.py b/backend/tests/test_provisioning_db_lead_agent.py new file mode 100644 index 000000000..875c51220 --- /dev/null +++ b/backend/tests/test_provisioning_db_lead_agent.py @@ -0,0 +1,146 @@ +# ruff: noqa: S101 +"""Unit tests for board-lead provisioning name behavior.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any +from uuid import UUID, uuid4 + +import pytest + +import app.services.openclaw.provisioning_db as provisioning_db +from app.models.agents import Agent +from app.models.boards import Board +from app.models.gateways import Gateway +from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig +from app.services.openclaw.provisioning_db import ( + LeadAgentOptions, + LeadAgentRequest, + OpenClawProvisioningService, +) + + +class _ExecResult: + def __init__(self, value: Agent | None) -> None: + self._value = value + + def first(self) -> Agent | None: + return self._value + + +@dataclass +class _FakeSession: + existing: Agent | None + commits: int = 0 + added: list[object] = field(default_factory=list) + refreshed: list[object] = field(default_factory=list) + + async def exec(self, _statement: object) -> _ExecResult: + return _ExecResult(self.existing) + + def add(self, value: object) -> None: + self.added.append(value) + + async def commit(self) -> None: + self.commits += 1 + + async def refresh(self, value: object) -> None: + self.refreshed.append(value) + + +def _board() -> Board: + organization_id = uuid4() + gateway_id = uuid4() + return Board( + id=uuid4(), + organization_id=organization_id, + gateway_id=gateway_id, + name="Roadmap", + slug="roadmap", + ) + + +def _gateway(*, organization_id: UUID) -> Gateway: + return Gateway( + id=uuid4(), + organization_id=organization_id, + name="Gateway", + url="ws://gateway.example/ws", + workspace_root="/tmp/openclaw", + ) + + +def _request( + *, + board: Board, + gateway: Gateway, + options: LeadAgentOptions | None = None, +) -> LeadAgentRequest: + return LeadAgentRequest( + board=board, + gateway=gateway, + config=GatewayClientConfig(url=gateway.url, token=None), + user=None, + options=options or LeadAgentOptions(), + ) + + +@pytest.mark.asyncio +async def test_ensure_board_lead_agent_preserves_existing_custom_name_without_override() -> None: + board = _board() + gateway = _gateway(organization_id=board.organization_id) + existing = Agent( + id=uuid4(), + board_id=board.id, + gateway_id=gateway.id, + name="Roadmap Captain", + is_board_lead=True, + openclaw_session_id=OpenClawProvisioningService.lead_session_key(board), + ) + session = _FakeSession(existing=existing) + service = OpenClawProvisioningService(session) # type: ignore[arg-type] + + lead, created = await service.ensure_board_lead_agent( + request=_request(board=board, gateway=gateway), + ) + + assert created is False + assert lead.name == "Roadmap Captain" + assert session.commits == 0 + + +@pytest.mark.asyncio +async def test_ensure_board_lead_agent_defaults_new_lead_name_when_none_provided( + monkeypatch: pytest.MonkeyPatch, +) -> None: + board = _board() + gateway = _gateway(organization_id=board.organization_id) + session = _FakeSession(existing=None) + service = OpenClawProvisioningService(session) # type: ignore[arg-type] + captured: dict[str, Any] = {} + + monkeypatch.setattr(provisioning_db, "mint_agent_token", lambda _agent: "raw-token") + + class _FakeOrchestrator: + def __init__(self, _session: object) -> None: + captured["session"] = _session + + async def run_lifecycle(self, **kwargs: Any) -> Agent: + captured["kwargs"] = kwargs + agent = next(item for item in session.added if isinstance(item, Agent)) + return agent + + monkeypatch.setattr(provisioning_db, "AgentLifecycleOrchestrator", _FakeOrchestrator) + + lead, created = await service.ensure_board_lead_agent( + request=_request(board=board, gateway=gateway), + ) + + assert created is True + assert lead.name == "Lead Agent" + assert lead.is_board_lead is True + assert lead.openclaw_session_id == OpenClawProvisioningService.lead_session_key(board) + assert session.commits == 1 + assert captured["session"] is session + assert captured["kwargs"]["auth_token"] == "raw-token"