fix(bot-mode): always hide Bot Mode sessions from the global Sessions sidebar
Bot Mode's group chats spawned one per-member session per room, and those "Group: ..." rows (plus canonical Bot Chats when the old eye-toggle pref was off) flooded the global Sessions sidebar — a 6-bot room dumped six identical rows into recents (reported with screenshot, Aug 17). Plugin (apps/desktop/src/plugins/hermes-bots/plugin.js): - session.create now passes hidden:true UNCONDITIONALLY for both canonical Bot Chats and group-room member sessions; the $hideBotChats pref, its eye toggle, and its storage hydrate are removed (Bot Mode sessions are plumbing or plugin-owned forever-chats, never scratch conversations). - hideOwnedBotSessions(): idempotent reconciliation sweep over every owned session id (bot meta canonical chats + each room's member sessions) via session.set_hidden, run on plugin load and on each gateway reconnect, so rows born visible under the old pref get cleaned up. - The Bots session browser and canonical-chat recovery scan pass include_hidden:true so they still see the rows they own. Gateway (tui_gateway/methods_session.py): - session.list honors an include_hidden param (default off — the resume picker and all global callers keep dropping hidden rows). - session.set_hidden gains a durable fallback: when no LIVE runtime session matches, resolve the stored session id in the target profile's state.db (via resolve_session_id) and flip the flag there. The sweep holds stored ids for chats that aren't live; the old live-only lookup 4001'd them. Validated E2E with real imports against a temp HERMES_HOME: born-hidden row (hidden=1), profile-scoped session.list default vs include_hidden (0 vs 1), and stored-id sweep on a non-live legacy row (hidden=1). Plugin suite 167/167; new RPC regression tests in tests/tui_gateway/test_session_hidden_rpc.py.
This commit is contained in:
@@ -152,13 +152,14 @@ function trackInboundActivity(roster) {
|
||||
/** Last good cron list, same idea as the roster snapshot. */
|
||||
const $lastJobs = atom([])
|
||||
|
||||
/** User pref: hide canonical "Bot Chat" sessions from the global Sessions
|
||||
* sidebar (they always remain in the Bots roster). Persisted via ctx.storage.
|
||||
* Default ON — Bot Chats are plugin-owned forever-chats, not scratch sessions,
|
||||
* so keeping them out of the shared recents list is the expected behavior.
|
||||
* Backed by the core generic `hidden` session flag (session.create hidden:true
|
||||
* / session.set_hidden); older gateways ignore it and Bot Chats stay visible. */
|
||||
const $hideBotChats = atom(true)
|
||||
// Bot Mode sessions are ALWAYS hidden from the global Sessions sidebar:
|
||||
// canonical Bot Chats are plugin-owned forever-chats and group-chat member
|
||||
// sessions are room plumbing — neither is a scratch conversation, and a
|
||||
// 6-member room would otherwise dump six identical "Group: ..." rows into
|
||||
// recents. Backed by the core generic `hidden` session flag (session.create
|
||||
// hidden:true / session.set_hidden); the Bots pane browses them via
|
||||
// session.list include_hidden. Older gateways ignore the flag and the
|
||||
// sessions simply stay visible there.
|
||||
|
||||
/** Bot the Routines tile is scoped to. Follows the live gateway profile
|
||||
* (the bot you're actually chatting with) and roster clicks. */
|
||||
@@ -268,27 +269,25 @@ async function saveBotMeta(name, patch) {
|
||||
return { serverPersisted: serverOutcome === 'persisted', serverOutcome }
|
||||
}
|
||||
|
||||
/** Flip the "hide Bot Chats from the sidebar" pref, persist it, and reconcile
|
||||
* every known canonical chat via the core session.set_hidden RPC so the change
|
||||
* applies to already-created Bot Chats (not just future ones). Feature-detected:
|
||||
* older gateways lack session.set_hidden and simply keep the chats visible. */
|
||||
async function setHideBotChats(hidden) {
|
||||
$hideBotChats.set(hidden)
|
||||
|
||||
try {
|
||||
Promise.resolve(pluginCtx?.storage?.set?.('hide-bot-chats', hidden)).catch(() => undefined)
|
||||
} catch {
|
||||
/* storage unavailable — pref holds for this window only */
|
||||
}
|
||||
|
||||
const meta = $botMeta.get()
|
||||
const ids = Object.values(meta)
|
||||
/** One-time reconciliation: Bot Mode sessions are always hidden, but rooms
|
||||
* and Bot Chats created before this policy (or while the old pref was off)
|
||||
* left visible rows behind. On every plugin load, sweep every session id we
|
||||
* own — canonical chats from bot meta plus each group room's member
|
||||
* sessions — through the core session.set_hidden RPC. Idempotent (the DB
|
||||
* setter is a no-op on already-hidden rows) and feature-detected: older
|
||||
* gateways lack session.set_hidden and simply keep the rows visible. */
|
||||
function hideOwnedBotSessions() {
|
||||
const canonical = Object.values($botMeta.get())
|
||||
.map(m => m && m.chat)
|
||||
.filter(Boolean)
|
||||
const rooms = Object.values($groupChats.get())
|
||||
.flatMap(room => Object.values(room?.sessions || {}))
|
||||
.filter(sid => Boolean(sid) && sid !== true)
|
||||
const ids = [...new Set([...canonical, ...rooms])]
|
||||
|
||||
await Promise.all(
|
||||
return Promise.all(
|
||||
ids.map(sid =>
|
||||
Promise.resolve(host.request('session.set_hidden', { session_id: sid, hidden })).catch(() => undefined)
|
||||
Promise.resolve(host.request('session.set_hidden', { session_id: sid, hidden: true })).catch(() => undefined)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -2526,7 +2525,8 @@ async function ensureRemoteCanonicalChat(route, profile) {
|
||||
const created = await host.requestProfile(route, 'session.create', {
|
||||
profile,
|
||||
title: 'Bot Chat',
|
||||
...($hideBotChats.get() ? { hidden: true } : {})
|
||||
// Bot Mode sessions are always hidden from the global sidebar.
|
||||
hidden: true
|
||||
})
|
||||
|
||||
return { runtime: created?.session_id || null, stored: created?.stored_session_id || null }
|
||||
@@ -2744,10 +2744,11 @@ function createCanonicalChat(name) {
|
||||
const res = await host.request('session.create', {
|
||||
profile: name,
|
||||
title: 'Bot Chat',
|
||||
// Born hidden from the global sidebar when the pref is on. Core applies
|
||||
// this via the generic `hidden` flag (deferred as pending_hidden until the
|
||||
// row exists); older gateways ignore the unknown param and it stays visible.
|
||||
...($hideBotChats.get() ? { hidden: true } : {})
|
||||
// Always born hidden from the global sidebar — Bot Mode sessions are
|
||||
// plugin-owned. Core applies this via the generic `hidden` flag
|
||||
// (deferred as pending_hidden until the row exists); older gateways
|
||||
// ignore the unknown param and it stays visible.
|
||||
hidden: true
|
||||
})
|
||||
const sid = res?.stored_session_id
|
||||
const runtime = res?.session_id
|
||||
@@ -3353,7 +3354,8 @@ async function ensureGroupChatSession(group, member) {
|
||||
const created = await requestForBot(member, 'session.create', {
|
||||
profile: member.name,
|
||||
title,
|
||||
...($hideBotChats.get() ? { hidden: true } : {})
|
||||
// Room member sessions are plumbing — always hidden from the sidebar.
|
||||
hidden: true
|
||||
})
|
||||
const stored = created?.stored_session_id || null
|
||||
|
||||
@@ -6673,7 +6675,9 @@ function useProfileSessions(botName, gatewayGeneration) {
|
||||
return useQuery({
|
||||
queryKey: [ID, 'profile-sessions', botName, gatewayGeneration],
|
||||
enabled: Boolean(botName),
|
||||
queryFn: () => host.request('session.list', { profile: botName, limit: PROFILE_SESSION_LIST_LIMIT }),
|
||||
// include_hidden: this browser exists precisely to see the profile's own
|
||||
// (always-hidden) Bot Mode sessions alongside its regular ones.
|
||||
queryFn: () => host.request('session.list', { profile: botName, limit: PROFILE_SESSION_LIST_LIMIT, include_hidden: true }),
|
||||
refetchInterval: 8000,
|
||||
staleTime: 4000,
|
||||
retry: false
|
||||
@@ -7306,7 +7310,6 @@ function BotsPane() {
|
||||
const [deleting, setDeleting] = useState(null)
|
||||
const [grouping, setGrouping] = useState(null)
|
||||
const [query, setQuery] = useState('')
|
||||
const hideBotChats = useValue($hideBotChats)
|
||||
const activityToasts = useValue($activityToasts)
|
||||
const sessionsWorkspaceName = useValue($botSessionsWorkspace)
|
||||
const groupChatName = useValue($groupChatWorkspace)
|
||||
@@ -7424,16 +7427,6 @@ function BotsPane() {
|
||||
children: jsx(Codicon, { name: activityToasts ? 'bell' : 'bell-slash' })
|
||||
})
|
||||
}),
|
||||
jsx(Tip, {
|
||||
label: hideBotChats ? 'Bot Chats hidden from Sessions — click to show' : 'Bot Chats shown in Sessions — click to hide',
|
||||
children: jsx('button', {
|
||||
type: 'button',
|
||||
className:
|
||||
'flex size-6 items-center justify-center rounded-md text-(--ui-text-tertiary) transition-colors hover:bg-(--chrome-action-hover) hover:text-foreground',
|
||||
onClick: () => void setHideBotChats(!hideBotChats),
|
||||
children: jsx(Codicon, { name: hideBotChats ? 'eye-closed' : 'eye' })
|
||||
})
|
||||
}),
|
||||
jsxs(DropdownMenu, {
|
||||
children: [
|
||||
jsx(Tip, {
|
||||
@@ -7792,18 +7785,9 @@ export default {
|
||||
/* no storage on this shell — defaults stay */
|
||||
}
|
||||
|
||||
// Hydrate the "hide Bot Chats from the sidebar" pref (default ON).
|
||||
try {
|
||||
Promise.resolve(ctx.storage?.get?.('hide-bot-chats'))
|
||||
.then(value => {
|
||||
if (typeof value === 'boolean') {
|
||||
$hideBotChats.set(value)
|
||||
}
|
||||
})
|
||||
.catch(() => undefined)
|
||||
} catch {
|
||||
/* no storage — default (hide) stays */
|
||||
}
|
||||
// Bot Mode sessions are always hidden now — the old "hide Bot Chats"
|
||||
// pref is gone (its stored key is simply ignored). The reconciliation
|
||||
// sweep below hides any rows born visible under the old pref.
|
||||
|
||||
// Hydrate the activity-toast pref (default OFF).
|
||||
try {
|
||||
@@ -7855,6 +7839,25 @@ export default {
|
||||
})
|
||||
host.state.gateway.listen(handleSessionsGatewayTransition)
|
||||
|
||||
// Reconciliation sweep: hide every Bot Mode session we know about, on
|
||||
// load and again on each reconnect (a swap can land on a gateway whose
|
||||
// rows were created before the always-hidden policy). Deferred a tick so
|
||||
// the meta/room storage hydrates above have landed; idempotent after that.
|
||||
// (Feature-guarded: bare vm test harnesses have no setTimeout global.)
|
||||
const scheduleHideSweep = () => {
|
||||
try {
|
||||
setTimeout(() => void hideOwnedBotSessions(), 0)
|
||||
} catch {
|
||||
void hideOwnedBotSessions()
|
||||
}
|
||||
}
|
||||
host.state.gateway.listen(state => {
|
||||
if (state === 'open') {
|
||||
scheduleHideSweep()
|
||||
}
|
||||
})
|
||||
scheduleHideSweep()
|
||||
|
||||
ctx.register({
|
||||
id: 'pane',
|
||||
area: 'panes',
|
||||
|
||||
@@ -3,14 +3,16 @@ import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import vm from 'node:vm'
|
||||
|
||||
// #46: canonical "Bot Chat" sessions can be hidden from the global Sessions
|
||||
// sidebar via the core generic `hidden` session flag, while staying in the Bots
|
||||
// roster. The plugin passes hidden:true on session.create when the pref is on,
|
||||
// and setHideBotChats reconciles existing chats via session.set_hidden.
|
||||
// Bot Mode sessions are ALWAYS hidden from the global Sessions sidebar
|
||||
// (canonical Bot Chats and group-chat member sessions alike) via the core
|
||||
// generic `hidden` session flag. There is no user pref: session.create
|
||||
// passes hidden:true unconditionally, and hideOwnedBotSessions() sweeps
|
||||
// every known plugin-owned session id through session.set_hidden so rows
|
||||
// born visible under the old pref get reconciled.
|
||||
|
||||
const source = readFileSync(new URL('../plugin.js', import.meta.url), 'utf8')
|
||||
|
||||
function loadCreate(hidePref) {
|
||||
function loadCreate() {
|
||||
const start = source.indexOf('const canonicalCreations = new Map()')
|
||||
const end = source.indexOf('function displayName(', start)
|
||||
const created = []
|
||||
@@ -26,7 +28,6 @@ function loadCreate(hidePref) {
|
||||
}
|
||||
},
|
||||
saveBotMeta: () => {},
|
||||
$hideBotChats: { get: () => hidePref },
|
||||
window: { setTimeout: cb => cb() }
|
||||
}
|
||||
const section = source.slice(start, end).concat('\nglobalThis.__c = { createCanonicalChat };\n')
|
||||
@@ -34,26 +35,54 @@ function loadCreate(hidePref) {
|
||||
return { create: context.__c.createCanonicalChat, created }
|
||||
}
|
||||
|
||||
test('createCanonicalChat passes hidden:true when the pref is on', async () => {
|
||||
const { create, created } = loadCreate(true)
|
||||
test('createCanonicalChat always passes hidden:true — no pref gate', async () => {
|
||||
const { create, created } = loadCreate()
|
||||
await create('alpha')
|
||||
assert.equal(created.length, 1)
|
||||
assert.equal(created[0].hidden, true)
|
||||
assert.equal(created[0].title, 'Bot Chat')
|
||||
})
|
||||
|
||||
test('createCanonicalChat omits hidden when the pref is off', async () => {
|
||||
const { create, created } = loadCreate(false)
|
||||
await create('beta')
|
||||
assert.equal(created.length, 1)
|
||||
assert.ok(!('hidden' in created[0]), 'hidden should be omitted, not false')
|
||||
test('group member session.create is unconditionally hidden too', () => {
|
||||
// Source contract on ensureGroupChatSession: the create carries a literal
|
||||
// hidden:true, with no $hideBotChats conditional anywhere in the plugin.
|
||||
const fn = source.slice(source.indexOf('async function ensureGroupChatSession('), source.indexOf('const GROUP_TURN_TIMEOUT_MS'))
|
||||
assert.match(fn, /hidden: true/)
|
||||
assert.equal(source.includes('$hideBotChats'), false, 'the old pref atom must be gone')
|
||||
})
|
||||
|
||||
test('setHideBotChats persists the pref and reconciles known chats via session.set_hidden', () => {
|
||||
// Source-level: the toggle calls session.set_hidden for each known canonical
|
||||
// chat id and persists the pref to storage.
|
||||
const fn = source.slice(source.indexOf('async function setHideBotChats('), source.indexOf('const avatarFetchInflight'))
|
||||
assert.match(fn, /storage\?\.set\?\.\('hide-bot-chats', hidden\)/)
|
||||
assert.match(fn, /session\.set_hidden.*session_id: sid, hidden/)
|
||||
assert.match(fn, /\.map\(m => m && m\.chat\)/)
|
||||
test('hideOwnedBotSessions sweeps canonical chats AND room member sessions', async () => {
|
||||
const start = source.indexOf('function hideOwnedBotSessions()')
|
||||
const end = source.indexOf('/** Fetch server-side avatars', start)
|
||||
const calls = []
|
||||
const context = {
|
||||
host: {
|
||||
request: async (method, params) => {
|
||||
calls.push({ method, params })
|
||||
return {}
|
||||
}
|
||||
},
|
||||
$botMeta: { get: () => ({ alpha: { chat: 'chat-a' }, beta: { chat: 'chat-b' }, gamma: {} }) },
|
||||
$groupChats: {
|
||||
get: () => ({
|
||||
Core: { sessions: { alpha: 'room-core-a', beta: 'room-core-b' } },
|
||||
Quiet: { sessions: { alpha: 'chat-a' } }, // duplicate id — must dedupe
|
||||
Legacy: {} // pre-sessions room shape
|
||||
})
|
||||
}
|
||||
}
|
||||
const section = source.slice(start, end).concat('\nglobalThis.__h = { hideOwnedBotSessions };\n')
|
||||
vm.runInNewContext(section, context, { filename: 'h.js' })
|
||||
await context.__h.hideOwnedBotSessions()
|
||||
|
||||
const ids = calls.map(c => c.params.session_id).sort()
|
||||
assert.deepEqual(ids, ['chat-a', 'chat-b', 'room-core-a', 'room-core-b'])
|
||||
assert.ok(calls.every(c => c.method === 'session.set_hidden' && c.params.hidden === true))
|
||||
})
|
||||
|
||||
test('the Bots session browser lists with include_hidden', () => {
|
||||
// The one session.list consumer that must see the always-hidden rows.
|
||||
// (Canonical-chat recovery now goes through profiles.list
|
||||
// preferred_session_ids, whose resolver already sees hidden rows.)
|
||||
assert.match(source, /session\.list', \{ profile: botName, limit: PROFILE_SESSION_LIST_LIMIT, include_hidden: true \}/)
|
||||
})
|
||||
|
||||
@@ -134,7 +134,7 @@ test('source contract: bot rows and Active now activate the owner before canonic
|
||||
test('source contract: workspaces disclose the bounded recent-session inventory', () => {
|
||||
assert.match(pluginSource, /queryKey: \[ID, 'profile-sessions', botName, gatewayGeneration\]/)
|
||||
assert.match(pluginSource, /enabled: Boolean\(botName\)/)
|
||||
assert.match(pluginSource, /host\.request\('session\.list', \{ profile: botName, limit: PROFILE_SESSION_LIST_LIMIT \}\)/)
|
||||
assert.match(pluginSource, /host\.request\('session\.list', \{ profile: botName, limit: PROFILE_SESSION_LIST_LIMIT, include_hidden: true \}\)/)
|
||||
assert.match(pluginSource, /Showing the \$\{PROFILE_SESSION_LIST_LIMIT\} most recent sessions\./)
|
||||
assert.match(pluginSource, /No matching sessions in the \$\{PROFILE_SESSION_LIST_LIMIT\} most recent\./)
|
||||
assert.doesNotMatch(pluginSource, /children: 'Activate profile'/)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""RPC-level tests for the generic hidden-session surface (tui_gateway).
|
||||
|
||||
Covers the two seams Bot Mode's "sessions are always hidden" policy leans on:
|
||||
|
||||
* ``session.set_hidden`` resolves a DURABLE stored session id when no live
|
||||
runtime session matches — plugins reconciling sessions they own (Bot Mode's
|
||||
hide sweep) hold stored ids for chats that aren't live right now. The old
|
||||
live-only lookup failed those with 4001 and the sweep silently no-opped.
|
||||
* ``session.list`` honors ``include_hidden`` so owning surfaces (the Bots
|
||||
pane's per-profile browser) can still enumerate the rows they hid, while
|
||||
every default caller keeps the hidden rows dropped.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
import tui_gateway.server as srv
|
||||
import tui_gateway.methods_session # noqa: F401 (registers the RPC methods)
|
||||
from hermes_state import SessionDB
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db(tmp_path, monkeypatch):
|
||||
database = SessionDB(tmp_path / "state.db")
|
||||
monkeypatch.setattr(srv, "_get_db", lambda: database)
|
||||
try:
|
||||
yield database
|
||||
finally:
|
||||
database.close()
|
||||
|
||||
|
||||
def _call(method: str, params: dict) -> dict:
|
||||
return srv._methods[method](1, params)
|
||||
|
||||
|
||||
def _seed(db, sid: str) -> None:
|
||||
db.create_session(sid, source="desktop")
|
||||
db._conn.execute("UPDATE sessions SET message_count = 1 WHERE id = ?", (sid,))
|
||||
db._conn.commit()
|
||||
|
||||
|
||||
def test_set_hidden_resolves_stored_id_without_live_session(db):
|
||||
"""A stored (non-live) session id must be hideable — the sweep path."""
|
||||
_seed(db, "stored-chat")
|
||||
assert srv._find_live_session_by_key("stored-chat") is None
|
||||
|
||||
envelope = _call("session.set_hidden", {"session_id": "stored-chat", "hidden": True})
|
||||
assert "error" not in envelope, envelope
|
||||
assert envelope["result"]["hidden"] is True
|
||||
assert db.get_session("stored-chat")["hidden"] == 1
|
||||
|
||||
# And back — unhide through the same durable path.
|
||||
envelope = _call("session.set_hidden", {"session_id": "stored-chat", "hidden": False})
|
||||
assert "error" not in envelope, envelope
|
||||
assert db.get_session("stored-chat")["hidden"] == 0
|
||||
|
||||
|
||||
def test_set_hidden_unknown_id_still_errors(db):
|
||||
envelope = _call("session.set_hidden", {"session_id": "no-such-session", "hidden": True})
|
||||
assert envelope.get("error"), envelope
|
||||
|
||||
|
||||
def test_session_list_include_hidden(db):
|
||||
_seed(db, "plain-chat")
|
||||
_seed(db, "bot-chat")
|
||||
assert db.set_session_hidden("bot-chat", True) is True
|
||||
|
||||
default_rows = _call("session.list", {})["result"]["sessions"]
|
||||
assert {s["id"] for s in default_rows} == {"plain-chat"}
|
||||
|
||||
all_rows = _call("session.list", {"include_hidden": True})["result"]["sessions"]
|
||||
assert {s["id"] for s in all_rows} == {"plain-chat", "bot-chat"}
|
||||
@@ -178,6 +178,11 @@ def _(rid, params: dict) -> dict:
|
||||
deny = frozenset({"kanban", "tool"})
|
||||
|
||||
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
|
||||
# them; the flag stays off for the resume picker and every other
|
||||
# global caller so `hidden` keeps meaning "not in shared lists".
|
||||
include_hidden = is_truthy_value(params.get("include_hidden", False))
|
||||
# Over-fetch modestly so per-source filtering doesn't leave us
|
||||
# short; the compression-tip projection in ``list_sessions_rich``
|
||||
# can also merge rows.
|
||||
@@ -189,6 +194,7 @@ def _(rid, params: dict) -> dict:
|
||||
limit=fetch_limit,
|
||||
order_by_last_active=True,
|
||||
compact_rows=True,
|
||||
include_hidden=include_hidden,
|
||||
)
|
||||
if (s.get("source") or "").strip().lower() not in deny
|
||||
][:limit]
|
||||
@@ -1190,23 +1196,44 @@ def _(rid, params: dict) -> dict:
|
||||
owns it — for plugins that manage their own sessions and don't want them
|
||||
cluttering the shared recents list. Flips the whole compression chain as a
|
||||
unit in the DB layer.
|
||||
|
||||
Resolution is two-tier: a LIVE runtime session id first (which also
|
||||
covers the not-yet-persisted draft via the ``pending_hidden`` deferral),
|
||||
then a durable stored id/key against the target profile's state.db —
|
||||
plugins reconciling sessions they own (e.g. Bot Mode's hide sweep) hold
|
||||
stored ids for chats that aren't live right now, and the live-only
|
||||
lookup silently failed those with 4001.
|
||||
"""
|
||||
session, err = _sess_nowait(params, rid)
|
||||
if err:
|
||||
return err
|
||||
hidden = is_truthy_value(params.get("hidden", True))
|
||||
with _session_db(session) as db:
|
||||
session, err = _sess_nowait(params, rid)
|
||||
if session is not None:
|
||||
with _session_db(session) as db:
|
||||
if db is None:
|
||||
return _db_unavailable_error(rid, code=5007)
|
||||
key = session["session_key"]
|
||||
try:
|
||||
changed = db.set_session_hidden(key, hidden)
|
||||
if not changed:
|
||||
# No row yet (write deferred to the first prompt): remember the
|
||||
# intent so _ensure_session_db_row is born hidden, mirroring the
|
||||
# pending_title deferral.
|
||||
session["pending_hidden"] = hidden
|
||||
return _ok(rid, {"hidden": hidden, "session_key": key})
|
||||
except Exception as e:
|
||||
return _err(rid, 5007, str(e))
|
||||
# Durable fallback: a stored session id (or key) in the requested
|
||||
# profile's db. ``resolve_session_id`` follows key/title aliases the
|
||||
# same way the REST pin/archive path does.
|
||||
target = str(params.get("session_id") or "").strip()
|
||||
with _profile_db(params) as db:
|
||||
if db is None:
|
||||
return _db_unavailable_error(rid, code=5007)
|
||||
key = session["session_key"]
|
||||
try:
|
||||
changed = db.set_session_hidden(key, hidden)
|
||||
if not changed:
|
||||
# No row yet (write deferred to the first prompt): remember the
|
||||
# intent so _ensure_session_db_row is born hidden, mirroring the
|
||||
# pending_title deferral.
|
||||
session["pending_hidden"] = hidden
|
||||
return _ok(rid, {"hidden": hidden, "session_key": key})
|
||||
resolved = db.resolve_session_id(target) if hasattr(db, "resolve_session_id") else target
|
||||
if not resolved:
|
||||
return err
|
||||
db.set_session_hidden(resolved, hidden)
|
||||
return _ok(rid, {"hidden": hidden, "session_key": resolved})
|
||||
except Exception as e:
|
||||
return _err(rid, 5007, str(e))
|
||||
|
||||
|
||||
@@ -182,6 +182,11 @@ when a teammate bot opens it headlessly from the CLI — so bot-to-bot
|
||||
replies and handoffs work without touching your SOUL.md, and your regular
|
||||
sessions stay untouched.
|
||||
|
||||
Bot Mode's sessions — each bot's canonical Bot Chat and every group-chat
|
||||
member session — are always hidden from the global Sessions sidebar. They
|
||||
live in the Bots pane (roster rows, room views, and each bot's session
|
||||
browser) instead of interleaving with your own conversations.
|
||||
|
||||
Don't want it? Flip it off in **Settings → Plugins → Bots** — the roster,
|
||||
routines pane, and composer middleware unregister live, no restart needed.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user