fix(bot-mode): canonical-chat adoption survives busy profiles via exact-title lookup
The #90732 adoption scan used session.list's 200-row recency window. A busy bot profile (group-chat traffic, routines, or accumulated fork spam) pushes an older forever-chat past row 200, the scan misses it, and the mint path re-enters the unique-title-conflict fork loop — same pathology, higher trigger threshold. Profile → Named Session is an exact registry (UNIQUE title index), so consult it exactly: - session.list gains a `title` param: indexed WHERE title = ? lookup, window-free, hidden rows resolve, archived/deny-listed do not, compression lineages resolve to the live tip (resolved_id), mirroring profiles.list's preferred_session resolver. - findExistingCanonicalChat sends title: 'Bot Chat'. Older gateways ignore the unknown param and return the windowed listing — the local scan stays as the compatibility rung. - Adoption opens the lineage tip (resolved_id) while pinning the durable id, same split as the preferred_session path.
This commit is contained in:
@@ -3384,17 +3384,24 @@ async function openStoredBotChat(name, storedId, summary) {
|
||||
/** Adopt-before-mint: the profile may already own a canonical Bot Chat that
|
||||
* the pin lost track of (pin cleared during an outage, ui_meta rolled back,
|
||||
* a fork squatting the title). The core UNIQUE title index guarantees at
|
||||
* most ONE session titled "Bot Chat" per profile db, so a title scan is an
|
||||
* exact registry lookup, not a heuristic. Minting while a "Bot Chat" row
|
||||
* exists is always wrong twice over: it forks the forever-chat AND the new
|
||||
* row can never take the (already held) canonical title, so the next
|
||||
* identity check misreads it and forks again — the infinite-fork loop.
|
||||
* include_hidden is required (canonical chats are always hidden); an older
|
||||
* gateway without it simply finds nothing and we fall through to mint. */
|
||||
* most ONE session titled "Bot Chat" per profile db — Profile → Named
|
||||
* Session is an exact registry, so consult it exactly: `title` asks the
|
||||
* gateway for an indexed WHERE title = ? lookup (window-free; a busy
|
||||
* profile can push the forever-chat past any recency window, which would
|
||||
* re-open the fork loop with a higher trigger threshold). Minting while a
|
||||
* "Bot Chat" row exists is always wrong twice over: it forks the
|
||||
* forever-chat AND the new row can never take the (already held) canonical
|
||||
* title, so the next identity check misreads it and forks again — the
|
||||
* infinite-fork loop. An older gateway ignores the unknown `title` param
|
||||
* and returns the plain windowed listing instead — the pre-exact-lookup
|
||||
* behavior — so the local scan below stays as the compatibility rung.
|
||||
* include_hidden is required (canonical chats are always hidden); a gateway
|
||||
* without it simply finds nothing and we fall through to mint. */
|
||||
async function findExistingCanonicalChat(name) {
|
||||
try {
|
||||
const res = await host.request('session.list', {
|
||||
profile: name,
|
||||
title: 'Bot Chat',
|
||||
limit: PROFILE_SESSION_LIST_LIMIT,
|
||||
include_hidden: true
|
||||
})
|
||||
@@ -3424,7 +3431,10 @@ function createCanonicalChat(name) {
|
||||
saveBotMeta(name, { chat: existing.id })
|
||||
|
||||
if (typeof host.openSession === 'function') {
|
||||
await openStoredBotChat(name, existing.id, existing)
|
||||
// The exact-lookup gateway reports the compression-lineage tip as
|
||||
// resolved_id; the pin stays the durable row id (same split the
|
||||
// preferred_session path uses).
|
||||
await openStoredBotChat(name, existing.resolved_id || existing.id, existing)
|
||||
}
|
||||
|
||||
return existing.id
|
||||
|
||||
@@ -177,6 +177,49 @@ def _(rid, params: dict) -> dict:
|
||||
# their own source.
|
||||
deny = frozenset({"kanban", "tool"})
|
||||
|
||||
# ``title``: EXACT-title registry lookup, not a listing. The core
|
||||
# UNIQUE title index means at most one session per db carries a
|
||||
# given exact title, so callers that treat a title as an identity
|
||||
# key (Bot Mode's canonical "Bot Chat" — Profile → Named Session)
|
||||
# get a window-free O(1) answer instead of scanning a recency
|
||||
# window that a busy profile can push the row out of. Hidden rows
|
||||
# resolve (canonical chats are born hidden); archived rows and
|
||||
# deny-listed sources do not; compression lineages resolve to the
|
||||
# live tip (``resolved_id``), mirroring profiles.list's
|
||||
# preferred_session resolver. Older clients never send this param;
|
||||
# newer clients falling back to older gateways just get the normal
|
||||
# windowed listing back (the param is ignored) and scan it.
|
||||
title_lookup = str(params.get("title") or "").strip()
|
||||
if title_lookup:
|
||||
row = db.get_session_by_title(title_lookup)
|
||||
if (
|
||||
not row
|
||||
or row.get("archived")
|
||||
or (row.get("source") or "").strip().lower() in deny
|
||||
):
|
||||
return _ok(rid, {"sessions": []})
|
||||
try:
|
||||
tip = db.resolve_resume_session_id(row["id"]) or row["id"]
|
||||
except Exception:
|
||||
tip = row["id"]
|
||||
tip_row = (db.get_session(tip) or row) if tip != row["id"] else row
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"sessions": [
|
||||
{
|
||||
"id": row["id"],
|
||||
"resolved_id": tip,
|
||||
"title": row.get("title") or "",
|
||||
"preview": tip_row.get("preview") or "",
|
||||
"started_at": row.get("started_at") or 0,
|
||||
"message_count": tip_row.get("message_count") or 0,
|
||||
"source": row.get("source") or "",
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
limit = int(params.get("limit", 200) or 200)
|
||||
# ``include_hidden``: surfaces that OWN hidden sessions (the Bots
|
||||
# pane's per-profile browser, plugin session pickers) need to list
|
||||
|
||||
Reference in New Issue
Block a user