diff --git a/apps/desktop/src/plugins/hermes-bots/plugin.js b/apps/desktop/src/plugins/hermes-bots/plugin.js index 2aacbe5be9..eed110e190 100644 --- a/apps/desktop/src/plugins/hermes-bots/plugin.js +++ b/apps/desktop/src/plugins/hermes-bots/plugin.js @@ -4149,6 +4149,24 @@ function createCanonicalChat(name) { const sid = res?.stored_session_id const runtime = res?.session_id + // session.create is intentionally lazy: its stored row does not exist until + // the first prompt. Mounting `sid` immediately therefore emits a noisy REST + // 404 ("Session not found"), and the turn-start auto-titler can win the race + // against the deferred `title: 'Bot Chat'` — under name-identity that is an + // identity outage: until the row is titled, the registry has no "Bot Chat" + // entry, so a second click during the intro turn mints a duplicate. + // session.title materializes the row now and records a user-authority title + // before either the open or kickoff, closing both the 404 race and the + // untitled window. Older gateways may not support the eager write; retain + // the kickoff-and-retry fallback below. + if (runtime) { + try { + await host.request('session.title', { session_id: runtime, title: CANONICAL_CHAT_TITLE }) + } catch { + /* compatibility fallback: prompt.submit will persist the lazy row */ + } + } + // Mount the session view FIRST, then send the kickoff — submitting into // an unmounted session left the intro reply invisible until reopen. let opened = false diff --git a/apps/desktop/src/plugins/hermes-bots/tests/canonical-chat-creation.test.mjs b/apps/desktop/src/plugins/hermes-bots/tests/canonical-chat-creation.test.mjs index e5b069bdd5..4174413ce9 100644 --- a/apps/desktop/src/plugins/hermes-bots/tests/canonical-chat-creation.test.mjs +++ b/apps/desktop/src/plugins/hermes-bots/tests/canonical-chat-creation.test.mjs @@ -22,7 +22,31 @@ function loadCanonicalCreation({ openSession, request }) { return { ...context.__canonical } } -test('regression: navigation retries after the kickoff persists a new canonical chat', async () => { +test('regression: creation materializes and titles the lazy row before opening it', async () => { + const events = [] + const runtime = loadCanonicalCreation({ + openSession: async id => events.push(`open:${id}`), + request: async (method, params) => { + events.push(method) + if (method === 'session.create') return { stored_session_id: 'stored-1', session_id: 'runtime-1' } + if (method === 'session.title') { + assert.deepEqual(params, { session_id: 'runtime-1', title: 'Bot Chat' }) + } + return {} + } + }) + + assert.equal(await runtime.createCanonicalChat('ops'), 'stored-1') + assert.deepEqual(events, [ + 'session.list', + 'session.create', + 'session.title', + 'open:stored-1', + 'prompt.submit' + ]) +}) + +test('compatibility: navigation retries after kickoff when eager title persistence is unavailable', async () => { const events = [] let attempts = 0 const runtime = loadCanonicalCreation({ @@ -33,6 +57,7 @@ test('regression: navigation retries after the kickoff persists a new canonical }, request: async method => { if (method === 'session.create') return { stored_session_id: 'stored-1', session_id: 'runtime-1' } + if (method === 'session.title') throw new Error('unknown method') if (method === 'prompt.submit') events.push('kickoff:persisted') return {} } diff --git a/contributors/emails/switchstatement@gmail.com b/contributors/emails/switchstatement@gmail.com new file mode 100644 index 0000000000..a8dbc4603c --- /dev/null +++ b/contributors/emails/switchstatement@gmail.com @@ -0,0 +1 @@ +krunkosaurus