feat(bot-mode): a reclaimed bot chat re-resumes itself — no stale-id error on the next send

When the gateway reaps the runtime behind the open bot chat (idle TTL,
LRU cap, or the WS-orphan mass reap that killed every background bot's
handle at once in the Aug 23 incident), the plugin now hears
session.reclaimed and re-resumes the canonical chat immediately, instead
of leaving the dead handle for the user's next send to trip over.

Matched on the stored id against both claim identities; guarded by the
open generation so a user action mid-re-resume wins; a failed re-resume
is swallowed — the next-send recovery ladder (#92928) stays the
backstop. Feature-detected on host.onEvent; disposed with the other
listeners.
This commit is contained in:
Teknium
2026-08-23 05:54:47 -07:00
parent f530cd2b54
commit d5281f5981
2 changed files with 104 additions and 0 deletions
@@ -14091,6 +14091,58 @@ export default {
})
: null
// Proactive reclaim refresh: when the gateway reaps the runtime behind
// the OPEN bot chat (idle TTL, LRU cap, WS-orphan reap — the mass-reap
// shape hits every background bot at once), re-resume the canonical
// chat immediately instead of letting the user's next send eat the
// stale-id error + recovery retry. Matched on the STORED id (the
// claim's ids are stored ids; the payload carries both). Best-effort:
// a failed re-resume (backend still down) leaves the lazy recovery on
// next send as the backstop. Feature-detected — older shells have no
// host.onEvent.
const stopReclaimSync =
typeof host.onEvent === 'function'
? host.onEvent('session.reclaimed', event => {
const payload = event?.payload || {}
const stored = String(payload.stored_session_id || '')
const claim = $openBotChat.get()
if (!stored || !claim) {
return
}
const owned = [claim.openedSessionId, claim.openedRegistryId].filter(Boolean)
if (!owned.includes(stored)) {
return
}
const bot = selectedRosterBot($lastRoster.get(), $selectedRosterKey.get())
if (!bot) {
return
}
const generation = botOpenGeneration
void openBotCanonicalChat(bot)
.then(opened => {
// A user action while the re-resume ran owns the center now.
if (!opened || generation !== botOpenGeneration) {
return
}
$openBotChat.set({
key: claim.key,
openedRegistryId: opened.registryId,
openedSessionId: opened.openedId
})
})
.catch(() => {
/* backend still down — next send recovers via the ladder */
})
})
: null
$botsPaneVisible.set(Boolean($sidebarVisible.get()))
$botChatFocused.set(sessionOwnsWorkspace())
$botsHomeFronted.set(Boolean(homeVisibleStore.get()))
@@ -14109,6 +14161,7 @@ export default {
stopGroupSync()
stopHomeVisibleSync()
stopFocusSync?.()
stopReclaimSync?.()
$botsHomeFronted.set(false)
closeBotsHomeWorkspace()
})
@@ -0,0 +1,51 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
// Proactive reclaim refresh (polish on top of #92928/#92901): when the
// gateway reaps the runtime behind the OPEN bot chat (session.reclaimed —
// idle TTL, LRU, or the mass WS-orphan reap that killed every background
// bot's handle at once in the Aug 23 incident), the plugin re-resumes the
// canonical chat immediately instead of letting the user's next send eat a
// stale-id error + recovery round-trip.
//
// Source-shape contracts:
// - the listener subscribes via host.onEvent('session.reclaimed'), feature-
// detected, and is disposed with the other listeners;
// - the match is on the STORED id against BOTH claim identities;
// - a stale generation or missing claim/bot is a no-op;
// - a failed re-resume is swallowed (next-send recovery ladder remains).
const pluginSource = readFileSync(new URL('../plugin.js', import.meta.url), 'utf8')
function reclaimBlock() {
const start = pluginSource.indexOf("host.onEvent('session.reclaimed'")
assert.ok(start > 0, 'reclaim listener exists')
return pluginSource.slice(start - 400, start + 1800)
}
test('listener is feature-detected and disposed', () => {
const block = reclaimBlock()
assert.match(block, /typeof host\.onEvent === 'function'/)
assert.match(pluginSource, /stopReclaimSync\?\.\(\)/)
})
test('match is stored-id against both claim identities', () => {
const block = reclaimBlock()
assert.match(block, /payload\.stored_session_id/)
assert.match(block, /\[claim\.openedSessionId, claim\.openedRegistryId\]\.filter\(Boolean\)/)
assert.match(block, /owned\.includes\(stored\)/)
})
test('re-resume guards the open generation and refreshes both claim ids', () => {
const block = reclaimBlock()
assert.match(block, /generation !== botOpenGeneration/)
assert.match(block, /openedRegistryId: opened\.registryId/)
assert.match(block, /openedSessionId: opened\.openedId/)
})
test('failed re-resume is swallowed — next-send recovery stays the backstop', () => {
const block = reclaimBlock()
assert.match(block, /\.catch\(\(\) => \{/)
assert.match(block, /next send recovers via the ladder/)
})