Skip to content

feat(acp): title Codex sessions from the agent and channel name#3028

Open
wpfleger96 wants to merge 2 commits into
mainfrom
wpfleger/acp-session-title-meta
Open

feat(acp): title Codex sessions from the agent and channel name#3028
wpfleger96 wants to merge 2 commits into
mainfrom
wpfleger/acp-session-title-meta

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 26, 2026

Copy link
Copy Markdown
Member

Codex names an ACP session from the first text it receives. Every managed Buzz agent opens with the same [Base] You are operating inside the Buzz platform… framing, so the Codex session list shows a wall of identical rows, and because sessions are keyed per channel, one agent active in several channels produces several indistinguishable ones.

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.

This half is inert until the adapter ships

ACP adapters ignore _meta members they do not recognize, so against today's @agentclientprotocol/codex-acp 1.1.x a Buzz session gets no title and nothing else changes. The consuming side is codex-acp#338, which reads _meta.sessionTitle and names the Codex thread from it; neither half is useful alone. codex_adapter_is_outdated_with_path gates on major version < 1 (desktop/src-tauri/src/managed_agents/discovery.rs:1166-1176), and this feature needs no version floor above that, so none is added — 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 user env layer, so the override is what actually runs, and it already reaches the hash through the effective 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 sessions all showed up unnamed in the thread list, and one agent
active in several channels produced several indistinguishable rows
because sessions are keyed per channel.

The title travels out of band in `session/new` `_meta.sessionTitle`, so
it costs no tokens and cannot perturb the prompt contract. `_meta` is
omitted entirely when there is no title, since an adapter may
distinguish an absent member from a null one.

Sanitization and `Agent · #channel` composition live in buzz-acp: the
desktop spawn path only resolves `display_name`-or-`name` and exports
`BUZZ_ACP_SESSION_TITLE`. Only the channel part is truncated to the
80-char cap, so the agent name always survives. DMs, unresolved
channels, and heartbeat sessions keep the bare agent name.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Round-1 review found four correctness defects in the session-title path.

An interior NUL in a display name cannot cross the environment boundary at
all, so `Command::env` failed the whole spawn — the child-side sanitizer ran
far too late. Control characters are now stripped at the desktop boundary,
with a fallback to the unique handle when a display name is nothing else.

`spawn_config_hash` did not cover the title, so an agent rename left the
running process silently stale with no restart badge. It now hashes the same
resolve the spawn writes, and skips it when an explicit
`BUZZ_ACP_SESSION_TITLE` override shadows the record (the override already
reaches the hash through the effective env).

The channel lookup ran twice on a new session. `ChannelInfoResolver` caches
only `Some`, so an unresolvable channel paid the full `fetch_channel_info`
retry sequence twice — ~6.5s each — in front of `session/new`, exactly when
the relay is degraded. One resolve now serves both the canvas DM check and
the title.

`fetch_channel_info` substitutes a literal `"unknown"` name for a metadata
event with no `name` tag; that sailed past the DM filter and titled every
unnamed channel `Agent · #unknown` — the same collision the suffix exists to
remove. It is now treated as absent.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
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.

Managed ACP sessions show base prompt as title in Codex (and other harness UIs)

1 participant