NousResearch/hermes-agent

Interactive session picker and numeric selection for /resume

Cluster 4988 · hermes cli. Data as of

Nothing open on GitHub. Fix merged in #31709 on 24 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
4items grouped
Known?
Yes. 4 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #31709. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
Nothing open on GitHub.

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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #31709 feat(cli,gateway): /resume accepts position numbers

    merged to main

    files: 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 (+2 more)

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

2 PRs closed without merging

From the close comments: duplicate 1 · merged via another change 1

  • #4990 feat(cli): add /resume interactive picker and numeric selection (#4988) · merged via another change → #31709
  • #8493 feat: interactive session picker for /resume and hermes resume · duplicate → #7880
Closed issues · 1
  • Closed#4988[Feature]: /resume with no arguments should show an interactive session picker
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.50: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
hermes cli
Merged PRs
#31709
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
24 May 2026

For your agent

The same facts, typed.