diff --git a/apps/desktop/src/plugins/hermes-bots/canonical-chat-creation.test.ts b/apps/desktop/src/plugins/hermes-bots/canonical-chat-creation.test.ts index a7b66d125d..635eb0c555 100644 --- a/apps/desktop/src/plugins/hermes-bots/canonical-chat-creation.test.ts +++ b/apps/desktop/src/plugins/hermes-bots/canonical-chat-creation.test.ts @@ -125,7 +125,15 @@ describe('the lazy row is materialized before anything else touches it', () => { const { createCanonicalChat } = await loadModule() await createCanonicalChat('alpha') - expect(created).toMatchObject({ hidden: true, title: 'Bot Chat' }) + expect(created).toMatchObject({ + hidden: true, + title: 'Bot Chat', + // The PR #97008 contract: the canonical Bot Chat's runtime always + // follows the profile's CURRENT config on resume — never the stored + // model/provider pin. Dropping this param silently regresses bots to + // the server's exact-title legacy fallback. + follow_profile_config: true + }) }) it('sends the one intro turn on New Bot creation (kickoff: true)', async () => { diff --git a/apps/desktop/src/plugins/hermes-bots/canonical-chat.ts b/apps/desktop/src/plugins/hermes-bots/canonical-chat.ts index ffcaffb590..62e1346801 100644 --- a/apps/desktop/src/plugins/hermes-bots/canonical-chat.ts +++ b/apps/desktop/src/plugins/hermes-bots/canonical-chat.ts @@ -302,7 +302,14 @@ export function createCanonicalChat( // plugin-owned. Core applies this via the generic `hidden` flag // (deferred as pending_hidden until the row exists); older gateways // ignore the unknown param and it stays visible. - hidden: true + hidden: true, + // Explicit contract (PR #97008): this session's runtime always follows + // the member profile's CURRENT config. Resume must NOT restore the + // stored model/provider pin from an old row — that left bot DMs stuck + // on a stale/dead provider after a profile switch. Older gateways + // ignore the unknown param; the server's exact-title backfill then + // covers the legacy path. + follow_profile_config: true }) const sid = res?.stored_session_id diff --git a/apps/desktop/src/plugins/hermes-bots/group-test-utils.ts b/apps/desktop/src/plugins/hermes-bots/group-test-utils.ts index d8cd15679c..158bdf8234 100644 --- a/apps/desktop/src/plugins/hermes-bots/group-test-utils.ts +++ b/apps/desktop/src/plugins/hermes-bots/group-test-utils.ts @@ -30,6 +30,7 @@ export interface ScriptedMessage { } export interface ScriptedSession { + contracts?: { follow_profile_config: boolean; room_plumbing: boolean } messages: ScriptedMessage[] profile: string runtime: string @@ -219,6 +220,10 @@ export function createGroupGateway(options: GatewayOptions = {}): ScriptedGatewa const title = String(params.title ?? '') const session: ScriptedSession = { + contracts: { + follow_profile_config: params.follow_profile_config === true, + room_plumbing: params.room_plumbing === true + }, messages: [], profile, runtime: `rt-${profile}-${sequence}`, diff --git a/apps/desktop/src/plugins/hermes-bots/group-turns.test.ts b/apps/desktop/src/plugins/hermes-bots/group-turns.test.ts index 43a77c19d8..90b7deaef4 100644 --- a/apps/desktop/src/plugins/hermes-bots/group-turns.test.ts +++ b/apps/desktop/src/plugins/hermes-bots/group-turns.test.ts @@ -81,6 +81,26 @@ describe('session resolution', () => { expect(room.gateway.sessions.get(String(fresh.stored))?.title).toBe('Group: r-abc') }) + it('creates member sessions with the room_plumbing + follow_profile_config contracts', async () => { + // The PR #97008 contracts: room member sessions always rebuild from the + // member profile's CURRENT config on resume, never a stale stored + // model/provider pin. Dropping either param silently regresses rooms to + // the server's hidden + "Group: " title legacy fallback. + const room = await loadRoom() + + room.chat.updateGroupChat('Contract', current => { + current.roomId = 'r-contract' + + return current + }) + const handle = await room.turns.ensureGroupChatSession('Contract', { name: 'research', title: '' }) + + expect(room.gateway.sessions.get(String(handle.stored))?.contracts).toEqual({ + follow_profile_config: true, + room_plumbing: true + }) + }) + it('mints fresh member sessions when a same-name group is recreated after disband', async () => { const room = await loadRoom() const member: GroupMember = { name: 'research', title: '' } diff --git a/apps/desktop/src/plugins/hermes-bots/group-turns.ts b/apps/desktop/src/plugins/hermes-bots/group-turns.ts index b648885799..b9bd3e172c 100644 --- a/apps/desktop/src/plugins/hermes-bots/group-turns.ts +++ b/apps/desktop/src/plugins/hermes-bots/group-turns.ts @@ -198,7 +198,13 @@ export async function ensureGroupChatSession(group: string, member: GroupMember) profile: member.name, title, // Room member sessions are plumbing — always hidden from the sidebar. - hidden: true + hidden: true, + // Explicit contracts (PR #97008): room plumbing sessions always rebuild + // from the member profile's CURRENT config on resume, never a stale + // stored model/provider pin. Older gateways ignore the unknown params; + // the server's hidden + "Group: " title fallback then covers legacy. + room_plumbing: true, + follow_profile_config: true })) as { session_id?: string; stored_session_id?: string } const stored = created?.stored_session_id || null