feat: title new sessions from _meta.sessionTitle - #920
Open
wpfleger96 wants to merge 1 commit into
Open
Conversation
ACP clients that manage their own session naming had no harness-neutral way to name a Claude Code session; only the SDK-shaped `_meta.claudeCode.options.title` spelling worked, which every other harness would have to special-case. Map the neutral `_meta.sessionTitle` onto the SDK's `Options.title` so one wire contract works across adapters. The SDK-shaped spelling still wins when both are supplied — it is the more specific request and already-released behavior. Applied only when creating or forking, since the SDK ignores `title` on resume, and reused the adapter's existing `sanitizeTitle` so there is one title contract. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
added a commit
to block/buzz
that referenced
this pull request
Jul 27, 2026
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](agentclientprotocol/codex-acp#338) — Codex - [aaif-goose/goose#10712](aaif-goose/goose#10712) — Goose - [claude-agent-acp#920](agentclientprotocol/claude-agent-acp#920) — Claude Code --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An ACP client that spawns several sessions against one agent has no harness-neutral way to label them. The SDK-shaped
_meta.claudeCode.options.titlealready reachesOptions.titlethrough the...userProvidedOptionsspread, but that spelling is Claude-specific — a client driving Claude Code alongside other ACP agents has to special-case it. This maps the neutral_meta.sessionTitleonto the same SDK option so one wire contract works across adapters.Options.titleis the SDK's own creation affordance: the CLI persists it as acustom-titleentry and sets its auto-title suppression latch, so the title survives the first turn instead of being replaced by the generated summary. Nothing downstream needs to change —maybeUpdateSessionTitlealready preferscustomTitleoversummary, so the turn-endsession_info_updatecarries the client's title verbatim.Behavior
_meta.sessionTitle_meta.claudeCode.options.titleoptions.title"Fix the login bug""Fix the login bug""harness-neutral""sdk-shaped""sdk-shaped"null/ non-string / blankuserProvidedOptions?.title ?? sanitizedSessionTitle, applied after the spread.resume === undefined || forkSession === truepredicate the adapter already uses to allocateoptions.sessionId. The SDK ignorestitleon resume regardless, but gating makes the contract explicit.sanitizeTitleis reused as-is, so client titles obey the same whitespace and 256-character contract as SDK summaries rather than acquiring a second title contract. A title that sanitizes to empty is omitted, letting the SDK auto-generate instead of receiving an empty string._metareads in this file treat malformed input.Related
_meta.sessionTitlecontract for Codex_meta.sessionTitleon everysession/new