NousResearch/hermes-agent

New /compact alias and flags for /compress command

Cluster 3240 · agent. Data as of

Nothing open on GitHub. Fix merged in #57029 on 2 Jul 2026. Check it covers your case.

The short answer

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

These members propose adding a /compact alias for the /compress command, along with preview, dry-run, and aggressive flags to enhance context compaction behavior. The work appears to include a salvaged prior pull request and a related feature request.

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.

  • #57029 feat(commands): /compact alias + preview flags for /compress (#3243 salvage)

    merged to main

    files: cli.py, gateway/slash_commands.py, hermes_cli/commands.py, hermes_cli/partial_compress.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 (+3 more)

    Show the diff
    • cli.py
      @@ -9314,9 +9314,11 @@ def _manual_compress(self, cmd_original: str = ""):
                   return
       
               from hermes_cli.partial_compress import (
      +            extract_compress_flags,
                   parse_partial_compress_args,
                   rejoin_compressed_head_and_tail,
                   split_history_for_partial_compress,
      +            summarize_compress_preview,
               )
       
               # Args after the command word (e.g. "/compress here 3" -> "here 3").
      @@ -9326,9 +9328,42 @@ def _manual_compress(self, cmd_original: str = ""):
                   if len(_parts) > 1:
                       raw_args = _parts[1].strip()
       
      +        # Strip --preview/--dry-run/--aggressive before positional parsing
      +        # so the flags coexist with 'here [N]' / focus-topic forms.
      +        raw_args, preview, aggressive = extract_compress_flags(raw_args)
               partial, keep_last, focus_topic = parse_partial_compress_args(raw_args)
               focus_topic = focus_topic or ""
       
      +        if aggressive:
      +            # LLM-free hard truncation is not supported: it would need its
      +            # own transcript-persistence path outside the guarded
      +            # _compress_context rotation machinery. Surface that instead of
      +            # silently mis-parsing the flag as a focus topic.
      +            print("(._.) --aggressive is not supported; use '/compress here [N]' "
      +                  "to keep only recent exchanges, or /undo to drop turns.")
      +            if not preview:
      +                return
      +
      +        if preview:
      +            from agent.model_metadata import estimate_request_tokens_rough
      +            _sys_prompt = getattr(self.agent, "_cached_system_prompt", "") or ""
      +            _tools = getattr(self.agent, "tools", None) or None
      +            approx_tokens = estimate_request_tokens_rough(
      +                self.conversation_history,
      +                system_prompt=_sys_prompt,
      +                tools=_tools,
      +            )
      +            report = summarize_compress_preview(
      +                self.conversation_history,
      +                partial,
      +                keep_last,
      +                focus_topic or None,
      +                approx_tokens,
      +            )
      +            for line in report["lines"]:
      +                print(f"🗜️  {line}")
      +            return
      +
               original_count = len(self.conversation_history)
               with self._busy_command("Compressing context..."):
                   try:
    • gateway/slash_commands.py
      @@ -3043,13 +3043,44 @@ async def _handle_compress_command(self, event: MessageEvent) -> str:
               # Parse args: either a focus topic (full compress) or the
               # boundary-aware "here [N]" form (partial compress).
               from hermes_cli.partial_compress import (
      +            extract_compress_flags,
                   parse_partial_compress_args,
                   rejoin_compressed_head_and_tail,
                   split_history_for_partial_compress,
      +            summarize_compress_preview,
               )
               _raw_args = (event.get_command_args() or "").strip()
      +        # Strip --preview/--dry-run/--aggressive before positional parsing
      +        # so the flags coexist with 'here [N]' / focus-topic forms.
      +        _raw_args, _preview, _aggressive = extract_compress_flags(_raw_args)
               partial, keep_last, focus_topic = parse_partial_compress_args(_raw_args)
       
      +        _agg_note = ""
      +        if _aggressive:
      +            # LLM-free hard truncation is not supported on this surface —
      +            # it would need its own transcript-persistence branch outside
      +            # the guarded _compress_context rotation machinery (#44794).
      +            _agg_note = t("gateway.compress.aggressive_unsupported")
      +            if not _preview:
      +                return _agg_note
      +
      +        if _preview:
      +            # Report what WOULD be compressed — no agent, no writes.
      +            from agent.model_metadata import estimate_request_tokens_rough
      +            _pv_msgs = [
      +                {"role": m.get("role"), "content": m.get("content")}
      +                for m in history
      +                if m.get("role") in {"user", "assistant"} and m.get("content")
      +            ]
      +            approx_tokens = [redacted:secret-assignment]
      +            report = summarize_compress_preview(
      +                _pv_msgs, partial, keep_last, focus_topic, approx_tokens
      +            )
      +            lines = [f"🗜️ {line}" for line in report["lines"]]
      +            if _aggressive:
      +                lines.append(_agg_note)
      +            return "\n".join(lines)
      +
               try:
                   from run_agent import AIAgent
                   from agent.manual_compression_feedback import summarize_manual_compression
    • hermes_cli/commands.py
      @@ -88,8 +88,8 @@ class CommandDef:
                      args_hint="<platform>", cli_only=True),
           CommandDef("branch", "Branch the current session (explore a different path)", "Session",
                      aliases=("fork",), args_hint="[name]"),
      -    CommandDef("compress", "Compress conversation context (add 'here [N]' to keep recent N turns)", "Session",
      -               args_hint="[here [N] | focus topic]"),
      +    CommandDef("compress", "Compress conversation context (add 'here [N]' to keep recent N turns; --preview shows what would happen)", "Session",
      +               aliases=("compact",), args_hint="[here [N] | focus topic | --preview|--dry-run]"),
           CommandDef("rollback", "List or restore filesystem checkpoints", "Session",
                      args_hint="[number]"),
           CommandDef("snapshot", "Create or restore state snapshots of Hermes config/state", "Session",
    • hermes_cli/partial_compress.py
      @@ -108,6 +108,95 @@ def parse_partial_compress_args(
           return False, DEFAULT_KEEP_LAST, text or None
       
       
      +def extract_compress_flags(raw_args: str) -> Tuple[str, bool, bool]:
      +    """Strip ``--preview``/``--dry-run``/``--aggressive`` flags from the
      +    argument string after ``/compress`` (or its ``/compact`` alias).
      +
      +    Flags may appear anywhere and coexist with the positional forms
      +    (``here [N]``, ``--keep N``, or a focus topic); the returned
      +    remainder is what :func:`parse_partial_compress_args` should see.
      +
      +    Returns ``(remaining_args, preview, aggressive_requested)``:
      +
      +    * ``preview`` — True when ``--preview`` or ``--dry-run`` was given.
      +      The caller must report what WOULD be compressed (message counts,
      +      token estimate, boundary) and make **no changes**.
      +    * ``aggressive_requested`` — True when ``--aggressive`` was given.
      +      The current surfaces do not implement an LLM-free hard-truncate
      +      path (it would need its own transcript-persistence branch outside
      +      the guarded ``_compress_context`` rotation machinery), so callers
      +      surface a "not supported" note instead of silently treating the
      +      flag as a focus topic.
      +    """
      +    preview = False
      +    aggressive = False
      +    kept: List[str] = []
      +    for tok in (raw_args or "").split():
      +        low = tok.lower()
      +        if low in ("--preview", "--dry-run", "--dryrun"):
      +            preview = True
      +        elif low == "--aggressive":
      +            aggressive = True
      +        else:
      +            kept.append(tok)
      +    return " ".join(kept), preview, aggressive
      +
      +
      +def summarize_compress_preview(
      +    history: List[Dict[str, Any]],
      +    partial: bool,
      +    keep_last: int,
      +    focus_topic: Optional[str],
      +    approx_tokens: int,
      +) -> Dict[str, Any]:
      +    """Build the ``/compress --preview`` report — pure, no side effects.
      +
      +    Shared by the CLI (``cli.py::_manual_compress``) and the gateway
      +    (``gateway/slash_commands.py::_handle_compress_command``) so both
      +    surfaces report the same numbers the real run would use.
      +
      +    Returns a dict with ``head_count``/``tail_count``/``lines`` where
      +    ``lines`` is a ready-to-print list of report strings.
      +    """
      +    total = len(history)
      +    head = list(history)
      +    tail: List[Dict[str, Any]] = []
      +    effective_partial = partial
      +    if partial:
      +        head, tail = split_history_for_partial_compress(history, keep_last)
      +        if not tail:
      +            # Same degenerate-split fallback the real run applies.
      +            effective_partial = False
      +            head, tail = list(history), []
      +
      +    lines = [
      +        "Preview — no changes made.",
      +        f"Would compress {len(head)} of {total} message(s) "
      +        f"(~{approx_tokens:,} tokens currently in context).",
      +    ]
      +    if effective_partial:
      +        lines.append(
      +            f"Boundary: keeping the last {keep_last} exchange(s) "
      +            f"({len(tail)} message(s)) verbatim."
      +        )
      +    elif partial:
      +        lines.append(
      +            "Boundary: 'here' split would keep everything — "
      +            "falling back to full compression."
      +        )
      +    if focus_topic:
      +        lines.append(f'Focus topic: "{focus_topic}"')
      +    lines.append("Run the command again without --preview to apply.")
      +
      +    return {
      +        "head_count": len(head),
      +        "tail_count": len(tail),
      +        "total": total,
      +        "partial": effective_partial,
      +        "lines": lines,
      +    }
      +
      +
       def _coerce_keep(value: str) -> int:
           """Parse a keep-count token, clamping to [1, MAX_KEEP_LAST]."""
           try:
    • locales/af.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "Nie genoeg gesprek om saam te pers nie (ten minste 4 boodskappe nodig)."
           no_provider:           "Geen verskaffer opgestel nie -- kan nie saampers nie."
           nothing_to_do:         "Niks om saam te pers nie (die transkripsie is steeds heeltemal beskermde konteks)."
      +    aggressive_unsupported: "--aggressive word nie ondersteun nie; gebruik '/compress here [N]' om net onlangse uitruilings te behou, of /undo om beurte te verwyder."
           focus_line:            "Fokus: \"{topic}\""
           summary_failed:        "⚠️ Opsomming kon nie gegenereer word nie ({error}). {count} historiese boodskap(pe) is verwyder en met 'n plekhouer vervang; vroeëre konteks kan nie meer herstel word nie. Oorweeg om jou auxiliary.compression-modelopstelling na te gaan."
           aborted:               "⚠️ Kompressie gestaak ({error}). Geen boodskappe is laat val nie — die gesprek is onveranderd. Voer /compress uit om weer te probeer, /reset vir 'n skoon sessie, of kyk na jou auxiliary.compression-modelkonfigurasie."
    • locales/de.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "Nicht genug Konversation zum Komprimieren (mindestens 4 Nachrichten erforderlich)."
           no_provider:           "Kein Anbieter konfiguriert — Komprimierung nicht möglich."
           nothing_to_do:         "Noch nichts zu komprimieren (das Transkript ist weiterhin vollständig geschützter Kontext)."
      +    aggressive_unsupported: "--aggressive wird nicht unterstützt; verwende '/compress here [N]', um nur die letzten Austausche zu behalten, oder /undo, um Beiträge zu entfernen."
           focus_line:            "Fokus: \"{topic}\""
           summary_failed:        "⚠️ Zusammenfassungsgenerierung fehlgeschlagen ({error}). {count} historische Nachricht(en) wurden entfernt und durch einen Platzhalter ersetzt; früherer Kontext ist nicht mehr wiederherstellbar. Überprüfen Sie die Konfiguration des auxiliary.compression-Modells."
           aborted:               "⚠️ Komprimierung abgebrochen ({error}). Keine Nachrichten wurden entfernt — die Konversation ist unverändert. Führe /compress aus, um es erneut zu versuchen, /reset für eine neue Sitzung, oder prüfe deine auxiliary.compression-Modellkonfiguration."
    • locales/en.yaml
      @@ -103,6 +103,7 @@ gateway:
           not_enough:            "Not enough conversation to compress (need at least 4 messages)."
           no_provider:           "No provider configured -- cannot compress."
           nothing_to_do:         "Nothing to compress yet (the transcript is still all protected context)."
      +    aggressive_unsupported: "--aggressive is not supported; use '/compress here [N]' to keep only recent exchanges, or /undo to drop turns."
           focus_line:            "Focus: \"{topic}\""
           summary_failed:        "⚠️ Summary generation failed ({error}). {count} historical message(s) were removed and replaced with a placeholder; earlier context is no longer recoverable. Consider checking your auxiliary.compression model configuration."
           aborted:               "⚠️ Compression aborted ({error}). No messages were dropped — conversation is unchanged. Run /compress to retry, /reset for a clean session, or check your auxiliary.compression model configuration."
    • locales/es.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "No hay suficiente conversación para comprimir (se necesitan al menos 4 mensajes)."
           no_provider:           "No hay proveedor configurado — no se puede comprimir."
           nothing_to_do:         "Aún no hay nada que comprimir (la transcripción sigue siendo todo contexto protegido)."
      +    aggressive_unsupported: "--aggressive no es compatible; usa '/compress here [N]' para conservar solo los intercambios recientes, o /undo para eliminar turnos."
           focus_line:            "Enfoque: \"{topic}\""
           summary_failed:        "⚠️ Falló la generación del resumen ({error}). Se eliminaron {count} mensaje(s) históricos y se reemplazaron por un marcador; el contexto anterior ya no se puede recuperar. Considera revisar la configuración del modelo auxiliary.compression."
           aborted:               "⚠️ Compresión abortada ({error}). No se eliminó ningún mensaje — la conversación está intacta. Ejecuta /compress para reintentar, /reset para una sesión limpia, o revisa la configuración de tu modelo auxiliary.compression."
    • locales/fr.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "Conversation insuffisante pour la compression (au moins 4 messages nécessaires)."
           no_provider:           "Aucun fournisseur configuré — compression impossible."
           nothing_to_do:         "Rien à compresser pour l'instant (la transcription est encore entièrement du contexte protégé)."
      +    aggressive_unsupported: "--aggressive n'est pas pris en charge ; utilisez '/compress here [N]' pour ne conserver que les échanges récents, ou /undo pour supprimer des tours."
           focus_line:            "Focus : \"{topic}\""
           summary_failed:        "⚠️ Échec de la génération du résumé ({error}). {count} message(s) historique(s) ont été supprimés et remplacés par un espace réservé ; le contexte antérieur n'est plus récupérable. Vérifiez la configuration du modèle auxiliary.compression."
           aborted:               "⚠️ Compression interrompue ({error}). Aucun message n'a été supprimé — la conversation est inchangée. Lancez /compress pour réessayer, /reset pour une nouvelle session, ou vérifiez la configuration de votre modèle auxiliary.compression."
    • locales/ga.yaml
      @@ -92,6 +92,7 @@ gateway:
           not_enough:            "Níl go leor comhrá le dlúthú (teastaíonn 4 theachtaireacht ar a laghad)."
           no_provider:           "Níl aon soláthraí cumraithe — ní féidir dlúthú."
           nothing_to_do:         "Níl aon rud le dlúthú fós (tá an traschríbhinn fós uile mar chomhthéacs cosanta)."
      +    aggressive_unsupported: "Ní thacaítear le --aggressive; úsáid '/compress here [N]' chun na malartuithe is déanaí amháin a choinneáil, nó /undo chun sealanna a scriosadh."
           focus_line:            "Fócas: \"{topic}\""
           summary_failed:        "⚠️ Theip ar ghiniúint achoimre ({error}). Baineadh {count} teachtaireacht stairiúil agus cuireadh ionadaí ina n-áit; níl an comhthéacs roimhe seo in-aisghabhála a thuilleadh. Smaoinigh ar an gcumraíocht auxiliary.compression a sheiceáil."
           aborted:               "⚠️ Cuireadh deireadh leis an dlúthú ({error}). Níor baineadh aon teachtaireacht — tá an comhrá gan athrú. Rith /compress chun é a thriail arís, /reset le haghaidh seisiún glan, nó seiceáil do chumraíocht samhla auxiliary.compression."
    • locales/hu.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "Nincs elég beszélgetés a tömörítéshez (legalább 4 üzenet kell)."
           no_provider:           "Nincs konfigurált szolgáltató — nem lehet tömöríteni."
           nothing_to_do:         "Még nincs mit tömöríteni (a teljes átirat még védett kontextus)."
      +    aggressive_unsupported: "A --aggressive nem támogatott; használd a '/compress here [N]' parancsot, hogy csak a legutóbbi váltásokat tartsd meg, vagy a /undo parancsot körök törléséhez."
           focus_line:            "Fókusz: \"{topic}\""
           summary_failed:        "⚠️ Az összefoglaló generálása sikertelen ({error}). {count} korábbi üzenet eltávolítva és helykitöltővel helyettesítve; a korábbi kontextus már nem helyreállítható. Érdemes ellenőrizni az auxiliary.compression modell konfigurációját."
           aborted:               "⚠️ Tömörítés megszakítva ({error}). Egyetlen üzenet sem lett eldobva — a beszélgetés változatlan. Futtass /compress parancsot az újrapróbálkozáshoz, /reset egy új munkamenethez, vagy ellenőrizd az auxiliary.compression modell konfigurációt."
    • locales/it.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "Conversazione insufficiente da comprimere (servono almeno 4 messaggi)."
           no_provider:           "Nessun provider configurato — impossibile comprimere."
           nothing_to_do:         "Niente da comprimere per ora (la trascrizione è ancora tutta contesto protetto)."
      +    aggressive_unsupported: "--aggressive non è supportato; usa '/compress here [N]' per conservare solo gli scambi recenti, oppure /undo per rimuovere i turni."
           focus_line:            "Focus: \"{topic}\""
           summary_failed:        "⚠️ Generazione del riepilogo non riuscita ({error}). {count} messaggio/i storico/i sono stati rimossi e sostituiti con un segnaposto; il contesto precedente non è più recuperabile. Considera di controllare la configurazione del modello auxiliary.compression."
           aborted:               "⚠️ Compressione interrotta ({error}). Nessun messaggio è stato eliminato — la conversazione è invariata. Esegui /compress per riprovare, /reset per una nuova sessione, o controlla la configurazione del modello auxiliary.compression."
    • locales/ja.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "圧縮するための会話が不十分です (少なくとも 4 件のメッセージが必要)。"
           no_provider:           "プロバイダーが構成されていません — 圧縮できません。"
           nothing_to_do:         "まだ圧縮するものがありません (トランスクリプトはすべて保護されたコンテキストのままです)。"
      +    aggressive_unsupported: "--aggressive はサポートされていません。'/compress here [N]' で直近のやり取りだけを残すか、/undo でターンを削除してください。"
           focus_line:            "フォーカス: \"{topic}\""
           summary_failed:        "⚠️ 要約の生成に失敗しました ({error})。{count} 件の履歴メッセージが削除され、プレースホルダーに置き換えられました。以前のコンテキストは復元できません。auxiliary.compression モデルの設定を確認してください。"
           aborted:               "⚠️ 圧縮が中止されました ({error})。メッセージは削除されていません — 会話はそのままです。再試行するには /compress、新しいセッションを開始するには /reset を実行するか、auxiliary.compression モデル設定を確認してください。"
    • locales/ko.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "압축할 대화가 충분하지 않습니다 (최소 4개의 메시지가 필요합니다)."
           no_provider:           "구성된 제공자가 없습니다 -- 압축할 수 없습니다."
           nothing_to_do:         "아직 압축할 내용이 없습니다 (대화 내용이 모두 보호된 컨텍스트입니다)."
      +    aggressive_unsupported: "--aggressive는 지원되지 않습니다. '/compress here [N]'으로 최근 대화만 유지하거나 /undo로 턴을 삭제하세요."
           focus_line:            "초점: \"{topic}\""
           summary_failed:        "⚠️ 요약 생성에 실패했습니다 ({error}). 과거 메시지 {count}개가 제거되어 자리표시자로 대체되었으며, 이전 컨텍스트는 더 이상 복구할 수 없습니다. auxiliary.compression 모델 설정을 확인해 보세요."
           aborted:               "⚠️ 압축이 중단되었습니다 ({error}). 메시지가 삭제되지 않았으며 대화는 그대로 유지됩니다. 다시 시도하려면 /compress를 실행하거나, 새 세션을 시작하려면 /reset을 사용하거나, auxiliary.compression 모델 설정을 확인하세요."
    • locales/pt.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "Não há conversa suficiente para comprimir (são necessárias pelo menos 4 mensagens)."
           no_provider:           "Nenhum fornecedor configurado — não é possível comprimir."
           nothing_to_do:         "Ainda não há nada para comprimir (a transcrição continua a ser todo o contexto protegido)."
      +    aggressive_unsupported: "--aggressive não é suportado; usa '/compress here [N]' para manter apenas as trocas recentes, ou /undo para remover turnos."
           focus_line:            "Foco: \"{topic}\""
           summary_failed:        "⚠️ Falha ao gerar o resumo ({error}). {count} mensagem(ns) histórica(s) foram removidas e substituídas por um marcador; o contexto anterior já não pode ser recuperado. Considera verificar a configuração do modelo auxiliary.compression."
           aborted:               "⚠️ Compressão abortada ({error}). Nenhuma mensagem foi removida — a conversa está inalterada. Executa /compress para tentar de novo, /reset para uma sessão nova, ou verifica a configuração do modelo auxiliary.compression."
    • locales/ru.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "Недостаточно беседы для сжатия (нужно минимум 4 сообщения)."
           no_provider:           "Провайдер не настроен — сжатие невозможно."
           nothing_to_do:         "Пока нечего сжимать (стенограмма всё ещё полностью является защищённым контекстом)."
      +    aggressive_unsupported: "--aggressive не поддерживается; используйте '/compress here [N]', чтобы сохранить только недавние обмены, или /undo, чтобы удалить ходы."
           focus_line:            "Фокус: \"{topic}\""
           summary_failed:        "⚠️ Не удалось сгенерировать сводку ({error}). {count} историч. сообщений было удалено и заменено заполнителем; предыдущий контекст больше нельзя восстановить. Проверьте конфигурацию модели auxiliary.compression."
           aborted:               "⚠️ Сжатие прервано ({error}). Сообщения не были удалены — разговор не изменился. Запустите /compress для повторной попытки, /reset для новой сессии или проверьте конфигурацию модели auxiliary.compression."
    • locales/tr.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "Sıkıştırmak için yeterli konuşma yok (en az 4 mesaj gerekli)."
           no_provider:           "Yapılandırılmış sağlayıcı yok — sıkıştırılamıyor."
           nothing_to_do:         "Henüz sıkıştırılacak bir şey yok (transkript hâlâ tamamen korunan bağlam)."
      +    aggressive_unsupported: "--aggressive desteklenmiyor; yalnızca son alışverişleri tutmak için '/compress here [N]' kullanın veya turları silmek için /undo kullanın."
           focus_line:            "Odak: \"{topic}\""
           summary_failed:        "⚠️ Özet oluşturma başarısız ({error}). {count} geçmiş mesaj kaldırılıp yer tutucuyla değiştirildi; önceki bağlam artık kurtarılamaz. auxiliary.compression model yapılandırmanızı kontrol edin."
           aborted:               "⚠️ Sıkıştırma iptal edildi ({error}). Hiçbir mesaj silinmedi — konuşma değişmedi. Tekrar denemek için /compress, temiz bir oturum için /reset komutunu çalıştırın veya auxiliary.compression model yapılandırmanızı kontrol edin."
    • locales/uk.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "Недостатньо розмови для стиснення (потрібно щонайменше 4 повідомлення)."
           no_provider:           "Постачальника не налаштовано — неможливо стиснути."
           nothing_to_do:         "Поки що немає що стискати (стенограма все ще є повністю захищеним контекстом)."
      +    aggressive_unsupported: "--aggressive не підтримується; використовуйте '/compress here [N]', щоб зберегти лише останні обміни, або /undo, щоб видалити ходи."
           focus_line:            "Фокус: \"{topic}\""
           summary_failed:        "⚠️ Не вдалося згенерувати зведення ({error}). {count} історичних повідомлень було видалено та замінено заповнювачем; попередній контекст більше не можна відновити. Перевірте конфігурацію моделі auxiliary.compression."
           aborted:               "⚠️ Стиснення скасовано ({error}). Жодне повідомлення не було видалено — розмова не змінилася. Виконайте /compress, щоб повторити спробу, /reset для нової сесії, або перевірте конфігурацію моделі auxiliary.compression."
    • locales/zh-hant.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "對話內容不足,無法壓縮(至少需要 4 則訊息)。"
           no_provider:           "未設定提供方 — 無法壓縮。"
           nothing_to_do:         "目前沒有可壓縮的內容(對話記錄仍全部為受保護的上下文)。"
      +    aggressive_unsupported: "不支援 --aggressive;請使用 '/compress here [N]' 只保留最近的對話,或使用 /undo 刪除回合。"
           focus_line:            "聚焦:\"{topic}\""
           summary_failed:        "⚠️ 摘要產生失敗({error})。{count} 則歷史訊息已被移除並以佔位符取代;先前的上下文已無法復原。建議檢查 auxiliary.compression 模型設定。"
           aborted:               "⚠️ 壓縮已中止 ({error})。未刪除任何訊息 — 對話保持不變。執行 /compress 重試,執行 /reset 開始新工作階段,或檢查你的 auxiliary.compression 模型設定。"
    • locales/zh.yaml
      @@ -88,6 +88,7 @@ gateway:
           not_enough:            "对话内容不足,无法压缩(至少需要 4 条消息)。"
           no_provider:           "未配置提供方 — 无法压缩。"
           nothing_to_do:         "暂无可压缩内容(对话记录仍全部为受保护上下文)。"
      +    aggressive_unsupported: "不支持 --aggressive;请使用 '/compress here [N]' 只保留最近的对话,或使用 /undo 删除回合。"
           focus_line:            "聚焦:\"{topic}\""
           summary_failed:        "⚠️ 摘要生成失败({error})。{count} 条历史消息已被移除并替换为占位符;之前的上下文已无法恢复。建议检查 auxiliary.compression 模型配置。"
           aborted:               "⚠️ 压缩已中止 ({error})。未删除任何消息 — 对话保持不变。运行 /compress 重试,运行 /reset 开始新会话,或检查你的 auxiliary.compression 模型配置。"
    • scripts/release.py
      @@ -45,6 +45,7 @@
       
       # Auto-extracted from noreply emails + manual overrides
       AUTHOR_MAP = {
      +    "[redacted:email]": "Mibayy",  # PR #3243 salvage (/compact alias + preview/aggressive flags for /compress)
           "[redacted:email]": "CrazyBoyM",  # PR #55828 salvage (image_gen openai-codex: wire image-to-image / reference-image editing via Codex Responses input_image parts; magic-byte + read-guard + 25MB-cap + clamp-to-16 hardening)
           "[email protected]": "r0gersm1th",  # PR #3219 salvage (whatsapp bridge: resolve LID sender IDs to phone numbers in the message payload so phone-based allowlists match; commit authored by collaborator r0gersm1th, PR by @ajmeese7)
           "[redacted:email]": "tarunravi",  # PR #2696 salvage (api-server: inline MEDIA:<path> image tags as base64 data URLs in final responses so remote OpenAI-compatible frontends can render server-local screenshots; the PR's tool-progress-streaming and SSE-sentinel pieces were independently superseded on main)
    • tests/cli/test_compress_flags.py
      @@ -0,0 +1,155 @@
      +"""Tests for /compress --preview/--dry-run/--aggressive flags and the
      +/compact alias (PR #3243 salvage).
      +
      +Covers the pure helpers in ``hermes_cli.partial_compress`` plus alias
      +resolution in the command registry. The CLI and gateway surfaces both
      +route through these helpers, so the flag semantics are pinned here once.
      +"""
      +
      +from hermes_cli.commands import COMMANDS, resolve_command
      +from hermes_cli.partial_compress import (
      +    DEFAULT_KEEP_LAST,
      +    extract_compress_flags,
      +    parse_partial_compress_args,
      +    summarize_compress_preview,
      +)
      +
      +
      +def _history(n_pairs: int) -> list[dict[str, str]]:
      +    h: list[dict[str, str]] = []
      +    for i in range(n_pairs):
      +        h.append({"role": "user", "content": f"u{i}"})
      +        h.append({"role": "assistant", "content": f"a{i}"})
      +    return h
      +
      +
      +# ── /compact alias resolution ─────────────────────────────────────────
      +
      +
      +def test_compact_resolves_to_compress():
      +    cmd = resolve_command("compact")
      +    assert cmd is not None
      +    assert cmd.name == "compress"
      +    assert "compact" in cmd.aliases
      +
      +
      +def test_compact_alias_with_slash():
      +    cmd = resolve_command("/compact")
      +    assert cmd is not None and cmd.name == "compress"
      +
      +
      +def test_compact_listed_in_flat_commands():
      +    assert "/compact" in COMMANDS
      +    assert "alias for /compress" in COMMANDS["/compact"]
      +
      +
      +def test_compress_args_hint_documents_preview():
      +    cmd = resolve_command("compress")
      +    assert cmd is not None
      +    assert "--preview" in (cmd.args_hint or "")
      +
      +
      +# ── extract_compress_flags ────────────────────────────────────────────
      +
      +
      +def test_no_flags_passthrough():
      +    rest, preview, aggressive = extract_compress_flags("here 3")
      +    assert rest == "here 3"
      +    assert preview is False
      +    assert aggressive is False
      +
      +
      +def test_preview_flag_stripped():
      +    rest, preview, aggressive = extract_compress_flags("--preview")
      +    assert rest == ""
      +    assert preview is True
      +    assert aggressive is False
      +
      +
      +def test_dry_run_is_preview():
      +    for form in ("--dry-run", "--dryrun", "--DRY-RUN"):
      +        _, preview, _ = extract_compress_flags(form)
      +        assert preview is True, form
      +
      +
      +def test_aggressive_flag_detected():
      +    rest, preview, aggressive = extract_compress_flags("--aggressive")
      +    assert rest == ""
      +    assert preview is False
      +    assert aggressive is True
      +
      +
      +def test_flags_coexist_with_here_form():
      +    rest, preview, aggressive = extract_compress_flags("--preview here 4")
      +    assert rest == "here 4"
      +    assert preview is True
      +    partial, keep, focus = parse_partial_compress_args(rest)
      +    assert partial is True and keep == 4 and focus is None
      +
      +
      +def test_flags_coexist_with_focus_topic():
      +    rest, preview, _ = extract_compress_flags("database schema --dry-run")
      +    assert rest == "database schema"
      +    assert preview is True
      +    partial, _, focus = parse_partial_compress_args(rest)
      +    assert partial is False and focus == "database schema"
      +
      +
      +def test_aggressive_dry_run_combo():
      +    rest, preview, aggressive = extract_compress_flags("--aggressive --dry-run")
      +    assert rest == ""
      +    assert preview is True and aggressive is True
      +
      +
      +def test_empty_args():
      +    rest, preview, aggressive = extract_compress_flags("")
      +    assert rest == "" and preview is False and aggressive is False
      +
      +
      +# ── summarize_compress_preview ────────────────────────────────────────
      +
      +
      +def test_preview_full_compress_counts():
      +    hist = _history(5)
      +    report = summarize_compress_preview(hist, False, DEFAULT_KEEP_LAST, None, 1234)
      +    assert report["head_count"] == 10
      +    assert report["tail_count"] == 0
      +    assert report["total"] == 10
      +    assert report["partial"] is False
      +    joined = "\n".join(report["lines"])
      +    assert "no changes made" in joined.lower()
      +    assert "10 of 10" in joined
      +    assert "1,234" in joined
      +
      +
      +def test_preview_partial_boundary_counts():
      +    hist = _history(5)
      +    report = summarize_compress_preview(hist, True, 2, None, 999)
      +    # Keeping last 2 exchanges = 4 tail messages, 6 head messages.
      +    assert report["head_count"] == 6
      +    assert report["tail_count"] == 4
      +    assert report["partial"] is True
      +    joined = "\n".join(report["lines"])
      +    assert "last 2 exchange" in joined
      +
      +
      +def test_preview_partial_degenerate_falls_back_to_full():
      +    hist = _history(2)  # keep_last=5 would swallow everything
      +    report = summarize_compress_preview(hist, True, 5, None, 100)
      +    assert report["partial"] is False
      +    assert report["head_count"] == 4
      +    joined = "\n".join(report["lines"])
      +    assert "falling back to full compression" in joined
      +
      +
      +def test_preview_includes_focus_topic():
      +    hist = _history(4)
      +    report = summarize_compress_preview(hist, False, DEFAULT_KEEP_LAST, "db schema", 50)
      +    assert 'Focus topic: "db schema"' in "\n".join(report["lines"])
      +
      +
      +def test_preview_is_side_effect_free():
      +    hist = _history(4)
      +    before = [dict(m) for m in hist]
      +    summarize_compress_preview(hist, True, 1, None, 10)
      +    assert hist == before
    • tests/gateway/test_compress_preview.py
      @@ -0,0 +1,120 @@
      +"""Tests for gateway /compress --preview/--dry-run/--aggressive flags
      +(PR #3243 salvage).
      +
      +The preview path must return a report WITHOUT building an agent or
      +touching the transcript; --aggressive must return an explanatory
      +message rather than being mis-parsed as a focus topic.
      +"""
      +
      +from datetime import datetime
      +from unittest.mock import MagicMock
      +
      +import pytest
      +
      +from gateway.config import GatewayConfig, Platform, PlatformConfig
      +from gateway.platforms.base import MessageEvent
      +from gateway.session import SessionEntry, SessionSource, build_session_key
      +
      +
      +def _make_source() -> SessionSource:
      +    return SessionSource(
      +        platform=Platform.TELEGRAM,
      +        user_id="u1",
      +        chat_id="c1",
      +        user_name="tester",
      +        chat_type="dm",
      +    )
      +
      +
      +def _make_event(text: str) -> MessageEvent:
      +    return MessageEvent(text=text, source=_make_source(), message_id="m1")
      +
      +
      +def _make_history(n_pairs: int = 3) -> list[dict[str, str]]:
      +    h: list[dict[str, str]] = []
      +    for i in range(n_pairs):
      +        h.append({"role": "user", "content": f"u{i}"})
      +        h.append({"role": "assistant", "content": f"a{i}"})
      +    return h
      +
      +
      +def _make_runner(history: list[dict[str, str]]):
      +    from gateway.run import GatewayRunner
      +
      +    runner = object.__new__(GatewayRunner)
      +    runner.config = GatewayConfig(
      +        platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="***")}
      +    )
      +    session_entry = SessionEntry(
      +        session_key=build_session_key(_make_source()),
      +        session_id="sess-1",
      +        created_at=datetime.now(),
      +        updated_at=datetime.now(),
      +        platform=Platform.TELEGRAM,
      +        chat_type="dm",
      +    )
      +    runner.session_store = MagicMock()
      +    runner.session_store.get_or_create_session.return_value = session_entry
      +    runner.session_store.load_transcript.return_value = history
      +    runner.session_store.rewrite_transcript = MagicMock()
      +    runner.session_store.update_session = MagicMock()
      +    runner.session_store._save = MagicMock()
      +    runner._session_db = None
      +    return runner
      +
      +
      [redacted:email]
      +async def test_preview_reports_without_mutating():
      +    runner = _make_runner(_make_history(3))
      +    result = await runner._handle_compress_command(_make_event("/compress --preview"))
      +    assert "no changes made" in result.lower()
      +    assert "6 of 6" in result
      +    runner.session_store.rewrite_transcript.assert_not_called()
      +    runner.session_store.update_session.assert_not_called()
      +
      +
      [redacted:email]
      +async def test_dry_run_alias_matches_preview():
      +    runner = _make_runner(_make_history(3))
      +    result = await runner._handle_compress_command(_make_event("/compress --dry-run"))
      +    assert "no changes made" in result.lower()
      +    runner.session_store.rewrite_transcript.assert_not_called()
      +
      +
      [redacted:email]
      +async def test_preview_with_here_boundary():
      +    runner = _make_runner(_make_history(4))
      +    result = await runner._handle_compress_command(
      +        _make_event("/compress --preview here 2")
      +    )
      +    assert "last 2 exchange" in result
      +    assert "4 of 8" in result
      +    runner.session_store.rewrite_transcript.assert_not_called()
      +
      +
      [redacted:email]
      +async def test_aggressive_returns_unsupported_note_without_mutating():
      +    runner = _make_runner(_make_history(3))
      +    result = await runner._handle_compress_command(
      +        _make_event("/compress --aggressive")
      +    )
      +    assert "--aggressive is not supported" in result
      +    runner.session_store.rewrite_transcript.assert_not_called()
      +
      +
      [redacted:email]
      +async def test_aggressive_dry_run_shows_preview_plus_note():
      +    runner = _make_runner(_make_history(3))
      +    result = await runner._handle_compress_command(
      +        _make_event("/compress --aggressive --dry-run")
      +    )
      +    assert "no changes made" in result.lower()
      +    assert "--aggressive is not supported" in result
      +    runner.session_store.rewrite_transcript.assert_not_called()
      +
      +
      [redacted:email]
      +async def test_preview_still_requires_enough_history():
      +    runner = _make_runner(_make_history(1))  # only 2 messages
      +    result = await runner._handle_compress_command(_make_event("/compress --preview"))
      +    assert "not enough" in result.lower()

1 PR closed without merging

From the close comments: merged via another change 1

  • #3243 feat(commands): `/compact` alias + `--preview`/`--dry-run`/`--aggressive` flags for `/compress` · merged via another change → #57029, #44794
Closed issues · 1
  • Closed#3240[Feature]: Enhanced Context Compaction Command (/compact)
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.67: 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
agent
Merged PRs
#57029
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
2 Jul 2026

For your agent

The same facts, typed.