Files
buzz/desktop/scripts
Will PflegerandGitHub f2fe3b63c2 feat(acp): title agent sessions from the agent and channel name (#3028)
ACP harnesses that name a session from the first text they receive all
land in the same place: every managed Buzz agent opens with the
identical `[Base] You are operating inside the Buzz platform…` framing,
so the harness session list shows a wall of indistinguishable rows.
Because sessions are keyed per channel, one agent active in several
channels produces several of them.

This sends the name out of band instead. `session/new` carries
`_meta.sessionTitle` with `Agent · #channel`, composed from the agent's
`display_name` (or its unique `name` handle) and the channel it is
serving. The prompt is untouched — no tokens spent, no perturbation of
the prompt contract, and nothing new for the desktop observer's section
parsing to handle.

The mechanism is harness-agnostic: Buzz sends the field on every ACP
`session/new` regardless of which harness is behind it, and adapters
that don't read it ignore it per spec.

## Inert until a consuming adapter ships

ACP adapters ignore `_meta` members they do not recognize, so against an
adapter with no reader a Buzz session gets no title and nothing else
changes. Three adapter halves consume it — Codex, Goose, and Claude Code
(linked below); this half and each reader are only useful together, and
each reader lands independently.

No version floor is added. `codex_adapter_is_outdated_with_path` already
gates codex-acp on major version `>= 1`
(`desktop/src-tauri/src/managed_agents/discovery.rs:1276-1284`) and this
feature needs nothing above that — an older adapter is not broken by the
extra member, it simply ignores it.

## What changes

**`crates/buzz-acp`** owns sanitization and composition.
`sanitize_session_title` collapses whitespace, drops control characters,
and caps at `SESSION_TITLE_MAX_CHARS` (80) by character, not byte, so a
multi-byte character cannot be split. `compose_session_title` truncates
only the channel part against that cap, so the agent name always
survives; when the agent name alone fills the cap the channel is dropped
rather than the name. `session_new_full` sets `_meta.sessionTitle` when
a title exists and omits `_meta` entirely when it does not, since an
adapter may distinguish an absent member from a null one.

**`desktop/src-tauri`** only resolves and exports.
`resolve_session_title` picks `display_name` or falls back to `name`,
and `spawn_agent_child` writes it to `BUZZ_ACP_SESSION_TITLE` — or
removes the variable when neither candidate yields anything printable.

DMs, unresolved channels, and heartbeat sessions get the bare agent name
with no channel suffix.

## Four properties that are easy to remove by accident

**Control characters are stripped at the desktop boundary, not in the
harness.** An interior NUL cannot cross the environment boundary at all
— `Command::env` fails the entire spawn rather than passing it through.
Deferring the strip to `buzz-acp` would let a corrupted display name
turn display chrome into a spawn failure. A display name that is *only*
control characters falls back to `name`.

**The title is hashed into `spawn_config_hash`.** Without it, renaming
an agent left the running process with a stale title and no restart
badge. The hash runs the same `resolve_session_title` the spawn writes,
and skips it when a user env override shadows `BUZZ_ACP_SESSION_TITLE` —
spawn writes the title *before* the layered user env, so the override is
what actually runs, and it already reaches the hash through
`descriptor.env`. Hashing the record-derived value under an override
would badge a rename that changes nothing.

**One channel resolve serves both consumers.**
`resolve_new_session_channel_context` returns `(is_dm, title_channel)`
from a single metadata lookup, feeding both the canvas block's DM check
and the title. `ChannelInfoResolver` caches only `Some`, so two
independent calls against an unresolvable channel pay the full
`fetch_channel_info` retry sequence twice — two timeouts plus a retry
delay each — directly in front of `session/new`, precisely when the
relay is already degraded.

**The `"unknown"` channel name is treated as absent.**
`fetch_channel_info` substitutes the literal `"unknown"` for a metadata
event with no `name` tag. Composing that sentinel would title every
unnamed channel `Agent · #unknown`, reintroducing the exact collision
the suffix exists to remove while naming a channel something it isn't.
The startup cache already refuses `channel_type == "unknown"` for the
same reason.

Closes #2334

Related — the adapter halves that consume `_meta.sessionTitle`:

-
[codex-acp#338](https://github.com/agentclientprotocol/codex-acp/pull/338)
— Codex
-
[aaif-goose/goose#10712](https://github.com/aaif-goose/goose/pull/10712)
— Goose
-
[claude-agent-acp#920](https://github.com/agentclientprotocol/claude-agent-acp/pull/920)
— Claude Code

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-07-27 12:46:43 -04:00
..