fix(desktop): a canonical-title race adopts the winner instead of forking the forever chat (#92473, part 2)

Between the registry miss and the eager session.title write, another
writer can take the canonical title (peer dm minting server-side, a
second machine, cross-connection sync). UNIQUE(title) rejects our write
with 'already in use' — which the compat path previously read as 'old
gateway' and prompted into OUR stray lazy session, forking the forever
chat. A uniqueness rejection now re-consults the registry and adopts the
winner; the zero-message stray is abandoned to the gateway pruner.
Genuine old-gateway failures (unknown method) keep the compat kickoff.
This commit is contained in:
Teknium
2026-08-26 03:46:33 -07:00
parent 19fde8a450
commit d22e2b9f6e
2 changed files with 107 additions and 1 deletions
+20 -1
View File
@@ -5798,7 +5798,26 @@ function createCanonicalChat(owner, { kickoff = false } = {}) {
try {
await requestForBot(bot, 'session.title', { session_id: runtime, title: CANONICAL_CHAT_TITLE })
titled = true
} catch {
} catch (error) {
// ADOPT-BEFORE-MINT: a title-uniqueness rejection is not an old
// gateway — it means another writer took the canonical title between
// our registry miss and this write (peer dm minting server-side, a
// second machine, cross-connection sync). Falling through to the
// compat path would prompt into OUR stray session and fork the
// forever chat. Re-consult the registry and adopt the winner; the
// stray lazy session holds zero messages and is simply abandoned
// (the gateway prunes it).
if (/already in use/i.test(String(error?.message || ''))) {
const winner = await findExistingCanonicalChat(owner)
if (winner?.id) {
if (typeof host.openSession === 'function') {
await openStoredBotChat(owner, winner.resolved_id || winner.id, winner)
}
return winner.id
}
}
/* compatibility fallback: prompt.submit will persist the lazy row */
}
}
@@ -0,0 +1,87 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import vm from 'node:vm'
// ADOPT-BEFORE-MINT (#92473 part 2): between the registry miss and our eager
// session.title write, another writer can take the canonical title (peer dm
// minting server-side, a second machine, cross-connection sync). UNIQUE(title)
// rejects our write with "already in use". Before this fix that rejection was
// read as "old gateway" and the compat path prompted into OUR stray lazy
// session — forking the forever chat. Now the mint re-consults the registry
// and adopts the winner; the stray zero-message session is abandoned to the
// gateway's pruner.
const source = readFileSync(new URL('../plugin.js', import.meta.url), 'utf8')
function loadCanonicalCreation({ openSession, request }) {
const start = source.indexOf('const canonicalCreations = new Map()')
const end = source.indexOf('function displayName(', start)
const context = {
host: { openSession, request },
backendTargetProfile: (route, name) => route?.targetProfile || name,
botOwner: name => (typeof name === 'string'
? { bot: { name }, key: name, name, route: null }
: { bot: name, key: name?.name, name: name?.name, route: name?.route || null }),
requestForBot: (_bot, method, params) => context.host.request(method, params),
botWorkspaceOwnerKey: bot => String(bot?.name || bot || ''),
window: { setTimeout: callback => callback() }
}
const section = source
.slice(start, end)
.concat('\nglobalThis.__canonical = { createCanonicalChat };\n')
assert.notEqual(start, -1, 'canonical creation section is missing')
assert.notEqual(end, -1, 'canonical creation section delimiter is missing')
vm.runInNewContext(section, context, { filename: 'canonical-adopt.js' })
return { ...context.__canonical }
}
test('title-uniqueness rejection adopts the racing winner instead of forking', async () => {
const events = []
let lists = 0
const runtime = loadCanonicalCreation({
openSession: async id => events.push(`open:${id}`),
request: async method => {
events.push(method)
if (method === 'session.list') {
lists += 1
// First consult: registry miss (this is why we mint at all). Second
// consult (after the conflict): the racing winner exists.
if (lists === 1) return { sessions: [] }
return { sessions: [{ id: 'winner-1', resolved_id: 'winner-1', title: 'Bot Chat', message_count: 3 }] }
}
if (method === 'session.create') return { stored_session_id: 'stray-1', session_id: 'rt-stray' }
if (method === 'session.title') {
throw new Error("Title 'Bot Chat' is already in use by session winner-1")
}
return {}
}
})
assert.equal(await runtime.createCanonicalChat('ops'), 'winner-1')
// The winner is opened; the stray is never prompted into or opened.
assert.ok(events.includes('open:winner-1'), `winner opened (events: ${events})`)
assert.ok(!events.includes('prompt.submit'), 'no prompt into the stray session')
assert.ok(!events.includes('open:stray-1'), 'stray session never mounted')
})
test('a NON-conflict title failure still takes the compat path (old gateways)', async () => {
const events = []
const runtime = loadCanonicalCreation({
openSession: async id => events.push(`open:${id}`),
request: async (method) => {
events.push(method)
if (method === 'session.list') return { sessions: [] }
if (method === 'session.create') return { stored_session_id: 'stored-1', session_id: 'rt-1' }
if (method === 'session.title') throw new Error('unknown method')
return {}
}
})
assert.equal(await runtime.createCanonicalChat('ops'), 'stored-1')
// Old gateway: eager title unsupported → the kickoff persists the lazy row.
assert.ok(events.includes('prompt.submit'), 'compat kickoff persists the session')
// Only the initial registry consult — a plain failure must not re-list.
assert.equal(events.filter(e => e === 'session.list').length, 1)
})