Summary
Add an optional owner-settable display_name column to agent_ownership and surface it end-to-end through GET /api/agents and the MCP list_agents response. Ships no UI — this is the data layer that unblocks the rest of the #964 split.
Follow-up 1 of 5 from the design decision on #964. The slug remains the identity everywhere machines look; display_name is additive and UI-only.
Context
Per the decision recorded on #964: a friendly agent name already renders on three surfaces today, sourced from template.yaml — but it isn't owner-editable, disappears when the agent stops (it's a live HTTP call into the container), and covers 3 of ~67 render sites. Two further surfaces already fake a friendly name: src/backend/routers/public.py falls back through template info → trinity.agent-type label → slug, and src/backend/routers/voice.py uses agent_name.replace("-", " ").title().
This issue makes the field durable and universally available. Precedence:
agent_ownership.display_name (owner-set, durable, renders when stopped)
↓ falls back to
template.yaml display_name (today's behavior)
↓ falls back to
agent_name (slug)
Acceptance Criteria
Technical Notes
Cheap by construction. GET /api/agents returns enriched plain dicts (no response_model); list_all_agents_fast supplies Docker-label identity and get_accessible_agents merges DB fields via one batched fleet-wide query. Adding a DB-sourced field here is +1 column, +0 queries. Exact precedent: mcp_exposed (#846) is an agent_ownership column already surfaced through this same path.
Do NOT add display_name to AgentStatus / list_all_agents_fast. That is a hot labels-only path (startup, telemetry, ops, monitoring); adding it there forces either a per-agent DB lookup or a new Docker label — and a mutable friendly name in a Docker label cuts against Invariant #11's slug-as-identity. Docker stays authoritative for name; the DB owns display_name.
MCP needs almost no code. list_agents / get_agent / get_agent_info in src/mcp-server/src/tools/agents.ts are thin pass-throughs that serialize the backend payload verbatim (list_agents only touches .name, to filter by permission), so the field flows through automatically once it's on GET /api/agents.
Naming collision. get_agent_info already returns a display_name meaning the template's name (from the agent container's /api/template/info). Per the #964 decision we keep the key and let the DB value win where both exist — one concept with a fallback chain. This needs an explicit doc note since GET /api/agents and GET /api/agents/{name}/info will both carry the key from different sources.
Rename is a no-op for this field. agent_ownership is not in AGENT_REFS; the cascade only re-keys the agent_name column, so the friendly name survives a slug rename. Intended semantic — state it explicitly.
Guard for downstream issues: AgentAvatar hashes the agent name into its gradient and derives initials from it. Later UI issues must keep passing the slug to AgentAvatar, or every agent's avatar silently recolors the moment a display name is set.
Depends on: nothing. Blocks: the other four #964 follow-ups.
Summary
Add an optional owner-settable
display_namecolumn toagent_ownershipand surface it end-to-end throughGET /api/agentsand the MCPlist_agentsresponse. Ships no UI — this is the data layer that unblocks the rest of the #964 split.Follow-up 1 of 5 from the design decision on #964. The slug remains the identity everywhere machines look;
display_nameis additive and UI-only.Context
Per the decision recorded on #964: a friendly agent name already renders on three surfaces today, sourced from
template.yaml— but it isn't owner-editable, disappears when the agent stops (it's a live HTTP call into the container), and covers 3 of ~67 render sites. Two further surfaces already fake a friendly name:src/backend/routers/public.pyfalls back through template info →trinity.agent-typelabel → slug, andsrc/backend/routers/voice.pyusesagent_name.replace("-", " ").title().This issue makes the field durable and universally available. Precedence:
Acceptance Criteria
display_name TEXT(nullable) added toagent_ownershipinsrc/backend/db/schema.pyand to the MetaData insrc/backend/db/tables.pysrc/backend/db/migrations.py(private fn + registry entry, following theagent_ownership_mcp_exposedpattern)src/backend/migrations/versions/on top of the current head0023_agent_sync_state_gc_signals, idempotent (ADD COLUMN IF NOT EXISTS)display_nameadded to the batched query insrc/backend/db/agent_settings/metadata.py(get_all_agent_metadata) — must add zero additional queriesdisplay_namemerged into the list response insrc/backend/services/agent_service/helpers.py, including the orphan branch (Docker-present/DB-absent) which setsNonefor shape consistencydisplay_name?: stringadded to theAgentinterface insrc/mcp-server/src/types.tslist_agentsreturns bothname(slug) anddisplay_name; agents remain addressable by slug onlylist_agents/get_agent_infotool descriptions updated to disambiguate the twodisplay_namesourcesdisplay_namesurvives a slug rename untoucheddocs/memory/architecture.mdagent_ownershipschema block updatedschema-parityCI job + a realalembic upgrade head)Technical Notes
Cheap by construction.
GET /api/agentsreturns enriched plain dicts (noresponse_model);list_all_agents_fastsupplies Docker-label identity andget_accessible_agentsmerges DB fields via one batched fleet-wide query. Adding a DB-sourced field here is+1 column, +0 queries. Exact precedent:mcp_exposed(#846) is anagent_ownershipcolumn already surfaced through this same path.Do NOT add
display_nametoAgentStatus/list_all_agents_fast. That is a hot labels-only path (startup, telemetry, ops, monitoring); adding it there forces either a per-agent DB lookup or a new Docker label — and a mutable friendly name in a Docker label cuts against Invariant #11's slug-as-identity. Docker stays authoritative forname; the DB ownsdisplay_name.MCP needs almost no code.
list_agents/get_agent/get_agent_infoinsrc/mcp-server/src/tools/agents.tsare thin pass-throughs that serialize the backend payload verbatim (list_agentsonly touches.name, to filter by permission), so the field flows through automatically once it's onGET /api/agents.Naming collision.
get_agent_infoalready returns adisplay_namemeaning the template's name (from the agent container's/api/template/info). Per the #964 decision we keep the key and let the DB value win where both exist — one concept with a fallback chain. This needs an explicit doc note sinceGET /api/agentsandGET /api/agents/{name}/infowill both carry the key from different sources.Rename is a no-op for this field.
agent_ownershipis not inAGENT_REFS; the cascade only re-keys theagent_namecolumn, so the friendly name survives a slug rename. Intended semantic — state it explicitly.Guard for downstream issues:
AgentAvatarhashes the agent name into its gradient and derives initials from it. Later UI issues must keep passing the slug toAgentAvatar, or every agent's avatar silently recolors the moment a display name is set.Depends on: nothing. Blocks: the other four #964 follow-ups.