Skip to content

feat(agents): editable display label with an immutable slug (ent#181)#1676

Merged
vybe merged 2 commits into
devfrom
feature/181-agent-display-label
Jul 17, 2026
Merged

feat(agents): editable display label with an immutable slug (ent#181)#1676
vybe merged 2 commits into
devfrom
feature/181-agent-display-label

Conversation

@dolho

@dolho dolho commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

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 /rename stops the container, rewrites agent_name across ~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 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_label that 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:

PUT /api/agents/a2a-demo/label {"label":"Marketing Bot"}
  -> {"label":"Marketing Bot","display_name":"Marketing Bot"}
GET /api/agents          -> a2a-demo carries display_label="Marketing Bot"
container agent-a2a-demo  -> still exists (nothing restarted)
GET /api/agents/a2a-demo -> 200 (the slug still routes)
PUT ... {"label":"   "}  -> label:null, display_name:"a2a-demo"  (blank reverts to the slug)

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 + Alembic 0025, 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 (mirrors get_tags_for_agents), and the four DatabaseManager facade 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 coerces label to the slug, so the UI can tell "no label" from "label equals slug" and show an empty field. WS agent_label_changed broadcast.
  • Frontend — one resolution helper (utils/agentName.js), not per-site label || 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).
  • The slug rename is demoted, not removed (FR-5): a separate "Rename the id instead…" affordance with copy that states what it actually does. Owners who need it keep it; it stops being the default gesture.

Requirements §1.3.1 landed before the code. 28 tests.

Notes for review

🤖 Generated with Claude Code

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
@dolho
dolho requested a review from vybe July 17, 2026 14:59
…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 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.

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.

@vybe
vybe merged commit 6c38fe9 into dev Jul 17, 2026
24 checks passed
vybe pushed a commit that referenced this pull request Jul 17, 2026
…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>
vybe pushed a commit that referenced this pull request Jul 18, 2026
…) (#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>
dolho added a commit that referenced this pull request Jul 20, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants