docs(agents-md): Bot Mode canonical-chat invariant is name-identity — corrections folded in

The cherry-picked #92121 text documented the pin-first contract (#92042 era).
Corrected to the registry contract this branch ships: identity is (profile,
'Bot Chat') via exact-title lookup; there is no session-id pin at any tier;
reviewer corollaries and regression-test references updated to the surviving
suites.
This commit is contained in:
Teknium
2026-08-22 01:09:39 -07:00
parent f70e6146dd
commit 14c59f0b50

View File

@@ -924,26 +924,34 @@ companion repo, not in this tree.
The desktop "Bots" experience ships bundled in-tree. Each bot is a Hermes
agent **profile** with a persistent identity. Its design rests on one settled
invariant that has been regressed twice, cost users real conversation
history both times, and is not open for re-litigation in a routine PR:
invariant that has been regressed repeatedly, cost users real conversation
history each time, and is not open for re-litigation in a routine PR:
**One bot = ONE canonical forever-chat ("Bot Chat"), ever.** The full
lifecycle when a bot row is clicked:
**One bot = ONE canonical forever-chat, identified by NAME.** The chat's one
and only identity is **(profile, session titled exactly "Bot Chat")** — the
state DB's UNIQUE(title) index makes that pair an exact registry of at most
one row. The full lifecycle when a bot row is clicked:
1. **A live pin ALWAYS and ONLY wins.** If the bot's pinned session resolves
(verified through the backend's `preferred_session` resolver), open it.
Nothing overrides it — not recency, not a newer visible session, not a
title mismatch on a pin that carries real history (grandfathered chats
stay adopted).
2. **No/dead pin → adopt before mint.** Look up the profile's existing
`Bot Chat` session by title via `session.list include_hidden:true` (the
state DB's unique-title index makes this an exact registry lookup) and
re-pin it. Only if none exists, create one.
3. **Create-time title conflict → adopt.** A failed/conflicting create means
the canonical session already exists — register that session as the pin.
Never mint a differently-titled replacement. (`set_session_title`
silently drops conflicting titles — returns 0 rows — which is how the
2026-08 infinite fork loop started.)
1. **Resolve the registry, every time.** Look up the profile's `Bot Chat`
session by exact title via `session.list {title, include_hidden: true}`
(indexed, window-free; hidden rows resolve because canonical chats are
always hidden; compression lineages resolve to the live tip). Row exists →
open it. That is the entire happy path.
2. **No row → create it,** titled `Bot Chat`, born hidden, kicked off with
the bot's intro. Creation adopts-before-minting: it re-runs the registry
lookup first, so a concurrent or pre-existing row is opened, never forked.
(`set_session_title` silently drops conflicting titles — returns 0 rows —
which is how the 2026-08 infinite fork loop started; adopt-before-mint is
what kills it.)
**There is NO session-id pin.** The previous design stored a pointer in
`ui_meta['hermes-bots'].chat` and verified it per click; five hardening
waves (#88690, #90732, #90751, the #91791 revert, #92042) each guarded a new
way that pointer dangled or got stolen — rows[0] steals, `last_session`
adoptions, transient clears, drifted-title welds (a pin re-anchored onto a
cron session passed every guard). Name-as-identity removes the failure class:
a name cannot dangle, and a corrupted historical pointer simply never gets
read. Legacy `chat` keys in ui_meta are ignored and dropped from merges.
Why recency must never win (the #91791 → #92042 lesson): canonical Bot
Chats are **unconditionally hidden** from the Sessions sidebar, so the bot
@@ -959,18 +967,24 @@ Corollaries for reviewers:
- There is no per-bot session browser, by explicit design (removed in
#90732). Do not add one back.
- A pinned session with real messages is the user's conversation whatever
its title says; only a pin resolving to an *empty* stray draft counts as
corrupted metadata.
- Reject any PR that reintroduces a stored session-id pointer as canonical
identity — including "as a fallback tier" or "for verification". The
registry lookup is the whole contract; pointers are how every prior
incident started.
- Reject any PR that consults recency, visibility, or "where the user left
off" while the pin is alive — reports that motivate such a change are
off" for the bot row's target — reports that motivate such a change are
almost always about side-chats, and the fix belongs in the Sessions
sidebar (hide-sweep false positives), not in the bot row's target.
- The gateway reports the registry row per profile as `canonical_session`
on `profiles.list` (resolved server-side by title); roster preview,
activity signals, and the `/new`→`/compact` guard all read it, so preview
identity and click identity are the same row by construction.
Regression tests encoding this contract:
`tests/bot-row-opens-canonical-chat.test.mjs`,
`tests/canonical-chat-adopt-before-mint.test.mjs`,
`tests/canonical-chat-pin.test.mjs`, `tests/hide-bot-chats.test.mjs`.
`tests/canonical-chat-registry.test.mjs` (includes a tripwire asserting the
open path never reads or writes a stored pointer),
`tests/canonical-chat-creation.test.mjs`, `tests/hide-bot-chats.test.mjs`,
and `tests/tui_gateway/test_profiles_list_canonical_session.py`.
---