{"repo":"NousResearch/hermes-agent","public_id":"4988","display_label":"Interactive session picker and numeric selection for /resume","summary":"These members all concern making /resume easier to use, mainly by showing an interactive session picker when no arguments are given. Some also mention selecting a session by position number, including in the gateway, but the titles alone don't show how far any of this has been implemented.","subsystem":"hermes_cli","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":31709,"merged_at":"2026-05-24T23:22:49+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-24T23:22:49+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":4988,"pr_number":31709,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-24T23:22:49+00:00"}],"members":[{"kind":"issue","number":4988,"title":"[Feature]: /resume with no arguments should show an interactive session picker","url":null,"state":"closed"},{"kind":"pr","number":4990,"title":"feat(cli): add /resume interactive picker and numeric selection (#4988)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#31709","number":31709}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":8493,"title":"feat: interactive session picker for /resume and hermes resume","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#7880","number":7880}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":31709,"title":"feat(cli,gateway): /resume accepts position numbers","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","gateway/run.py","locales/af.yaml","locales/de.yaml","locales/en.yaml","locales/es.yaml","locales/fr.yaml","locales/ga.yaml","locales/hu.yaml","locales/it.yaml","locales/ja.yaml","locales/ko.yaml","locales/pt.yaml","locales/ru.yaml","locales/tr.yaml","locales/uk.yaml","locales/zh-hant.yaml","locales/zh.yaml","scripts/release.py","tests/cli/test_cli_init.py","tests/cli/test_cli_resume_command.py","tests/gateway/test_resume_command.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -6172,15 +6172,16 @@ def _show_recent_sessions(self, *, reason: str = \"history\", limit: int = 10) ->\n         else:\n             print(\"  Recent sessions:\")\n         print()\n-        print(f\"  {'Title':<32} {'Preview':<40} {'Last Active':<13} {'ID'}\")\n-        print(f\"  {'─' * 32} {'─' * 40} {'─' * 13} {'─' * 24}\")\n-        for session in sessions:\n+        print(f\"  {'#':<3} {'Title':<32} {'Preview':<40} {'Last Active':<13} {'ID'}\")\n+        print(f\"  {'─' * 3} {'─' * 32} {'─' * 40} {'─' * 13} {'─' * 24}\")\n+        for idx, session in enumerate(sessions, start=1):\n             title = session.get(\"title\") or \"—\"\n             preview = (session.get(\"preview\") or \"\")[:38]\n             last_active = _relative_time(session.get(\"last_active\"))\n-            print(f\"  {title:<32} {preview:<40} {last_active:<13} {session['id']}\")\n+            print(f\"  {idx:<3} {title:<32} {preview:<40} {last_active:<13} {session['id']}\")\n         print()\n-        print(\"  Use /resume <session id or title> to continue where you left off.\")\n+        print(\"  Use /resume <number>, /resume <session id>, or /resume <session title> to continue.\")\n+        print(\"  Example: /resume 2\")\n         print()\n         return True\n ","@@ -6525,7 +6526,7 @@ def _handle_resume_command(self, cmd_original: str) -> None:\n         target = parts[1].strip() if len(parts) > 1 else \"\"\n \n         if not target:\n-            _cprint(\"  Usage: /resume <session_id_or_title>\")\n+            _cprint(\"  Usage: /resume <number|session_id_or_title>\")\n             if self._show_recent_sessions(reason=\"resume\"):\n                 return\n             _cprint(\"  Tip:   Use /history or `hermes sessions list` to find sessions.\")","@@ -6536,10 +6537,20 @@ def _handle_resume_command(self, cmd_original: str) -> None:\n             _cprint(f\"  {format_session_db_unavailable()}\")\n             return\n \n-        # Resolve title or ID\n-        from hermes_cli.main import _resolve_session_by_name_or_id\n-        resolved = _resolve_session_by_name_or_id(target)\n-        target_id = resolved or target\n+        # Resolve numbered selection, title, or ID\n+        if target.isdigit():\n+            sessions = self._list_recent_sessions(limit=10)\n+            index = int(target)\n+            if index < 1 or index > len(sessions):\n+                _cprint(f\"  Resume index {index} is out of range.\")\n+                _cprint(\"  Use /resume with no arguments to see available sessions.\")\n+                return\n+            selected = sessions[index - 1]\n+            target_id = selected[\"id\"]\n+        else:\n+            from hermes_cli.main import _resolve_session_by_name_or_id\n+            resolved = _resolve_session_by_name_or_id(target)\n+            target_id = resolved or target\n \n         session_meta = self._session_db.get_session(target_id)\n         if not session_meta:"]},{"file":"gateway/run.py","hunks":["@@ -12741,7 +12741,7 @@ async def _handle_title_command(self, event: MessageEvent) -> str:\n                 return t(\"gateway.title.current_no_title\", session_id=session_id)\n \n     async def _handle_resume_command(self, event: MessageEvent) -> str:\n-        \"\"\"Handle /resume command — switch to a previously-named session.\"\"\"\n+        \"\"\"Handle /resume command — list or switch to a previous session.\"\"\"\n         if not self._session_db:\n             from hermes_state import format_session_db_unavailable\n             return format_session_db_unavailable(prefix=t(\"gateway.shared.session_db_unavailable_prefix\"))","@@ -12750,30 +12750,44 @@ async def _handle_resume_command(self, event: MessageEvent) -> str:\n         session_key = self._session_key_for_source(source)\n         name = event.get_command_args().strip()\n \n+        def _list_titled_sessions() -> list[dict]:\n+            user_source = source.platform.value if source.platform else None\n+            sessions = self._session_db.list_sessions_rich(source=user_source, limit=10)\n+            return [s for s in sessions if s.get(\"title\")][:10]\n+\n         if not name:\n             # List recent titled sessions for this user/platform\n             try:\n-                user_source = source.platform.value if source.platform else None\n-                sessions = self._session_db.list_sessions_rich(\n-                    source=user_source, limit=10\n-                )\n-                titled = [s for s in sessions if s.get(\"title\")]\n+                titled = _list_titled_sessions()\n                 if not titled:\n                     return t(\"gateway.resume.no_named_sessions\")\n                 lines = [t(\"gateway.resume.list_header\")]\n-                for s in titled[:10]:\n+                for idx, s in enumerate(titled[:10], start=1):\n                     title = s[\"title\"]\n                     preview = s.get(\"preview\", \"\")[:40]\n                     preview_part = t(\"gateway.resume.list_preview_suffix\", preview=preview) if preview else \"\"\n-                    lines.append(t(\"gateway.resume.list_item\", title=title, preview_part=preview_part))\n-                lines.append(t(\"gateway.resume.list_footer\"))\n+                    lines.append(t(\"gateway.resume.list_item_numbered\", index=idx, title=title, preview_part=preview_part))\n+                lines.append(t(\"gateway.resume.list_footer_numbered\"))\n                 return \"\\n\".join(lines)\n             except Exception as e:\n                 logger.debug(\"Failed to list titled sessions: %s\", e)\n                 return t(\"gateway.resume.list_failed\", error=e)\n \n-        # Resolve the name to a session ID.\n-        target_id = self._session_db.resolve_session_by_title(name)\n+        # Resolve a numbered choice or a title to a session ID.\n+        if name.isdigit():\n+            try:\n+                titled = _list_titled_sessions()\n+            except Exception as e:\n+                logger.debug(\"Failed to list titled sessions for numeric resume: %s\", e)\n+                return t(\"gateway.resume.list_failed\", error=e)\n+            index = int(name)\n+            if index < 1 or index > len(titled):\n+                return t(\"gateway.resume.out_of_range\", index=index)\n+            target = titled[index - 1]\n+            target_id = target.get(\"id\")\n+            name = target.get(\"title\") or name\n+        else:\n+            target_id = self._session_db.resolve_session_by_title(name)\n         if not target_id:\n             return t(\"gateway.resume.not_found\", name=name)\n         # Compression creates child continuations that hold the live transcript."]},{"file":"locales/af.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     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.\"\n     list_header:           \"📋 **Benoemde Sessies**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\nGebruik: `/resume <session name>`\"\n+    list_footer_numbered:  \"\\nGebruik: `/resume <sessienaam>` of `/resume <nommer>` (bv. `/resume 1` vir die mees onlangse)\"\n     list_failed:           \"Kon nie sessies lys nie: {error}\"\n+    out_of_range:          \"Hervat-indeks {index} is buite bereik.\\nGebruik `/resume` sonder argumente om beskikbare sessies te sien.\"\n     not_found:             \"Geen sessie gevind wat by '**{name}**' pas nie.\\nGebruik `/resume` sonder argumente om beskikbare sessies te sien.\"\n     already_on:            \"📌 Reeds op sessie **{name}**.\"\n     switch_failed:         \"Kon nie sessie verander nie.\""]},{"file":"locales/de.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     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.\"\n     list_header:           \"📋 **Benannte Sitzungen**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\nVerwendung: `/resume <Sitzungsname>`\"\n+    list_footer_numbered:  \"\\nVerwendung: `/resume <Sitzungsname>` oder `/resume <Nummer>` (z. B. `/resume 1` für die zuletzt verwendete)\"\n     list_failed:           \"Sitzungen konnten nicht aufgelistet werden: {error}\"\n+    out_of_range:          \"Wiederaufnahme-Index {index} liegt außerhalb des gültigen Bereichs.\\nVerwenden Sie `/resume` ohne Argumente, um verfügbare Sitzungen anzuzeigen.\"\n     not_found:             \"Keine Sitzung passend zu '**{name}**' gefunden.\\nVerwenden Sie `/resume` ohne Argumente, um verfügbare Sitzungen zu sehen.\"\n     already_on:            \"📌 Bereits in Sitzung **{name}**.\"\n     switch_failed:         \"Sitzungswechsel fehlgeschlagen.\""]},{"file":"locales/en.yaml","hunks":["@@ -237,9 +237,12 @@ gateway:\n     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.\"\n     list_header:           \"📋 **Named Sessions**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\nUsage: `/resume <session name>`\"\n+    list_footer_numbered:  \"\\nUsage: `/resume <session name>` or `/resume <number>` (e.g. `/resume 1` for the most recent)\"\n     list_failed:           \"Could not list sessions: {error}\"\n+    out_of_range:          \"Resume index {index} is out of range.\\nUse `/resume` with no arguments to see available sessions.\"\n     not_found:             \"No session found matching '**{name}**'.\\nUse `/resume` with no arguments to see available sessions.\"\n     already_on:            \"📌 Already on session **{name}**.\"\n     switch_failed:         \"Failed to switch session.\""]},{"file":"locales/es.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     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.\"\n     list_header:           \"📋 **Sesiones con nombre**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\nUso: `/resume <nombre de sesión>`\"\n+    list_footer_numbered:  \"\\nUso: `/resume <nombre de sesión>` o `/resume <número>` (p. ej. `/resume 1` para la más reciente)\"\n     list_failed:           \"No se pudieron listar las sesiones: {error}\"\n+    out_of_range:          \"El índice de reanudación {index} está fuera de rango.\\nUsa `/resume` sin argumentos para ver las sesiones disponibles.\"\n     not_found:             \"No se encontró ninguna sesión que coincida con '**{name}**'.\\nUsa `/resume` sin argumentos para ver las sesiones disponibles.\"\n     already_on:            \"📌 Ya estás en la sesión **{name}**.\"\n     switch_failed:         \"No se pudo cambiar de sesión.\""]},{"file":"locales/fr.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     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.\"\n     list_header:           \"📋 **Sessions nommées**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\nUsage : `/resume <nom de session>`\"\n+    list_footer_numbered:  \"\\nUtilisation : `/resume <nom de session>` ou `/resume <numéro>` (par exemple `/resume 1` pour la plus récente)\"\n     list_failed:           \"Impossible de lister les sessions : {error}\"\n+    out_of_range:          \"L'index de reprise {index} est hors limites.\\nUtilisez `/resume` sans arguments pour voir les sessions disponibles.\"\n     not_found:             \"Aucune session correspondant à '**{name}**' trouvée.\\nUtilisez `/resume` sans argument pour voir les sessions disponibles.\"\n     already_on:            \"📌 Déjà sur la session **{name}**.\"\n     switch_failed:         \"Échec du changement de session.\""]},{"file":"locales/ga.yaml","hunks":["@@ -226,9 +226,12 @@ gateway:\n     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í.\"\n     list_header:           \"📋 **Seisiúin Ainmnithe**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\nÚsáid: `/resume <session name>`\"\n+    list_footer_numbered:  \"\\nÚsáid: `/resume <ainm seisiúin>` nó `/resume <uimhir>` (m.sh. `/resume 1` don cheann is déanaí)\"\n     list_failed:           \"Níorbh fhéidir seisiúin a liostáil: {error}\"\n+    out_of_range:          \"Tá an t-innéacs atosaithe {index} as raon.\\nÚsáid `/resume` gan argóintí chun na seisiúin atá ar fáil a fheiceáil.\"\n     not_found:             \"Níor aimsíodh aon seisiún ag teacht le '**{name}**'.\\nÚsáid `/resume` gan argóintí chun seisiúin atá ar fáil a fheiceáil.\"\n     already_on:            \"📌 Cheana ar an seisiún **{name}**.\"\n     switch_failed:         \"Theip ar athrú seisiúin.\""]},{"file":"locales/hu.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     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á.\"\n     list_header:           \"📋 **Elnevezett munkamenetek**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\nHasználat: `/resume <munkamenet neve>`\"\n+    list_footer_numbered:  \"\\nHasználat: `/resume <munkamenet neve>` vagy `/resume <szám>` (pl. `/resume 1` a legutóbbihoz)\"\n     list_failed:           \"Nem sikerült listázni a munkameneteket: {error}\"\n+    out_of_range:          \"A folytatási index ({index}) tartományon kívül esik.\\nA `/resume` argumentumok nélküli használata megjeleníti az elérhető munkameneteket.\"\n     not_found:             \"Nem található '**{name}**' nevű munkamenet.\\nArgumentumok nélkül használd a `/resume` parancsot az elérhető munkamenetek megtekintéséhez.\"\n     already_on:            \"📌 Már a **{name}** munkamenetben vagy.\"\n     switch_failed:         \"Nem sikerült munkamenetet váltani.\""]},{"file":"locales/it.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     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.\"\n     list_header:           \"📋 **Sessioni con nome**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\nUso: `/resume <session name>`\"\n+    list_footer_numbered:  \"\\nUso: `/resume <nome sessione>` o `/resume <numero>` (es. `/resume 1` per la più recente)\"\n     list_failed:           \"Impossibile elencare le sessioni: {error}\"\n+    out_of_range:          \"L'indice di ripresa {index} è fuori intervallo.\\nUsa `/resume` senza argomenti per vedere le sessioni disponibili.\"\n     not_found:             \"Nessuna sessione trovata corrispondente a '**{name}**'.\\nUsa `/resume` senza argomenti per vedere le sessioni disponibili.\"\n     already_on:            \"📌 Già nella sessione **{name}**.\"\n     switch_failed:         \"Cambio di sessione non riuscito.\""]},{"file":"locales/ja.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     no_named_sessions:     \"名前付きセッションが見つかりません。\\n`/title セッション名` で現在のセッションに名前を付けると、後で `/resume セッション名` で戻れます。\"\n     list_header:           \"📋 **名前付きセッション**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\n使い方: `/resume <セッション名>`\"\n+    list_footer_numbered:  \"\\n使い方: `/resume <セッション名>` または `/resume <番号>`（例: 最新のセッションには `/resume 1`）\"\n     list_failed:           \"セッションを一覧表示できませんでした: {error}\"\n+    out_of_range:          \"再開インデックス {index} は範囲外です。\\n引数なしで `/resume` を実行すると、利用可能なセッションが表示されます。\"\n     not_found:             \"'**{name}**' に一致するセッションが見つかりません。\\n引数なしで `/resume` を実行すると利用可能なセッションを表示します。\"\n     already_on:            \"📌 既にセッション **{name}** にいます。\"\n     switch_failed:         \"セッションの切り替えに失敗しました。\""]},{"file":"locales/ko.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     no_named_sessions:     \"이름이 지정된 세션이 없습니다.\\n현재 세션에 이름을 지정하려면 `/title 내 세션`을 사용하고, 나중에 `/resume 내 세션`으로 돌아오세요.\"\n     list_header:           \"📋 **이름이 지정된 세션**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\n사용법: `/resume <session name>`\"\n+    list_footer_numbered:  \"\\n사용법: `/resume <세션 이름>` 또는 `/resume <번호>` (예: 가장 최근 세션은 `/resume 1`)\"\n     list_failed:           \"세션 목록을 가져올 수 없습니다: {error}\"\n+    out_of_range:          \"재개 인덱스 {index}이(가) 범위를 벗어났습니다.\\n인자 없이 `/resume`을 실행하면 사용 가능한 세션이 표시됩니다.\"\n     not_found:             \"'**{name}**'와 일치하는 세션이 없습니다.\\n사용 가능한 세션을 보려면 인수 없이 `/resume`을 사용하세요.\"\n     already_on:            \"📌 이미 **{name}** 세션에 있습니다.\"\n     switch_failed:         \"세션 전환에 실패했습니다.\""]},{"file":"locales/pt.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     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.\"\n     list_header:           \"📋 **Sessões com nome**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\nUso: `/resume <nome da sessão>`\"\n+    list_footer_numbered:  \"\\nUso: `/resume <nome da sessão>` ou `/resume <número>` (ex.: `/resume 1` para a mais recente)\"\n     list_failed:           \"Não foi possível listar as sessões: {error}\"\n+    out_of_range:          \"O índice de retomada {index} está fora do intervalo.\\nUse `/resume` sem argumentos para ver as sessões disponíveis.\"\n     not_found:             \"Não foi encontrada nenhuma sessão correspondente a '**{name}**'.\\nUsa `/resume` sem argumentos para ver as sessões disponíveis.\"\n     already_on:            \"📌 Já estás na sessão **{name}**.\"\n     switch_failed:         \"Falha ao mudar de sessão.\""]},{"file":"locales/ru.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     no_named_sessions:     \"Именованных сеансов не найдено.\\nИспользуйте `/title Мой сеанс`, чтобы назвать текущий сеанс, затем `/resume Мой сеанс`, чтобы вернуться к нему позже.\"\n     list_header:           \"📋 **Именованные сеансы**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\nИспользование: `/resume <название сеанса>`\"\n+    list_footer_numbered:  \"\\nИспользование: `/resume <имя сеанса>` или `/resume <номер>` (например, `/resume 1` для самого недавнего)\"\n     list_failed:           \"Не удалось получить список сеансов: {error}\"\n+    out_of_range:          \"Индекс возобновления {index} вне диапазона.\\nИспользуйте `/resume` без аргументов, чтобы увидеть доступные сеансы.\"\n     not_found:             \"Сеанс, соответствующий '**{name}**', не найден.\\nИспользуйте `/resume` без аргументов, чтобы увидеть доступные сеансы.\"\n     already_on:            \"📌 Уже в сеансе **{name}**.\"\n     switch_failed:         \"Не удалось переключить сеанс.\""]},{"file":"locales/tr.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     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.\"\n     list_header:           \"📋 **Adlandırılmış Oturumlar**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\nKullanım: `/resume <oturum adı>`\"\n+    list_footer_numbered:  \"\\nKullanım: `/resume <oturum adı>` veya `/resume <numara>` (örn. en yenisi için `/resume 1`)\"\n     list_failed:           \"Oturumlar listelenemedi: {error}\"\n+    out_of_range:          \"Devam endeksi {index} aralık dışında.\\nKullanılabilir oturumları görmek için `/resume` komutunu argümansız çalıştırın.\"\n     not_found:             \"'**{name}**' ile eşleşen oturum bulunamadı.\\nKullanılabilir oturumları görmek için argümansız `/resume` kullanın.\"\n     already_on:            \"📌 Zaten **{name}** oturumundasınız.\"\n     switch_failed:         \"Oturum değiştirilemedi.\""]},{"file":"locales/uk.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     no_named_sessions:     \"Іменованих сеансів не знайдено.\\nВикористайте `/title Мій сеанс`, щоб назвати поточний сеанс, потім `/resume Мій сеанс`, щоб повернутися до нього.\"\n     list_header:           \"📋 **Іменовані сеанси**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\nВикористання: `/resume <назва сеансу>`\"\n+    list_footer_numbered:  \"\\nВикористання: `/resume <назва сесії>` або `/resume <номер>` (наприклад, `/resume 1` для найновішої)\"\n     list_failed:           \"Не вдалося отримати список сеансів: {error}\"\n+    out_of_range:          \"Індекс відновлення {index} поза межами діапазону.\\nВикористовуйте `/resume` без аргументів, щоб переглянути доступні сесії.\"\n     not_found:             \"Сеанс, що відповідає '**{name}**', не знайдено.\\nВикористайте `/resume` без аргументів, щоб побачити доступні сеанси.\"\n     already_on:            \"📌 Уже в сеансі **{name}**.\"\n     switch_failed:         \"Не вдалося переключити сеанс.\""]},{"file":"locales/zh-hant.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     no_named_sessions:     \"找不到已命名的工作階段。\\n使用 `/title 我的工作階段` 為目前工作階段命名，然後使用 `/resume 我的工作階段` 返回。\"\n     list_header:           \"📋 **已命名工作階段**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\n用法：`/resume <工作階段名稱>`\"\n+    list_footer_numbered:  \"\\n用法：`/resume <會話名稱>` 或 `/resume <編號>`（例如，`/resume 1` 表示最近的會話）\"\n     list_failed:           \"無法列出工作階段：{error}\"\n+    out_of_range:          \"恢復索引 {index} 超出範圍。\\n請使用不帶參數的 `/resume` 查看可用會話。\"\n     not_found:             \"找不到符合 '**{name}**' 的工作階段。\\n使用不帶參數的 `/resume` 檢視可用的工作階段。\"\n     already_on:            \"📌 已在工作階段 **{name}** 上。\"\n     switch_failed:         \"切換工作階段失敗。\""]},{"file":"locales/zh.yaml","hunks":["@@ -222,9 +222,12 @@ gateway:\n     no_named_sessions:     \"未找到已命名的会话。\\n使用 `/title 我的会话` 为当前会话命名，然后用 `/resume 我的会话` 返回。\"\n     list_header:           \"📋 **已命名会话**\\n\"\n     list_item:             \"• **{title}**{preview_part}\"\n+    list_item_numbered:    \"{index}. **{title}**{preview_part}\"\n     list_preview_suffix:   \" — _{preview}_\"\n     list_footer:           \"\\n用法：`/resume <会话名称>`\"\n+    list_footer_numbered:  \"\\n用法：`/resume <会话名称>` 或 `/resume <编号>`（例如，`/resume 1` 表示最近的会话）\"\n     list_failed:           \"无法列出会话：{error}\"\n+    out_of_range:          \"恢复索引 {index} 超出范围。\\n请使用不带参数的 `/resume` 查看可用会话。\"\n     not_found:             \"未找到匹配 '**{name}**' 的会话。\\n使用不带参数的 `/resume` 查看可用会话。\"\n     already_on:            \"📌 已在会话 **{name}** 上。\"\n     switch_failed:         \"切换会话失败。\""]},{"file":"scripts/release.py","hunks":["@@ -1283,6 +1283,7 @@\n     \"[redacted:email]\": \"egilewski\",  # PR #30432 (MEDIA path traversal fix, GHSA-jmf9-9729-7pp8)\n     \"[redacted:email]\": \"McClean-Edison\",  # PR #29817 (register_auxiliary_task plugin API)\n     \"[redacted:email]\": \"SamuelZ12\",  # PR #7480 (show recap after in-session resume)\n+    \"[redacted:email]\": \"daizhonggeng\",  # PR #9020 (numbered /resume selection)\n }\n \n "]},{"file":"tests/cli/test_cli_init.py","hunks":["@@ -331,7 +331,8 @@ def test_resume_without_target_lists_recent_sessions(self, capsys):\n \n         assert \"Recent sessions\" in output\n         assert \"Checking Running Hermes Agent\" in output\n-        assert \"Use /resume <session id or title> to continue\" in output\n+        assert \"Use /resume\" in output\n+        assert \"session title\" in output\n \n     def test_resume_updates_hermes_session_id_env_and_context(self, tmp_path):\n         from gateway.session_context import _UNSET, _VAR_MAP, get_session_env"]},{"file":"tests/cli/test_cli_resume_command.py","hunks":["@@ -0,0 +1,77 @@\n+from unittest.mock import MagicMock, patch\n+\n+from cli import HermesCLI\n+\n+\n+def _make_cli():\n+    cli_obj = HermesCLI.__new__(HermesCLI)\n+    cli_obj.session_id = \"current_session\"\n+    cli_obj._resumed = False\n+    cli_obj._pending_title = None\n+    cli_obj.conversation_history = []\n+    cli_obj.agent = None\n+    cli_obj._session_db = MagicMock()\n+    # _handle_resume_command now triggers _display_resumed_history (#31695),\n+    # which reads self.resume_display. \"minimal\" short-circuits the recap so\n+    # the test only exercises session-switch behavior.\n+    cli_obj.resume_display = \"minimal\"\n+    return cli_obj\n+\n+\n+class TestCliResumeCommand:\n+    def test_show_recent_sessions_includes_indexes_and_resume_hint(self, capsys):\n+        cli_obj = _make_cli()\n+        cli_obj._list_recent_sessions = MagicMock(return_value=[\n+            {\"id\": \"sess_002\", \"title\": \"Coding\", \"preview\": \"build feature\", \"last_active\": None},\n+            {\"id\": \"sess_001\", \"title\": \"Research\", \"preview\": \"read docs\", \"last_active\": None},\n+        ])\n+\n+        shown = cli_obj._show_recent_sessions(reason=\"resume\")\n+        output = capsys.readouterr().out\n+\n+        assert shown is True\n+        assert \"1\" in output\n+        assert \"2\" in output\n+        assert \"Coding\" in output\n+        assert \"Research\" in output\n+        assert \"/resume 2\" in output\n+        assert \"/resume <session title>\" in output\n+\n+    def test_handle_resume_by_index_switches_to_numbered_session(self):\n+        cli_obj = _make_cli()\n+        cli_obj._list_recent_sessions = MagicMock(return_value=[\n+            {\"id\": \"sess_002\", \"title\": \"Coding\"},\n+            {\"id\": \"sess_001\", \"title\": \"Research\"},\n+        ])\n+        cli_obj._session_db.get_session.return_value = {\"id\": \"sess_001\", \"title\": \"Research\"}\n+        cli_obj._session_db.get_messages_as_conversation.return_value = [\n+            {\"role\": \"user\", \"content\": \"hello\"},\n+            {\"role\": \"assistant\", \"content\": \"hi\"},\n+        ]\n+        # resolve_resume_session_id passes the id through when no compression chain.\n+        cli_obj._session_db.resolve_resume_session_id.return_value = \"sess_001\"\n+\n+        with (\n+            patch(\"hermes_cli.main._resolve_session_by_name_or_id\", return_value=None),\n+            patch(\"cli._cprint\") as mock_cprint,\n+        ):\n+            cli_obj._handle_resume_command(\"/resume 2\")\n+\n+        printed = \" \".join(str(call) for call in mock_cprint.call_args_list)\n+        assert cli_obj.session_id == \"sess_001\"\n+        assert \"Resumed session sess_001\" in printed\n+        assert \"Research\" in printed\n+\n+    def test_handle_resume_by_index_out_of_range(self):\n+        cli_obj = _make_cli()\n+        cli_obj._list_recent_sessions = MagicMock(return_value=[\n+            {\"id\": \"sess_002\", \"title\": \"Coding\"},\n+        ])\n+\n+        with patch(\"cli._cprint\") as mock_cprint:\n+            cli_obj._handle_resume_command(\"/resume 9\")\n+\n+        printed = \" \".join(str(call) for call in mock_cprint.call_args_list)\n+        assert \"out of range\" in printed.lower()\n+        assert \"/resume\" in printed\n+        assert cli_obj.session_id == \"current_session\""]},{"file":"tests/gateway/test_resume_command.py","hunks":["@@ -88,6 +88,9 @@ async def test_list_named_sessions_when_no_arg(self, tmp_path):\n         assert \"Research\" in result\n         assert \"Coding\" in result\n         assert \"Named Sessions\" in result\n+        assert \"1.\" in result\n+        assert \"2.\" in result\n+        assert \"/resume 1\" in result\n         db.close()\n \n     @pytest.mark.asyncio","@@ -104,6 +107,47 @@ async def test_list_shows_usage_when_no_titled(self, tmp_path):\n         assert \"/title\" in result\n         db.close()\n \n+    @pytest.mark.asyncio\n+    async def test_resume_by_index(self, tmp_path):\n+        \"\"\"Numeric argument resumes the indexed titled session from the list.\"\"\"\n+        from hermes_state import SessionDB\n+        db = SessionDB(db_path=tmp_path / \"state.db\")\n+        db.create_session(\"sess_001\", \"telegram\")\n+        db.create_session(\"sess_002\", \"telegram\")\n+        db.set_session_title(\"sess_001\", \"Research\")\n+        db.set_session_title(\"sess_002\", \"Coding\")\n+        db.create_session(\"current_session_001\", \"telegram\")\n+\n+        event = _make_event(text=\"/resume 2\")\n+        runner = _make_runner(session_db=db, current_session_id=\"current_session_001\",\n+                              event=event)\n+        result = await runner._handle_resume_command(event)\n+\n+        assert \"Resumed\" in result\n+        runner.session_store.switch_session.assert_called_once()\n+        call_args = runner.session_store.switch_session.call_args\n+        assert call_args[0][1] == \"sess_001\"\n+        db.close()\n+\n+    @pytest.mark.asyncio\n+    async def test_resume_index_out_of_range(self, tmp_path):\n+        \"\"\"Out-of-range numeric arguments show a helpful error.\"\"\"\n+        from hermes_state import SessionDB\n+        db = SessionDB(db_path=tmp_path / \"state.db\")\n+        db.create_session(\"sess_001\", \"telegram\")\n+        db.set_session_title(\"sess_001\", \"Research\")\n+        db.create_session(\"current_session_001\", \"telegram\")\n+\n+        event = _make_event(text=\"/resume 9\")\n+        runner = _make_runner(session_db=db, current_session_id=\"current_session_001\",\n+                              event=event)\n+        result = await runner._handle_resume_command(event)\n+\n+        assert \"out of range\" in result.lower()\n+        assert \"/resume\" in result\n+        runner.session_store.switch_session.assert_not_called()\n+        db.close()\n+\n     @pytest.mark.asyncio\n     async def test_resume_by_name(self, tmp_path):\n         \"\"\"Resolves a title and switches to that session.\"\"\"\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}