diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index 4d27cb5c59..41b829f6d8 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -5199,10 +5199,19 @@ class GatewaySlashCommandsMixin: s for s in titled if await self._resume_row_visible(source, s, allow_all) ] + # A non-admin `--all` silently falls back to same-origin + # scoping; say so instead of rendering an unexplained + # narrower list (sibling of the /sessions `all` notice). + scope_note = ( + t("gateway.resume.all_requires_admin") + if allow_all and not self._resume_caller_is_admin(source) + else None + ) if not titled: if source.platform == Platform.MATRIX and not allow_all: return t("gateway.resume.matrix_no_named_sessions") - return t("gateway.resume.no_named_sessions") + base = t("gateway.resume.no_named_sessions") + return f"{base}\n{scope_note}" if scope_note else base lines = [t("gateway.resume.list_header")] for idx, s in enumerate(titled[:10], start=1): title = s["title"] @@ -5213,6 +5222,8 @@ class GatewaySlashCommandsMixin: preview = s.get("preview", "")[:40] preview_part = t("gateway.resume.list_preview_suffix", preview=preview) if preview else "" lines.append(t("gateway.resume.list_item_numbered", index=idx, title=title, preview_part=preview_part)) + if scope_note: + lines.append(scope_note) lines.append(t("gateway.resume.list_footer_numbered")) return "\n".join(lines) except Exception as e: @@ -5358,6 +5369,15 @@ class GatewaySlashCommandsMixin: # `/sessions all` and enumerate other origins' session ids / titles / # previews / sources — the enumeration half of the /resume IDOR. cross_origin = include_all and self._resume_caller_is_admin(source) + # Don't silently no-op a requested widening: a non-admin `/sessions all` + # used to render the same scoped list with zero feedback, which reads + # as "my session vanished" (community report, Aug 2026). + scope_notice = None + if include_all and not cross_origin: + scope_notice = ( + "_Note: `all` (cross-chat listing) requires a configured admin; " + "showing this chat's sessions only._" + ) current_entry = await self.async_session_store.get_or_create_session(source) rows = await asyncio.to_thread( query_session_listing, @@ -5390,6 +5410,7 @@ class GatewaySlashCommandsMixin: rows, include_source=cross_origin, title=title, + notice=scope_notice, ) async def _handle_branch_command(self, event: MessageEvent) -> str: diff --git a/hermes_cli/session_listing.py b/hermes_cli/session_listing.py index 3caa78f4c8..7c2c0e4e0d 100644 --- a/hermes_cli/session_listing.py +++ b/hermes_cli/session_listing.py @@ -99,14 +99,23 @@ def format_gateway_session_listing( *, include_source: bool = False, title: str = "Sessions", + notice: str | None = None, ) -> str: - """Render a compact Markdown-ish session list for gateway messengers.""" + """Render a compact Markdown-ish session list for gateway messengers. + + ``notice`` appends an explanatory line above the footer — used e.g. when + a requested scope widening (``all``) was declined so the caller isn't + left guessing why sessions are missing. + """ if not rows: - return ( + parts = [ "No sessions found.\n" "Use `/title My Session` to name this chat, or `/sessions full` " "to include unnamed sessions." - ) + ] + if notice: + parts.append(notice) + return "\n".join(parts) lines = [f"📋 **{title}**", ""] for idx, row in enumerate(rows, start=1): @@ -119,6 +128,8 @@ def format_gateway_session_listing( preview_part = f" — _{preview}_" if preview else "" lines.append(f"{idx}. **{title_text}**{current_part}{source_part} — `{session_id}`{preview_part}") lines.append("") + if notice: + lines.append(notice) lines.append("Resume: `/resume ` or `/resume ` from `/resume`.") lines.append("More: `/sessions all`, `/sessions full`, `/sessions search `.") return "\n".join(lines) diff --git a/locales/af.yaml b/locales/af.yaml index a2e6727dd9..21806156ab 100644 --- a/locales/af.yaml +++ b/locales/af.yaml @@ -269,6 +269,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "Geen benoemde sessies gevind nie.\nGebruik `/title My Sessie` om jou huidige sessie 'n naam te gee, en dan `/resume My Sessie` om later daarheen terug te keer." + all_requires_admin: "_Let wel: `--all` (kruis-geselslys) vereis 'n opgestelde administrateur; slegs hierdie geselsie se sessies word gewys._" list_header: "📋 **Benoemde Sessies**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/ar.yaml b/locales/ar.yaml index bc74699b45..1f68628a3b 100644 --- a/locales/ar.yaml +++ b/locales/ar.yaml @@ -289,6 +289,7 @@ gateway: matrix_cross_room_success: "⚠️ استئناف عبر الغرف: استُؤنفت **{title}** داخل غرفة Matrix ‏**{room}**.\nستستخدم الرسائل المستقبلية في هذه الغرفة ذلك النص حتى `/reset` أو `/resume` آخر.{msg_part}" blocked_not_owner: "⚠️ حُجب /resume: '**{name}**' تخصّ مستخدمًا أو محادثة مختلفة. يمكنك فقط استئناف جلسات هذه المحادثة." no_named_sessions: "لم يُعثر على جلسات مُسمّاة.\nاستخدم `/title My Session` لتسمية جلستك الحالية، ثم `/resume My Session` للعودة إليها لاحقًا." + all_requires_admin: "_ملاحظة: يتطلب `--all` (سرد عبر الدردشات) مسؤولاً مُهيأً؛ تُعرض جلسات هذه الدردشة فقط._" list_header: "📋 **الجلسات المُسمّاة**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/de.yaml b/locales/de.yaml index d01642d671..bc00bfe32f 100644 --- a/locales/de.yaml +++ b/locales/de.yaml @@ -269,6 +269,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "Keine benannten Sitzungen gefunden.\nVerwenden Sie `/title Meine Sitzung`, um die aktuelle Sitzung zu benennen, dann `/resume Meine Sitzung`, um später dorthin zurückzukehren." + all_requires_admin: "_Hinweis: `--all` (chatübergreifende Liste) erfordert einen konfigurierten Administrator; es werden nur die Sitzungen dieses Chats angezeigt._" list_header: "📋 **Benannte Sitzungen**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/en.yaml b/locales/en.yaml index 834e5a0547..2adac023f2 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -281,6 +281,7 @@ gateway: matrix_cross_room_success: "⚠️ Cross-room resume: resumed **{title}** inside Matrix room **{room}**.\nFuture messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "No named sessions found.\nUse `/title My Session` to name your current session, then `/resume My Session` to return to it later." + all_requires_admin: "_Note: `--all` (cross-chat listing) requires a configured admin; showing this chat's sessions only._" list_header: "📋 **Named Sessions**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/es.yaml b/locales/es.yaml index c90d1c3d04..6b06a52afb 100644 --- a/locales/es.yaml +++ b/locales/es.yaml @@ -266,6 +266,7 @@ gateway: matrix_cross_room_success: "⚠️ Reanudación entre salas: **{title}** reanudada dentro de la sala de Matrix **{room}**.\nLos próximos mensajes en esta sala usarán esa transcripción hasta `/reset` u otro `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "No se encontraron sesiones con nombre.\nUsa `/title Mi sesión` para nombrar la sesión actual y luego `/resume Mi sesión` para volver a ella." + all_requires_admin: "_Nota: `--all` (listado entre chats) requiere un administrador configurado; se muestran solo las sesiones de este chat._" list_header: "📋 **Sesiones con nombre**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/fr.yaml b/locales/fr.yaml index abd3e284b7..4ce9760969 100644 --- a/locales/fr.yaml +++ b/locales/fr.yaml @@ -269,6 +269,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "Aucune session nommée trouvée.\nUtilisez `/title Ma session` pour nommer la session actuelle, puis `/resume Ma session` pour y revenir plus tard." + all_requires_admin: "_Remarque : `--all` (liste inter-discussions) nécessite un administrateur configuré ; seules les sessions de cette discussion sont affichées._" list_header: "📋 **Sessions nommées**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/ga.yaml b/locales/ga.yaml index daff6c5b08..92ef5363ea 100644 --- a/locales/ga.yaml +++ b/locales/ga.yaml @@ -273,6 +273,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "Níor aimsíodh aon seisiún ainmnithe.\nÚsáid `/title M'Ainm Seisiúin` chun do sheisiún reatha a ainmniú, ansin `/resume M'Ainm Seisiúin` chun filleadh air níos déanaí." + all_requires_admin: "_Nóta: teastaíonn riarthóir cumraithe le `--all` (liostú traschomhrá); ní thaispeántar ach seisiúin an chomhrá seo._" list_header: "📋 **Seisiúin Ainmnithe**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/hu.yaml b/locales/hu.yaml index d69f9da511..b8feb1b994 100644 --- a/locales/hu.yaml +++ b/locales/hu.yaml @@ -269,6 +269,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "Nem található elnevezett munkamenet.\nHasználd a `/title Saját munkamenet` parancsot a jelenlegi munkamenet elnevezéséhez, majd a `/resume Saját munkamenet` paranccsal térhetsz vissza hozzá." + all_requires_admin: "_Megjegyzés: az `--all` (csevegések közötti listázás) beállított adminisztrátort igényel; csak ennek a csevegésnek a munkamenetei jelennek meg._" list_header: "📋 **Elnevezett munkamenetek**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/it.yaml b/locales/it.yaml index 7b2da1c8f6..758be5d8a7 100644 --- a/locales/it.yaml +++ b/locales/it.yaml @@ -269,6 +269,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "Nessuna sessione con nome trovata.\nUsa `/title My Session` per dare un nome alla sessione attuale, poi `/resume My Session` per tornare a essa in seguito." + all_requires_admin: "_Nota: `--all` (elenco tra chat) richiede un amministratore configurato; vengono mostrate solo le sessioni di questa chat._" list_header: "📋 **Sessioni con nome**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/ja.yaml b/locales/ja.yaml index ab7c840b44..28b41682aa 100644 --- a/locales/ja.yaml +++ b/locales/ja.yaml @@ -269,6 +269,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "名前付きセッションが見つかりません。\n`/title セッション名` で現在のセッションに名前を付けると、後で `/resume セッション名` で戻れます。" + all_requires_admin: "_注: `--all`(チャット横断の一覧)には設定済みの管理者権限が必要です。このチャットのセッションのみ表示しています。_" list_header: "📋 **名前付きセッション**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/ko.yaml b/locales/ko.yaml index f038b52a83..ecf58bbc68 100644 --- a/locales/ko.yaml +++ b/locales/ko.yaml @@ -269,6 +269,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "이름이 지정된 세션이 없습니다.\n현재 세션에 이름을 지정하려면 `/title 내 세션`을 사용하고, 나중에 `/resume 내 세션`으로 돌아오세요." + all_requires_admin: "_참고: `--all`(채팅 간 목록)은 구성된 관리자가 필요합니다. 이 채팅의 세션만 표시합니다._" list_header: "📋 **이름이 지정된 세션**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/pt.yaml b/locales/pt.yaml index 4e2f4a34c6..1ac1fd4b00 100644 --- a/locales/pt.yaml +++ b/locales/pt.yaml @@ -269,6 +269,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "Não foram encontradas sessões com nome.\nUsa `/title A minha sessão` para nomear a sessão atual e depois `/resume A minha sessão` para voltar a ela." + all_requires_admin: "_Nota: `--all` (listagem entre conversas) requer um administrador configurado; mostram-se apenas as sessões desta conversa._" list_header: "📋 **Sessões com nome**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/ru.yaml b/locales/ru.yaml index e033d0668d..51c892e02e 100644 --- a/locales/ru.yaml +++ b/locales/ru.yaml @@ -269,6 +269,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "Именованных сеансов не найдено.\nИспользуйте `/title Мой сеанс`, чтобы назвать текущий сеанс, затем `/resume Мой сеанс`, чтобы вернуться к нему позже." + all_requires_admin: "_Примечание: `--all` (список по всем чатам) требует настроенного администратора; показаны только сеансы этого чата._" list_header: "📋 **Именованные сеансы**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/tr.yaml b/locales/tr.yaml index 3dabe13b65..a88b1d0586 100644 --- a/locales/tr.yaml +++ b/locales/tr.yaml @@ -269,6 +269,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "Adlandırılmış oturum bulunamadı.\nMevcut oturumu adlandırmak için `/title Oturumum`, daha sonra geri dönmek için `/resume Oturumum` kullanın." + all_requires_admin: "_Not: `--all` (sohbetler arası listeleme) yapılandırılmış bir yönetici gerektirir; yalnızca bu sohbetin oturumları gösteriliyor._" list_header: "📋 **Adlandırılmış Oturumlar**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/uk.yaml b/locales/uk.yaml index d9907b006a..730a052cd5 100644 --- a/locales/uk.yaml +++ b/locales/uk.yaml @@ -269,6 +269,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "Іменованих сеансів не знайдено.\nВикористайте `/title Мій сеанс`, щоб назвати поточний сеанс, потім `/resume Мій сеанс`, щоб повернутися до нього." + all_requires_admin: "_Примітка: `--all` (список між чатами) потребує налаштованого адміністратора; показано лише сеанси цього чату._" list_header: "📋 **Іменовані сеанси**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/zh-hant.yaml b/locales/zh-hant.yaml index 2a9520fca3..9468fbba1c 100644 --- a/locales/zh-hant.yaml +++ b/locales/zh-hant.yaml @@ -269,6 +269,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "找不到已命名的工作階段。\n使用 `/title 我的工作階段` 為目前工作階段命名,然後使用 `/resume 我的工作階段` 返回。" + all_requires_admin: "_注意:`--all`(跨聊天清單)需要已設定的管理員;僅顯示本聊天的工作階段。_" list_header: "📋 **已命名工作階段**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/locales/zh.yaml b/locales/zh.yaml index 696623cfc8..f659de9a24 100644 --- a/locales/zh.yaml +++ b/locales/zh.yaml @@ -269,6 +269,7 @@ Use `/title My Session` to name the current room session, `/resume --all` to lis Future messages in this room will use that transcript until `/reset` or another `/resume`.{msg_part}" blocked_not_owner: "⚠️ /resume blocked: '**{name}**' belongs to a different user or chat. You can only resume sessions from this chat." no_named_sessions: "未找到已命名的会话。\n使用 `/title 我的会话` 为当前会话命名,然后用 `/resume 我的会话` 返回。" + all_requires_admin: "_注意:`--all`(跨聊天列表)需要已配置的管理员;仅显示本聊天的会话。_" list_header: "📋 **已命名会话**\n" list_item: "• **{title}**{preview_part}" list_item_numbered: "{index}. **{title}**{preview_part}" diff --git a/tests/gateway/test_resume_command.py b/tests/gateway/test_resume_command.py index 4a69a31637..74172b5561 100644 --- a/tests/gateway/test_resume_command.py +++ b/tests/gateway/test_resume_command.py @@ -107,6 +107,44 @@ class TestHandleResumeCommand: assert "/resume 1" in result db.close() + @pytest.mark.asyncio + async def test_resume_all_nonadmin_downgrade_is_announced(self, tmp_path): + """A non-admin `/resume --all` must say the widening was declined.""" + from hermes_state import SessionDB + db = SessionDB(db_path=tmp_path / "state.db") + event = _make_event(text="/resume --all") + lane_key = _session_key_for_event(event) + db.create_session( + "sess_001", "telegram", session_key=lane_key, + user_id="12345", chat_id="67890", + ) + db.set_session_title("sess_001", "Research") + + runner = _make_runner(session_db=db, event=event) + result = await runner._handle_resume_command(event) + assert "Research" in result + assert "requires a configured admin" in result + db.close() + + @pytest.mark.asyncio + async def test_resume_plain_listing_has_no_scope_notice(self, tmp_path): + """No downgrade notice when `--all` wasn't requested.""" + from hermes_state import SessionDB + db = SessionDB(db_path=tmp_path / "state.db") + event = _make_event(text="/resume") + lane_key = _session_key_for_event(event) + db.create_session( + "sess_001", "telegram", session_key=lane_key, + user_id="12345", chat_id="67890", + ) + db.set_session_title("sess_001", "Research") + + runner = _make_runner(session_db=db, event=event) + result = await runner._handle_resume_command(event) + assert "Research" in result + assert "requires a configured admin" not in result + db.close() + @pytest.mark.asyncio async def test_resume_clears_session_model_overrides(self, tmp_path): @@ -420,7 +458,9 @@ class TestHandleSessionsCommand: after_resume = await runner._handle_sessions_command(event) assert "Legacy reset child" in after_resume - assert "Legacy reset parent" not in after_resume + # The parent is now the CURRENT session: since #68547 it stays in the + # listing with a "(current)" marker instead of being hidden. + assert "**Legacy reset parent** (current)" in after_resume child_row = db.get_session(child_id) assert child_row is not None assert json.loads(child_row["model_config"])["_reset_from"] == root_id @@ -489,7 +529,9 @@ class TestHandleSessionsCommand: assert "Greeting via Telegram" in result assert "Store memories with priority" in result assert "Extract AI news to Telegram" in result - assert "Current Telegram work" not in result + # The live tip is the current session — listed with the marker since + # #68547 rather than hidden. + assert "**Current Telegram work** (current)" in result db.close() @pytest.mark.asyncio @@ -535,14 +577,59 @@ class TestHandleSessionsCommand: ) result = await runner._handle_sessions_command(event) - assert result.count("Lane Work") == 10 - assert "Lane Work 1" in result - assert "Lane Work 0" not in result + # The current tip now occupies one of the 10 slots with a marker + # (#68547) instead of being hidden; its compressed-away root stays out. + assert "**Current compressed tip** (current)" in result + assert result.count("Lane Work") == 9 + assert "`lane_root_2`" in result + assert "`lane_root_1`" not in result + assert "`lane_root_0`" not in result assert "Foreign Work" not in result - assert "current_tip" not in result assert "current_root" not in result db.close() + @pytest.mark.asyncio + async def test_sessions_all_nonadmin_downgrade_is_announced(self, tmp_path): + """A non-admin `/sessions all` must say the widening was declined.""" + from hermes_state import SessionDB + + db = SessionDB(db_path=tmp_path / "state.db") + event = _make_event(text="/sessions all") + lane_key = _session_key_for_event(event) + db.create_session( + "sess_local", "telegram", session_key=lane_key, + user_id="12345", chat_id="67890", + ) + db.set_session_title("sess_local", "Local Work") + + runner = _make_runner(session_db=db, event=event) + result = await runner._handle_sessions_command(event) + + assert "Local Work" in result + assert "requires a configured admin" in result + db.close() + + @pytest.mark.asyncio + async def test_sessions_plain_listing_has_no_scope_notice(self, tmp_path): + """No notice when the caller never asked for `all`.""" + from hermes_state import SessionDB + + db = SessionDB(db_path=tmp_path / "state.db") + event = _make_event(text="/sessions") + lane_key = _session_key_for_event(event) + db.create_session( + "sess_local", "telegram", session_key=lane_key, + user_id="12345", chat_id="67890", + ) + db.set_session_title("sess_local", "Local Work") + + runner = _make_runner(session_db=db, event=event) + result = await runner._handle_sessions_command(event) + + assert "Local Work" in result + assert "requires a configured admin" not in result + db.close() + @pytest.mark.asyncio async def test_sessions_admin_all_preserves_cross_origin_widening(self, tmp_path): from hermes_state import SessionDB diff --git a/tests/hermes_cli/test_session_listing.py b/tests/hermes_cli/test_session_listing.py index 08fa7f6b87..4f4d17ddf3 100644 --- a/tests/hermes_cli/test_session_listing.py +++ b/tests/hermes_cli/test_session_listing.py @@ -90,6 +90,25 @@ class TestFormatGatewaySessionListing: assert "**AN-94 Prestige Barrel Build #2** (current)" in listing + def test_notice_appears_above_footer(self): + listing = format_gateway_session_listing( + [{"id": "sess_an94", "title": "AN-94"}], + notice="_Note: `all` requires admin._", + ) + lines = listing.splitlines() + notice_idx = lines.index("_Note: `all` requires admin._") + footer_idx = next(i for i, l in enumerate(lines) if l.startswith("Resume:")) + assert notice_idx < footer_idx + + def test_notice_on_empty_listing(self): + listing = format_gateway_session_listing([], notice="_scoped_") + assert "No sessions found." in listing + assert "_scoped_" in listing + + def test_no_notice_by_default(self): + listing = format_gateway_session_listing([{"id": "x", "title": "T"}]) + assert "Note:" not in listing + class TestQuerySessionListingLaneScope: @pytest.fixture diff --git a/website/docs/reference/slash-commands.md b/website/docs/reference/slash-commands.md index 3f17e773e7..5405230994 100644 --- a/website/docs/reference/slash-commands.md +++ b/website/docs/reference/slash-commands.md @@ -254,7 +254,7 @@ The messaging gateway supports the following built-in commands inside Telegram, | `/topic [off\|help\|session-id]` | **Telegram DM only.** Manage user-managed multi-session topic mode. `/topic` enables it or shows status; `/topic off` disables it and clears bindings; `/topic help` shows usage; `/topic ` inside a topic restores a previous session. See [Multi-session DM mode](/user-guide/messaging/telegram#multi-session-dm-mode-topic). | | `/title [name]` | Set or show the session title. | | `/resume [name]` | Resume a previously named session. | -| `/sessions [all] [search ]` | List previous sessions for this chat. `/sessions search ` filters by title/id match (most recently active first); `/sessions all` lists across origins (admin only). | +| `/sessions [all] [search ]` | List previous sessions for this chat; the active session appears with a `(current)` marker. `/sessions search ` filters by title/id match (most recently active first); `/sessions all` lists across origins (admin only — non-admins get a notice and the chat-scoped list). | | `/usage` | Show token usage, estimated cost breakdown (input/output), context window state, session duration, and — when available from the active provider — an **Account limits** section with remaining quota / credits pulled live from the provider's API. | | `/topup` | Show your Nous balance and manage billing on the portal. | | `/whoami` | Show your slash command access level (admin / user). | diff --git a/website/docs/user-guide/messaging/index.md b/website/docs/user-guide/messaging/index.md index ef25303025..a0aaf6b5e4 100644 --- a/website/docs/user-guide/messaging/index.md +++ b/website/docs/user-guide/messaging/index.md @@ -225,7 +225,7 @@ Sessions persist across messages until they reset. The agent remembers your conv ### Finding Past Sessions (`/sessions`) -`/sessions` lists your previous sessions for the current chat, and `/sessions ` resumes one (shorthand for `/resume`). When the list grows long, `/sessions search ` (alias `find`) filters by title or session-id match, ordered by most recently active. Cross-origin listing with `/sessions all` is admin-only — regular users only ever see sessions from their own chat origin. +`/sessions` lists your previous sessions for the current chat — including the one you're in now, marked `(current)` — and `/sessions ` resumes one (shorthand for `/resume`). When the list grows long, `/sessions search ` (alias `find`) filters by title or session-id match, ordered by most recently active. Cross-origin listing with `/sessions all` is admin-only — regular users get a notice explaining the list stayed chat-scoped, and only ever see sessions from their own chat origin. ### Persistent `/model` Overrides