Skip to content

fix(greeting): introduce the agent by its s.space display name - #6270

Merged
sonichi merged 3 commits into
mainfrom
fix/greeting-uses-display-name
Nov 6, 2026
Merged

sonichi merged 3 commits into
mainfrom
fix/greeting-uses-display-name

Conversation

@sonichi

@sonichi sonichi commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

Priority

Level: high
Reason: P1 from QA stress tests: on a fresh install the agent's greeting introduces it by a different name than the one s.space shows for it.

Closes

N/A (QA report, no issue filed).

Summary

Bug. A test agent whose display name differs from its internal persona name greeted its owner with the persona/default name instead of its s.space display name.

Root cause. Nothing hands the text agent its own display name. The bridge writes no agent-name task header (and reads only the mxid from whoami), SUTANDO_DISPLAY_NAME only flows outward in the profile card, and neither CLAUDE.md/AGENTS.md nor the room-onboard skill ("I'm ") names a source. The model fills the gap with "Sutando" or a persona/Stand name from memory. Only the tutorial's Getting started page looked the display name up, privately in getting_started.py.

Fix. One resolver, src/agent_display_name.py, for the name used in any self-introduction:

  1. the agent's live Matrix profile displayname (what s.space shows);
  2. the agent_display_name the gateway recorded for this same agent on its connection row (state/connections.json), when (1) is unreachable;
  3. SUTANDO_DISPLAY_NAME, else AGENT_DISPLAY_NAME (env, else the channel env file);
  4. a name from the mxid localpart;
  5. Sutando.

A Stand persona name is never used. CLI: python3 src/agent_display_name.py [--room ROOM] [--json].

Every introduction path now uses it:

  • first-contact reply / any greeting: one line in CLAUDE.md (AGENTS.md regenerated) under Identity;
  • room-onboard greeting: step 3 names the command;
  • tutorial --name auto: getting_started.resolve_name delegates to the resolver (its private profile/localpart/mxid code moved there, so there's one copy).

No tuned prompt text changed beyond naming the name source.

Look first at: src/agent_display_name.py, then skills/tutorial/scripts/getting_started.py (the delegation).

Out of scope, flagged: the local voice and phone prompts (src/voice-agent-config.ts, skills/phone-conversation/) introduce the agent as "Sutando — ". These are tuned voice prompts on surfaces that are not s.space, so they're left unchanged here. Moving them to this name needs a separate decision.

Docs: N/A. The behavior lives in the skill docs this PR updates (room-onboard, tutorial) and in CLAUDE.md. No docs/ canonical page covers greeting names. docs/src-map.md is regenerated for the new module.

Checklist

  • No other open PR on this topic (gh pr list --state open --search "greeting OR display name OR persona")
  • Author email is the CLA-mapped one
  • Single concern
  • Bug confirmed on origin/main (below)
  • Test added: tests/agent-display-name.test.py
  • Before/after evidence below
  • Live path? N/A. No bridge, delivery or startup code changes; agent instructions and a read-only resolver only.
  • Stacked PR? N/A
  • Hardcoded paths: scripts/review-checks.sh PASS
  • Uses connections.read_rows / channel_credentials / claude_home_path, no new path resolution

Before / after evidence

The test fixture is a test agent with display name "Display Name X" and a different persona name in stand-identity.json.

Before (origin/main + the new test):

ModuleNotFoundError: No module named 'agent_display_name'

With only the resolver module copied onto main, the greeting-path contract still fails, because no introduction path names a source:

AssertionError: 'agent_display_name.py' not found in '# Sutando\n\nYou are operating as part of Sutando ...

After (head):

== tests/agent-display-name.test.py           Ran 13 tests  OK
== tests/tutorial-getting-started.test.py     Ran 123 tests OK (skipped=1)
== tests/tutorial-deployed-fence.test.py      Ran 7 tests   OK
== tests/hermetic-env-structural.test.py      Ran 28 tests  OK

Each run used env -i, nice -n 19, and the shared test lock.

Mutations (each killed):

  • configured name ordered before the live profile name: FAILED (failures=1)
  • connection row matched without checking it is this agent's mxid: FAILED (failures=1)

Also run: ruff check clean, bash scripts/agents-md-sync.sh --check up to date, lint-skill 0 errors, lint-workspace-resolution / lint-claude-home-path / lint-sutando-home-path / lint-host-label pass, tests/agents-md-sync.test.py and tests/claude-md-conduct-rules.test.py pass.

Agent-assisted: written by Claude Code for Chi Wang.

🤖 Generated with Claude Code

A greeting (the first-contact reply, the room-onboard greeting) had no
source for the agent's own name, so the model fell back to a persona or
default name that can differ from the display name s.space shows.

src/agent_display_name.py is the one resolver for that name: the live
Matrix profile displayname, then the connection record's display name for
this agent, then SUTANDO_DISPLAY_NAME / AGENT_DISPLAY_NAME, then the mxid
localpart, then "Sutando"; never a Stand persona name. The tutorial's
`--name auto` now delegates to it, and CLAUDE.md/AGENTS.md and the
room-onboard skill tell the agent to use it when introducing itself.

Sutando-Task: task-51d648dbbfc7cfcd16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 93%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • skills/tutorial/scripts/getting_started.py (100%)
  • src/agent_display_name.py (100%)

Summary

  • Total: 116 lines
  • Missing: 0 lines
  • Coverage: 100%

…process

resolve() with no mxid, and the CLI's --room / --agent-mxid / own-identity
order, now run in process so diff coverage sees them; the channel_value
import guard is dropped (the module sits beside it in src/).

Sutando-Task: task-51d648dbbfc7cfcd16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at b27cf73. CI is green, and the design is right: one resolver, the tutorial delegates to it with no copy left behind, and voice/phone are untouched. One fix is needed before this merges:

--room loses the cross-server guard (src/agent_display_name.py, _main). room_agent_mxid() returns '' on purpose when the room's connection names no identity. Its docstring says this is "so another server's identity is never used". mxid = room_agent_mxid(args.room) or None turns that '' into None, so the next block falls back to agent_mxid(), the primary connection's identity. A NoConnectionForRoom raised inside the suppress does the same.

Failure case: an install with several connections greets a room on a secondary server whose row names no identity. It introduces itself by the primary agent's display name, the wrong-name bug this PR exists to fix. Keep '' as '', so resolve("") gives the configured name and then "Sutando". Decide explicitly whether NoConnectionForRoom should fall back. Add a test for the '' return; the current test pins the fallback.

Non-blocking:

  • The from channel_credentials import ... in configured_name and agent_mxid sits outside any suppress. A failed import makes resolve() raise, which breaks the "never fails" promise.
  • The profile tier wins with Synapse's default displayname, the localpart. An agent that never set a name is introduced as its raw account name. That matches what s.space shows, so it's fine if intended.

…nt's name

`--room` turned room_agent_mxid()'s deliberate '' (the room's connection
names no identity) into None, and swallowed NoConnectionForRoom the same
way, so both fell back to the primary connection's identity and its
display name. Both now resolve with no mxid: the configured name, else
"Sutando". A tier that raises (a failed import included) now leaves the
next tier instead of failing resolve().

Sutando-Task: task-51d648dbbfc7cfcd16
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

@sonichi
sonichi requested a review from qingyun-wu November 6, 2026 17:41
@sonichi

sonichi commented Nov 6, 2026

Copy link
Copy Markdown
Owner Author

Thanks @qingyun-wu, good catch. Fixed in 5bcb555.

Cross-server guard (blocking). In _main, room_agent_mxid()'s '' stays '' now. A NoConnectionForRoom, or any other failure looking up the room's connection, also resolves to ''. I chose not to fall back there: if no connection serves the room, the primary's identity is just as wrong as in the '' case. Both paths go to resolve(""), which gives the configured name and then "Sutando". They never fall back to the primary agent. None (a one-connection install) still uses the agent's own identity.

Never fails (non-blocking). I took this one too: a tier that raises, a failed import included, now falls through to the next tier instead of raising from resolve().

Profile tier with the server's default displayname. That's intended. It is what s.space shows for the agent, so the greeting matches it.

Tests. I added three to tests/agent-display-name.test.py: a '' room, a NoConnectionForRoom room (both with the primary row naming a display name that must not be used), and a raising tier. The existing test that pinned the fallback now covers only the None case.

  • Before (b27cf73 plus the new tests): FAILED (failures=2, errors=1). The two room cases and the raising tier fail.
  • After (5bcb555): Ran 13 tests ... OK. tests/tutorial-getting-started.test.py also passes (123 tests).
  • Mutations: putting back or None fails test_a_room_whose_connection_names_no_identity. Making the room-lookup failure fall back fails test_a_room_no_connection_serves.

(agent-assisted, Claude Code for Chi Wang's seat)

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 5bcb555; the blocking point is fixed. room_agent_mxid's '' is now kept and resolves to the configured name or 'Sutando', and any failure of the room lookup sets mxid to '', so the primary's identity is never borrowed. OtherServerRoomTest pins both cases, and both would fail at b27cf73. resolve() now isolates each tier, so a failed import can't make it raise. CI: 46 pass, 1 skipped. Nit, non-blocking: the broad except at the room lookup also catches an ImportError of channel_credentials. That fails closed and is consistent with the rule, but narrowing it to NoConnectionForRoom would make the intent exact.

@sonichi
sonichi merged commit b8dc659 into main Nov 6, 2026
75 of 77 checks passed
@sonichi
sonichi deleted the fix/greeting-uses-display-name branch November 6, 2026 20:00
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