feat(agents): editable display label with an immutable slug (ent#181)#1676
Conversation
An agent had exactly one name — its slug — so "rename" meant the heavyweight identity change: `PUT /rename` stops the container, rewrites ~20 tables, clears every per-agent Redis keyspace, renames avatar files, and STILL leaves the agent's volumes under the old base, because Docker can rename neither a volume nor its immutable `trinity.agent-name` label. That path is the root of #1664/#1665/#1667/#1669/#1671. Yet "call it Marketing Bot" is the common case, and it should not touch any of that. Adds a display label that is rendered, never resolved. The slug stays the identity everything machine-facing keys on; the label is presentation. - `agent_ownership.display_label TEXT`, nullable — NULL means "render the slug", so no backfill and every existing agent is unchanged until someone sets one; clearing reverts to the slug rather than blanking a name. Dual-track migration (Invariant #3): SQLite `agent_ownership_display_label` + Alembic 0025, plus schema.py / tables.py. - `DisplayLabelMixin` (Invariant #2 — new setting, new mixin), with a batched reader for the fleet list so the hottest endpoint stays one query, and the four facade pass-throughs (the #1666 lesson: mocked tests can't see a facade gap). - `GET`/`PUT /api/agents/{name}/label`, owner-only. Read never coerces `label` to the slug — the UI must tell "no label" from "label equals slug". WS `agent_label_changed` broadcast. - Frontend: one resolution helper (`utils/agentName.js`) — no per-site `label || name`, which would show one agent under two names (§1.3.1 FR-3). The header pencil edits the label and shows the slug as secondary text (FR-4); list/tile surfaces render the label with the slug in the tooltip. The store goes through the shared axios client (Invariant #7). - The slug rename is demoted, not removed (FR-5): a separate "Rename the id instead…" affordance with copy stating what it does (restart, re-key, volumes stay under the old id) — owners who need it keep it; it stops being the default gesture. Requirements §1.3.1 written before the code (rule #1). Verified end-to-end on the live stack: label set → slug untouched (container intact, `/api/agents/{slug}` still 200, nothing restarted), blank and null both clear back to the slug. 28 tests. Decision (maintainer): OSS-core; demote the slug rename; label everywhere. Closes trinity-enterprise#181
…adcast live (ent#181)
Self-review (/review) caught two consistency gaps I'd introduced.
1. `GET /api/agents/{name}` — the endpoint AgentHeader loads on page open —
enriched the agent dict with owner/is_owner/can_share but NOT display_label.
So on a fresh load or refresh the header showed the SLUG; the label only
appeared after an edit round-tripped through the store. Reproduced live
(detail returned display_label=None for a labelled agent), fixed by adding
the one read, re-verified live. §1.3.1 FR-3 — the surfaces must agree.
2. The `agent_label_changed` broadcast was dead: it set only `type`, but the
frontend WS client switches on `event` (both are the convention, per
agent_started et al.), and no handler existed — so a label change on one
client never reached others. Added `event` + the `data` shape the other
agent_* events use, and a handler that updates the cached agent (the slug
never moves, so it's a pure re-render).
Endpoint test updated to assert the new broadcast shape.
vybe
left a comment
There was a problem hiding this comment.
Validated via /validate-pr. Requirements §1.3.1 landed before code; dual-track migration complete (SQLite agent_ownership_display_label + Alembic 0025 chained on 0024, schema.py + tables.py); label is rendered-never-resolved with one frontend resolution helper; owner-gated PUT with soft-delete guard; batched fleet read; slug rename demoted with honest copy. Real-DB harness tests pin the invariants (slug never moves, NULL=slug, no name reservation).
Minor follow-ups (non-blocking): (1) tests/registry.json carries a duplicate entry for test_181_agent_display_label.py — dedupe next touch; (2) GET/PUT /api/agents/{name}/label isn't in architecture.md's endpoint table yet; (3) cross-tracker ref — set status-in-dev on trinity-enterprise#181 manually after merge. All checks green.
…hain Alembic revision to 0026 after #1676 took 0025 Both PRs chained an 0025_* revision onto 0024; #1676 merged first, so this PR's revision becomes 0026_operator_queue_request_id with down_revision 0025_agent_ownership_display_label. SQLite migration list keeps both entries in dev order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…) (#1687) Follow-up 5/5 to the #964 display-name track, building on the ent#181 display_label foundation (#1676): the label + immutable slug, the `utils/agentName.js` resolver (`agentDisplayName`/`hasDistinctLabel`/ `agentNameTooltip`), and the `agent_label_changed` WS handler already shipped. This spreads the label to the surfaces #1676 didn't touch. Decision (AC #1): resolve slug -> label on the FRONTEND off the loaded agents, not by adding a mutable `display_name` to operator/monitoring/executions payloads. Those endpoints are high-volume; duplicating a presentation value across each invites staleness and N sources of truth. Two store getters do it from one place and stay live via the existing WS handler: - `displayNameForSlug(slug)` -> label or slug (prose) - `agentRefForSlug(slug)` -> agent object or bare slug (feeds the tooltip) An unloaded slug falls back to itself, so a cold surface never regresses. Render rule by surface class (#964): - Dense operational tables keep the SLUG primary, label as a hover tooltip: ExecutionsPanel, operator QueueCard/QueueList/QueueItemDetail/ResolvedCard/ NotificationsPanel, MonitoringPanel (alert + health rows), MobileAdmin ops + notif rows, RoleMatrix (100px column, slug-only + tooltip). - Prose / toasts use the label alone: Agents.vue toasts, Settings.vue agent confirm + subscription option, PortalConversation header/switcher/placeholder, PortalFilesPanel headings/toast. Comma-joined lists (GitHub-PAT propagation) stay slug — long labels make them unreadable. - Collaboration graph: network.js carries `display_label` on the agent and system-agent node data + a live `agent_label_changed` handler; AgentNode renders the label but keeps `data.label` (the slug) as the action key for router.push / toggleAutonomy / toggleAgentRunning. AgentAvatar stays on slug. - Router tab titles resolve the label on warm SPA nav, fall back to the slug on a cold direct load (store not fetched yet); next nav self-heals. - Reviewed against the rule: ExecutionDetail breadcrumb + source-agent attribution (label + slug link), PermissionsPanel target rows, ReplayTimeline lane, FoldersPanel consumer/source rows, AgentWorkspace + AgentBrainOrb headers. Terminal header stays slug (machine-facing). Requirements §1.3.1 FR-6 records the decision. No backend change. All 20 touched SFCs compile via @vue/compiler-sfc; the 3 JS files pass node --check. Related to #1643 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onflicts) Brings the A2A inbound-server branch (OSS #1628) up to current dev so it can merge. Three conflicts, all resolved as additive: - db/migrations.py — MIGRATIONS list: kept dev's four new SQLite migrations (display_label, operator_queue_request_id, users_github_pat, agent_reminders) and appended A2A's agent_ownership_a2a_exposed. Idempotent by name, order-safe. - db/schedules.py — dev split it into the db/schedules/ 10-mixin package (#1481); accepted the delete and ported A2A's sole change (the "a2a" trigger bucket) into db/schedules/analytics.py::_TRIGGER_BUCKETS. - tests/registry.json — unioned by `file`: dev's 88 entries + the 3 A2A-only test files (test_157_a2a_inbound_server, test_180_a2a_exposed_skills, test_a2a_exposure_mixin) = 91. Alembic re-chain (the #1676-class hazard): A2A's revision was 0025 chained onto 0024, but dev's own 0025_agent_ownership_display_label also chains onto 0024 — two heads, `alembic upgrade head` would fail and Postgres boot with it. Renamed 0025→0029_agent_ownership_a2a_exposed and re-chained down_revision onto dev's head 0028_agent_reminders. Verified a single Alembic head; docstring in the SQLite mirror updated to the new number. Verified: no conflict markers, backend byte-compiles, registry.json parses, single Alembic head, both agent_ownership columns present in schema.py/tables.py and both SQLite migrations registered. Enterprise submodule pointer unchanged.
Implements trinity-enterprise#181. OSS-core (maintainer decision — an agent's display name is basic UX, not a paid capability).
The problem
An agent has one name: its slug. So "rename" is the heavyweight identity change —
PUT /renamestops the container, rewritesagent_nameacross ~20 tables, clears every per-agent Redis keyspace, renames avatar files, and still leaves the agent's volumes under the old base, because Docker can rename neither a volume nor its immutabletrinity.agent-namelabel. That single operation is the root of #1664 (P0) / #1665 / #1667 / #1669 / #1671.But "call it Marketing Bot" is the common case, and it should touch none of that.
The fix: render a label, never resolve it
A per-agent
display_labelthat is presentation only. The slug stays the identity every route, container, volume, MCP key and A2A card keys on.Verified end-to-end on the live stack — this is the property the whole feature turns on:
Shape
agent_ownership.display_label TEXT, nullable. NULL = "render the slug", so no backfill and every existing agent is unchanged until someone sets one. Dual-track migration per Invariant Feature/vector log retention #3 (SQLite + Alembic0025, schema.py, tables.py).DisplayLabelMixin(Invariant Feature/gemini runtime support #2), with a batched reader for the fleet list so the hottest endpoint stays one query (mirrorsget_tags_for_agents), and the fourDatabaseManagerfacade pass-throughs — the fix(volumes): rename-safe volume identity — sweep, purge, create and recovery (#1664, #1665, #1667) #1666 lesson that mocked tests can't see a facade gap.GET/PUT /api/agents/{name}/label, owner-only. The read never coerceslabelto the slug, so the UI can tell "no label" from "label equals slug" and show an empty field. WSagent_label_changedbroadcast.utils/agentName.js), not per-sitelabel || name(which would show one agent under two names — §1.3.1 FR-3). The header pencil edits the label and keeps the slug visible as secondary text (FR-4); list/tile surfaces render the label with the slug in the tooltip. The store goes through the shared axios client (Invariant security: Fix token logging and add HTML reports to gitignore #7).Requirements §1.3.1 landed before the code. 28 tests.
Notes for review
0025onto dev's current head0024_volume_base_name. The A2A branch (feat(a2a): A2A interoperability — inbound server (well-known + JSON-RPC/SSE) + control MCP tools (ent#157/#160) #1628) also adds a0025. Whichever merges second must re-chain to0026— same fix I just did on feat(a2a): A2A interoperability — inbound server (well-known + JSON-RPC/SSE) + control MCP tools (ent#157/#160) #1628 when fix(volumes): rename-safe volume identity — sweep, purge, create and recovery (#1664, #1665, #1667) #1666 landed under it. (SQLite's track is a list and unions cleanly; only Postgres branches, which is whypg-migrationsis the job that catches it.)npm run buildfails locally on an unrelated pre-existing issue (mermaiddeclared in package.json, not installed,AgentWorkspace.vue) — reproduces with this branch stashed. My four touched SFCs compile cleanly via@vue/compiler-sfc.🤖 Generated with Claude Code