fix(desktop): a failed Bot Chat registry lookup no longer forks the bot's forever chat
findExistingCanonicalChat() swallowed every lookup error and returned null — indistinguishable from 'this bot has no Bot Chat yet' — so a transient RPC failure against a just-restarted backend (the exact post-desktop-update window) sent createCanonicalChat() straight to session.create, minting a fresh 'Bot Chat' while the real one (data intact, hidden) still held the canonical title. Users experienced this as bots losing all context after every desktop update. The lookup now fails CLOSED: a failed registry consultation throws, both open paths surface their existing 'try again' toast, and session.create can never fire off an unknown ownership state. Tests: two new VM-executed regression tests (sabotage-verified — both fail with the old fail-open catch); hide-bot-chats source-shape regex updated for the new layout. 364/364 plugin tests green.
This commit is contained in:
@@ -4096,18 +4096,31 @@ function isCanonicalBotChatHistory(history) {
|
||||
* (window-free; a busy profile can push the forever-chat past any recency
|
||||
* window). include_hidden is required (canonical chats are always hidden). */
|
||||
async function findExistingCanonicalChat(name) {
|
||||
// FAIL CLOSED. A failed registry lookup MUST NOT read as "no Bot Chat
|
||||
// exists" — that is the one remaining way to fork a bot's forever chat.
|
||||
// The failure lives exactly in the post-update window: the desktop
|
||||
// restarts every profile backend, the first bot click races the warm-up,
|
||||
// the lookup RPC fails transiently, and a swallowed error here sent
|
||||
// createCanonicalChat() straight to session.create — minting a fresh
|
||||
// "Bot Chat" while the real one (data intact, hidden) still held the
|
||||
// canonical title. Users read that as "my bot lost all context after the
|
||||
// update". Both open paths catch and toast "try again", which is the
|
||||
// correct outcome for a transient lookup failure: retry, never mint.
|
||||
let res
|
||||
try {
|
||||
const res = await host.request('session.list', {
|
||||
res = await host.request('session.list', {
|
||||
profile: name,
|
||||
title: CANONICAL_CHAT_TITLE,
|
||||
limit: PROFILE_SESSION_LIST_LIMIT,
|
||||
include_hidden: true
|
||||
})
|
||||
const rows = res?.sessions ?? []
|
||||
return rows.find(row => isCanonicalBotChatHistory(row)) || null
|
||||
} catch {
|
||||
return null
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error && error.message ? ` (${error.message})` : ''
|
||||
throw new Error(`Could not check ${name}'s Bot Chat registry${detail} — not starting a new chat`)
|
||||
}
|
||||
|
||||
const rows = res?.sessions ?? []
|
||||
return rows.find(row => isCanonicalBotChatHistory(row)) || null
|
||||
}
|
||||
|
||||
/** Create the bot's ONE forever chat: a real session titled "Bot Chat",
|
||||
|
||||
@@ -175,3 +175,50 @@ test('an ordinary titled session never satisfies the registry lookup', async ()
|
||||
'no row titled "Bot Chat" → create; never adopt an ordinary conversation')
|
||||
assert.ok(!runtime.opened.some(o => o.id === 'scratch'))
|
||||
})
|
||||
|
||||
// ── 4. a failed lookup fails CLOSED — never "no chat exists" ────────────────
|
||||
//
|
||||
// The post-update window: the desktop restarts every profile backend, the
|
||||
// first bot click races the warm-up, and the registry lookup RPC fails
|
||||
// transiently. Swallowing that error and returning null made the failure
|
||||
// indistinguishable from "this bot has no Bot Chat yet", so the create path
|
||||
// minted a fresh forever-chat while the real one (data intact, hidden) still
|
||||
// held the canonical title — read by users as "my bot lost everything after
|
||||
// the update". A lookup failure must surface (the open paths toast
|
||||
// "try again"), never resolve to mint.
|
||||
|
||||
test('a failed registry lookup rejects instead of minting a replacement chat', async () => {
|
||||
const runtime = loadOpenPath({
|
||||
request: async method => {
|
||||
if (method === 'session.list') {
|
||||
throw new Error('gateway not ready')
|
||||
}
|
||||
if (method === 'session.create') {
|
||||
throw new Error('must not create: a failed lookup is not "no chat exists"')
|
||||
}
|
||||
return {}
|
||||
}
|
||||
})
|
||||
|
||||
await assert.rejects(() => runtime.openBotCanonicalChat('ops'), /Bot Chat registry/)
|
||||
assert.ok(!runtime.requests.some(r => r.method === 'session.create'),
|
||||
'session.create must never fire off a failed lookup')
|
||||
assert.equal(runtime.opened.length, 0)
|
||||
})
|
||||
|
||||
test('createCanonicalChat also refuses to mint when the adoption lookup fails', async () => {
|
||||
const runtime = loadOpenPath({
|
||||
request: async method => {
|
||||
if (method === 'session.list') {
|
||||
throw new Error('backend warming up')
|
||||
}
|
||||
if (method === 'session.create') {
|
||||
throw new Error('must not create: adoption check failed, ownership is unknown')
|
||||
}
|
||||
return {}
|
||||
}
|
||||
})
|
||||
|
||||
await assert.rejects(() => runtime.createCanonicalChat('ops'), /Bot Chat registry/)
|
||||
assert.ok(!runtime.requests.some(r => r.method === 'session.create'))
|
||||
})
|
||||
|
||||
@@ -157,5 +157,5 @@ test('the canonical-chat adoption scan lists with include_hidden', () => {
|
||||
// findExistingCanonicalChat (the registry lookup) — canonical Bot Chats
|
||||
// are born hidden, so a visible-only scan would miss the very row that IS
|
||||
// the bot's identity.
|
||||
assert.match(source, /include_hidden: true\s*\}\)\s*const rows = res\?\.sessions \?\? \[\]\s*return rows\.find\(row => isCanonicalBotChatHistory\(row\)\)/)
|
||||
assert.match(source, /include_hidden: true\s*\}\)\s*\} catch \(error\) \{[\s\S]{0,400}?const rows = res\?\.sessions \?\? \[\]\s*return rows\.find\(row => isCanonicalBotChatHistory\(row\)\)/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user