Skip to content

Desks and rosters

Steven Enamakel edited this page Sep 1, 2026 · 2 revisions

Desks and rosters

Two snapshots, both borrowed, both supplied by the host on the way in.

The roster

Roster is a borrowed view of who is present: agent members, human people, and whatever retirement or ordering the operator has applied on top. It is constructed from slices the host already read, and it validates its own structure before anything else uses it.

let members = [RosterMember { id: "alice".into(), name: Some("Alice".into()) }];
let roster = Roster::new(&members, &[], &[]);

Agents and people are distinct on purpose. A person mention cannot start an agent turn, and neither can a desk or an @everyone. Only a direct, active agent mention can, which is one message, one turn enforced at the level where a name is read.

Desks

A desk is a group conversation with an ordered member list. It is declared in a blueprint and then merged with what the operator did at runtime: added members, retired members, an explicit ordering, extra desks.

Desk {
    id: "engineering".into(),
    name: "Engineering".into(),
    description: Some("Build the product".into()),
    members: vec!["alice".into(), "bob".into()],
    responder_mode: ResponderMode::Lead,
}

DeskSet::new folds the declared desks with those overlays and hands back a borrowed view. Member order matters: the first member who has not been retired is the desk lead, unless a complete DeskOrder replaces that order outright.

Responder mode

ResponderMode::Lead picks the first effective active member. That is the default and it is fully deterministic.

ResponderMode::Auto asks the runtime Selector when the desk has at least two effective members, and falls back deterministically when the selector is unavailable, disabled, or returns something that does not name exactly one candidate. See Responder ladder.

Conversation identity

The subtle part is the default desk, which has four spellings and one identity. A console addresses its default thread as "main". An unaddressed message stores None. Older events carry "". The desk's own id and display name are "General".

assert!(is_general_chat(None));
assert!(is_general_chat(Some("")));
assert!(is_general_chat(Some("main")));
assert!(is_general_chat(Some("General")));
assert!(!is_general_chat(Some("engineering")));

All four are one conversation, and same_conversation is the only place that decides so. Everything else compares verbatim, case sensitively.

That single function matters more than it looks. When two surfaces decide "same conversation?" by different rules, a thread root stored as None fails to match the "General" it is rendered under, and a threaded continuation quietly resumes in the channel instead of the thread. One transcript splits across whichever id happened to write each message. Answering it in one place is the fix.

Clone this wiki locally