refactor(bots): share the stale Bot Chat tile probe between the roster click and tip open

isStaleBotChatTile(canonicalIds) replaces the copy in openStoredBotChat, whose
try/catch duplicated the optional call; the stale-tiles test no longer
contradicts its own comment.
This commit is contained in:
Brooklyn Nicholson
2026-09-30 10:31:18 -05:00
parent 99d7269054
commit aecf596db9
5 changed files with 14 additions and 21 deletions

View File

@@ -18,9 +18,10 @@ const { openBotCanonicalChat, prepareBotSource } = vi.hoisted(() => ({
prepareBotSource: vi.fn()
}))
vi.mock('./canonical-chat', () => ({
vi.mock('./canonical-chat', async () => ({
CANONICAL_CHAT_TITLE: 'Bot Chat',
ensureBotMetadata: vi.fn(async () => ({})),
isStaleBotChatTile: (await vi.importActual<typeof import('./canonical-chat')>('./canonical-chat')).isStaleBotChatTile,
notifyBotOpenFailure: vi.fn(),
openBotCanonicalChat,
prepareBotSource,

View File

@@ -74,8 +74,7 @@ describe('120810: compressed Bot Chat discards the stale tile', () => {
// Live tip and registry row are never stale.
expect(probe({ storedSessionId: 'tip-9', workspaceTabTitle: 'Bot Chat' })).toBe(false)
expect(probe({ storedSessionId: 'root-1', workspaceTabTitle: 'Bot Chat' })).toBe(false)
// User + threads are never stale.
expect(probe({ storedSessionId: 'side-1', workspaceTabTitle: 'Bot Chat' })).toBe(true)
// A user + thread carries its own title, so it is never stale.
expect(probe({ storedSessionId: 'side-1', workspaceTabTitle: 'my thread' })).toBe(false)
// Tip opens after the probe.
expect(hostMock.openSession.mock.calls[0][0]).toBe('tip-9')

View File

@@ -60,6 +60,12 @@ interface CanonicalChatRow extends CanonicalSession {
readonly live_message_count?: SessionListRow['live_message_count']
}
/** A Bot Chat tile left on an old compression segment: titled as the canonical
* chat but keyed to none of the lineage ids the owner currently resolves to. */
export const isStaleBotChatTile =
(canonicalIds: readonly string[]) => (tile: { storedSessionId: string; workspaceTabTitle?: string }) =>
tile.workspaceTabTitle === CANONICAL_CHAT_TITLE && !canonicalIds.includes(String(tile.storedSessionId))
/** Should the open wait for a painted transcript? The paintable row count
* decides when the gateway reports it; the denormalized total is the only
* fallback older gateways offer, and no count at all means the row is
@@ -153,16 +159,7 @@ async function openStoredBotChat(
// old-segment tile is discarded here or it survives beside the tip
// (hermes-agent#120810). Same owner-scoped probe the roster click runs.
const canonicalIds = [...new Set([summary?.id, storedId].filter(Boolean).map(String))]
try {
host.focusOpenWorkspaceSession?.(
ownerKey,
tile => tile.workspaceTabTitle === CANONICAL_CHAT_TITLE && !canonicalIds.includes(String(tile.storedSessionId)),
canonicalIds
)
} catch {
// Older shells: the tip open below still lands.
}
host.focusOpenWorkspaceSession?.(ownerKey, isStaleBotChatTile(canonicalIds), canonicalIds)
await host.openSession(storedId, {
...(route

View File

@@ -16,9 +16,10 @@ const { openBotCanonicalChat, prepareBotSource } = vi.hoisted(() => ({
prepareBotSource: vi.fn()
}))
vi.mock('./canonical-chat', () => ({
vi.mock('./canonical-chat', async () => ({
CANONICAL_CHAT_TITLE: 'Bot Chat',
ensureBotMetadata: vi.fn(async () => ({})),
isStaleBotChatTile: (await vi.importActual<typeof import('./canonical-chat')>('./canonical-chat')).isStaleBotChatTile,
notifyBotOpenFailure: vi.fn(),
openBotCanonicalChat,
prepareBotSource,

View File

@@ -18,7 +18,7 @@ import {
rosterWatermarks,
saveSelectedRosterBot
} from './bot-state'
import { CANONICAL_CHAT_TITLE, notifyBotOpenFailure, openBotCanonicalChat, prepareBotSource } from './canonical-chat'
import { isStaleBotChatTile, notifyBotOpenFailure, openBotCanonicalChat, prepareBotSource } from './canonical-chat'
import { $botMeta, botActivitySession, botRosterKey, botSelectionKey, newBotChat } from './data'
import { $groupChats, $groupChatWorkspace } from './group-chat'
import { openGroupChat } from './group-chat-view'
@@ -197,13 +197,8 @@ function focusExistingBotTab(bot: RosterRow): null | { registryId: string; store
return null
}
const isStaleTile = (tile: { storedSessionId: string; workspaceTabTitle?: string }) =>
typeof tile.workspaceTabTitle === 'string' &&
tile.workspaceTabTitle === CANONICAL_CHAT_TITLE &&
!canonicalIds.includes(String(tile.storedSessionId))
try {
const focused = host.focusOpenWorkspaceSession(botWorkspaceOwnerKey(bot), isStaleTile, canonicalIds)
const focused = host.focusOpenWorkspaceSession(botWorkspaceOwnerKey(bot), isStaleBotChatTile(canonicalIds), canonicalIds)
return typeof focused === 'string' && focused
? { registryId: String(canonical!.id), storedSessionId: focused }