{"repo":"NousResearch/hermes-agent","public_id":"3240","display_label":"New /compact alias and flags for /compress command","summary":"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.","subsystem":"agent","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":57029,"merged_at":"2026-07-02T12:10:31+00:00"},"resolution":{"state":"resolved","by":"pr-57029"},"freshness":{"state":"verified","last_verified_at":"2026-07-02T12:10:31+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":3240,"pr_number":57029,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-02T12:10:31+00:00"}],"members":[{"kind":"issue","number":3240,"title":"[Feature]: Enhanced Context Compaction Command (/compact)","url":null,"state":"closed"},{"kind":"pr","number":3243,"title":"feat(commands): `/compact` alias + `--preview`/`--dry-run`/`--aggressive` flags for `/compress`","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#57029","number":57029},{"kind":"pr","value":"#44794","number":44794}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":57029,"title":"feat(commands): /compact alias + preview flags for /compress (#3243 salvage)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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","scripts/release.py","tests/cli/test_compress_flags.py","tests/gateway/test_compress_preview.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -9314,9 +9314,11 @@ def _manual_compress(self, cmd_original: str = \"\"):\n             return\n \n         from hermes_cli.partial_compress import (\n+            extract_compress_flags,\n             parse_partial_compress_args,\n             rejoin_compressed_head_and_tail,\n             split_history_for_partial_compress,\n+            summarize_compress_preview,\n         )\n \n         # Args after the command word (e.g. \"/compress here 3\" -> \"here 3\").","@@ -9326,9 +9328,42 @@ def _manual_compress(self, cmd_original: str = \"\"):\n             if len(_parts) > 1:\n                 raw_args = _parts[1].strip()\n \n+        # Strip --preview/--dry-run/--aggressive before positional parsing\n+        # so the flags coexist with 'here [N]' / focus-topic forms.\n+        raw_args, preview, aggressive = extract_compress_flags(raw_args)\n         partial, keep_last, focus_topic = parse_partial_compress_args(raw_args)\n         focus_topic = focus_topic or \"\"\n \n+        if aggressive:\n+            # LLM-free hard truncation is not supported: it would need its\n+            # own transcript-persistence path outside the guarded\n+            # _compress_context rotation machinery. Surface that instead of\n+            # silently mis-parsing the flag as a focus topic.\n+            print(\"(._.) --aggressive is not supported; use '/compress here [N]' \"\n+                  \"to keep only recent exchanges, or /undo to drop turns.\")\n+            if not preview:\n+                return\n+\n+        if preview:\n+            from agent.model_metadata import estimate_request_tokens_rough\n+            _sys_prompt = getattr(self.agent, \"_cached_system_prompt\", \"\") or \"\"\n+            _tools = getattr(self.agent, \"tools\", None) or None\n+            approx_tokens = estimate_request_tokens_rough(\n+                self.conversation_history,\n+                system_prompt=_sys_prompt,\n+                tools=_tools,\n+            )\n+            report = summarize_compress_preview(\n+                self.conversation_history,\n+                partial,\n+                keep_last,\n+                focus_topic or None,\n+                approx_tokens,\n+            )\n+            for line in report[\"lines\"]:\n+                print(f\"🗜️  {line}\")\n+            return\n+\n         original_count = len(self.conversation_history)\n         with self._busy_command(\"Compressing context...\"):\n             try:"]},{"file":"gateway/slash_commands.py","hunks":["@@ -3043,13 +3043,44 @@ async def _handle_compress_command(self, event: MessageEvent) -> str:\n         # Parse args: either a focus topic (full compress) or the\n         # boundary-aware \"here [N]\" form (partial compress).\n         from hermes_cli.partial_compress import (\n+            extract_compress_flags,\n             parse_partial_compress_args,\n             rejoin_compressed_head_and_tail,\n             split_history_for_partial_compress,\n+            summarize_compress_preview,\n         )\n         _raw_args = (event.get_command_args() or \"\").strip()\n+        # Strip --preview/--dry-run/--aggressive before positional parsing\n+        # so the flags coexist with 'here [N]' / focus-topic forms.\n+        _raw_args, _preview, _aggressive = extract_compress_flags(_raw_args)\n         partial, keep_last, focus_topic = parse_partial_compress_args(_raw_args)\n \n+        _agg_note = \"\"\n+        if _aggressive:\n+            # LLM-free hard truncation is not supported on this surface —\n+            # it would need its own transcript-persistence branch outside\n+            # the guarded _compress_context rotation machinery (#44794).\n+            _agg_note = t(\"gateway.compress.aggressive_unsupported\")\n+            if not _preview:\n+                return _agg_note\n+\n+        if _preview:\n+            # Report what WOULD be compressed — no agent, no writes.\n+            from agent.model_metadata import estimate_request_tokens_rough\n+            _pv_msgs = [\n+                {\"role\": m.get(\"role\"), \"content\": m.get(\"content\")}\n+                for m in history\n+                if m.get(\"role\") in {\"user\", \"assistant\"} and m.get(\"content\")\n+            ]\n+            approx_tokens = [redacted:secret-assignment]\n+            report = summarize_compress_preview(\n+                _pv_msgs, partial, keep_last, focus_topic, approx_tokens\n+            )\n+            lines = [f\"🗜️ {line}\" for line in report[\"lines\"]]\n+            if _aggressive:\n+                lines.append(_agg_note)\n+            return \"\\n\".join(lines)\n+\n         try:\n             from run_agent import AIAgent\n             from agent.manual_compression_feedback import summarize_manual_compression"]},{"file":"hermes_cli/commands.py","hunks":["@@ -88,8 +88,8 @@ class CommandDef:\n                args_hint=\"<platform>\", cli_only=True),\n     CommandDef(\"branch\", \"Branch the current session (explore a different path)\", \"Session\",\n                aliases=(\"fork\",), args_hint=\"[name]\"),\n-    CommandDef(\"compress\", \"Compress conversation context (add 'here [N]' to keep recent N turns)\", \"Session\",\n-               args_hint=\"[here [N] | focus topic]\"),\n+    CommandDef(\"compress\", \"Compress conversation context (add 'here [N]' to keep recent N turns; --preview shows what would happen)\", \"Session\",\n+               aliases=(\"compact\",), args_hint=\"[here [N] | focus topic | --preview|--dry-run]\"),\n     CommandDef(\"rollback\", \"List or restore filesystem checkpoints\", \"Session\",\n                args_hint=\"[number]\"),\n     CommandDef(\"snapshot\", \"Create or restore state snapshots of Hermes config/state\", \"Session\","]},{"file":"hermes_cli/partial_compress.py","hunks":["@@ -108,6 +108,95 @@ def parse_partial_compress_args(\n     return False, DEFAULT_KEEP_LAST, text or None\n \n \n+def extract_compress_flags(raw_args: str) -> Tuple[str, bool, bool]:\n+    \"\"\"Strip ``--preview``/``--dry-run``/``--aggressive`` flags from the\n+    argument string after ``/compress`` (or its ``/compact`` alias).\n+\n+    Flags may appear anywhere and coexist with the positional forms\n+    (``here [N]``, ``--keep N``, or a focus topic); the returned\n+    remainder is what :func:`parse_partial_compress_args` should see.\n+\n+    Returns ``(remaining_args, preview, aggressive_requested)``:\n+\n+    * ``preview`` — True when ``--preview`` or ``--dry-run`` was given.\n+      The caller must report what WOULD be compressed (message counts,\n+      token estimate, boundary) and make **no changes**.\n+    * ``aggressive_requested`` — True when ``--aggressive`` was given.\n+      The current surfaces do not implement an LLM-free hard-truncate\n+      path (it would need its own transcript-persistence branch outside\n+      the guarded ``_compress_context`` rotation machinery), so callers\n+      surface a \"not supported\" note instead of silently treating the\n+      flag as a focus topic.\n+    \"\"\"\n+    preview = False\n+    aggressive = False\n+    kept: List[str] = []\n+    for tok in (raw_args or \"\").split():\n+        low = tok.lower()\n+        if low in (\"--preview\", \"--dry-run\", \"--dryrun\"):\n+            preview = True\n+        elif low == \"--aggressive\":\n+            aggressive = True\n+        else:\n+            kept.append(tok)\n+    return \" \".join(kept), preview, aggressive\n+\n+\n+def summarize_compress_preview(\n+    history: List[Dict[str, Any]],\n+    partial: bool,\n+    keep_last: int,\n+    focus_topic: Optional[str],\n+    approx_tokens: int,\n+) -> Dict[str, Any]:\n+    \"\"\"Build the ``/compress --preview`` report — pure, no side effects.\n+\n+    Shared by the CLI (``cli.py::_manual_compress``) and the gateway\n+    (``gateway/slash_commands.py::_handle_compress_command``) so both\n+    surfaces report the same numbers the real run would use.\n+\n+    Returns a dict with ``head_count``/``tail_count``/``lines`` where\n+    ``lines`` is a ready-to-print list of report strings.\n+    \"\"\"\n+    total = len(history)\n+    head = list(history)\n+    tail: List[Dict[str, Any]] = []\n+    effective_partial = partial\n+    if partial:\n+        head, tail = split_history_for_partial_compress(history, keep_last)\n+        if not tail:\n+            # Same degenerate-split fallback the real run applies.\n+            effective_partial = False\n+            head, tail = list(history), []\n+\n+    lines = [\n+        \"Preview — no changes made.\",\n+        f\"Would compress {len(head)} of {total} message(s) \"\n+        f\"(~{approx_tokens:,} tokens currently in context).\",\n+    ]\n+    if effective_partial:\n+        lines.append(\n+            f\"Boundary: keeping the last {keep_last} exchange(s) \"\n+            f\"({len(tail)} message(s)) verbatim.\"\n+        )\n+    elif partial:\n+        lines.append(\n+            \"Boundary: 'here' split would keep everything — \"\n+            \"falling back to full compression.\"\n+        )\n+    if focus_topic:\n+        lines.append(f'Focus topic: \"{focus_topic}\"')\n+    lines.append(\"Run the command again without --preview to apply.\")\n+\n+    return {\n+        \"head_count\": len(head),\n+        \"tail_count\": len(tail),\n+        \"total\": total,\n+        \"partial\": effective_partial,\n+        \"lines\": lines,\n+    }\n+\n+\n def _coerce_keep(value: str) -> int:\n     \"\"\"Parse a keep-count token, clamping to [1, MAX_KEEP_LAST].\"\"\"\n     try:"]},{"file":"locales/af.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"Nie genoeg gesprek om saam te pers nie (ten minste 4 boodskappe nodig).\"\n     no_provider:           \"Geen verskaffer opgestel nie -- kan nie saampers nie.\"\n     nothing_to_do:         \"Niks om saam te pers nie (die transkripsie is steeds heeltemal beskermde konteks).\"\n+    aggressive_unsupported: \"--aggressive word nie ondersteun nie; gebruik '/compress here [N]' om net onlangse uitruilings te behou, of /undo om beurte te verwyder.\"\n     focus_line:            \"Fokus: \\\"{topic}\\\"\"\n     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.\"\n     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.\""]},{"file":"locales/de.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"Nicht genug Konversation zum Komprimieren (mindestens 4 Nachrichten erforderlich).\"\n     no_provider:           \"Kein Anbieter konfiguriert — Komprimierung nicht möglich.\"\n     nothing_to_do:         \"Noch nichts zu komprimieren (das Transkript ist weiterhin vollständig geschützter Kontext).\"\n+    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.\"\n     focus_line:            \"Fokus: \\\"{topic}\\\"\"\n     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.\"\n     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.\""]},{"file":"locales/en.yaml","hunks":["@@ -103,6 +103,7 @@ gateway:\n     not_enough:            \"Not enough conversation to compress (need at least 4 messages).\"\n     no_provider:           \"No provider configured -- cannot compress.\"\n     nothing_to_do:         \"Nothing to compress yet (the transcript is still all protected context).\"\n+    aggressive_unsupported: \"--aggressive is not supported; use '/compress here [N]' to keep only recent exchanges, or /undo to drop turns.\"\n     focus_line:            \"Focus: \\\"{topic}\\\"\"\n     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.\"\n     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.\""]},{"file":"locales/es.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"No hay suficiente conversación para comprimir (se necesitan al menos 4 mensajes).\"\n     no_provider:           \"No hay proveedor configurado — no se puede comprimir.\"\n     nothing_to_do:         \"Aún no hay nada que comprimir (la transcripción sigue siendo todo contexto protegido).\"\n+    aggressive_unsupported: \"--aggressive no es compatible; usa '/compress here [N]' para conservar solo los intercambios recientes, o /undo para eliminar turnos.\"\n     focus_line:            \"Enfoque: \\\"{topic}\\\"\"\n     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.\"\n     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.\""]},{"file":"locales/fr.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"Conversation insuffisante pour la compression (au moins 4 messages nécessaires).\"\n     no_provider:           \"Aucun fournisseur configuré — compression impossible.\"\n     nothing_to_do:         \"Rien à compresser pour l'instant (la transcription est encore entièrement du contexte protégé).\"\n+    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.\"\n     focus_line:            \"Focus : \\\"{topic}\\\"\"\n     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.\"\n     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.\""]},{"file":"locales/ga.yaml","hunks":["@@ -92,6 +92,7 @@ gateway:\n     not_enough:            \"Níl go leor comhrá le dlúthú (teastaíonn 4 theachtaireacht ar a laghad).\"\n     no_provider:           \"Níl aon soláthraí cumraithe — ní féidir dlúthú.\"\n     nothing_to_do:         \"Níl aon rud le dlúthú fós (tá an traschríbhinn fós uile mar chomhthéacs cosanta).\"\n+    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.\"\n     focus_line:            \"Fócas: \\\"{topic}\\\"\"\n     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.\"\n     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.\""]},{"file":"locales/hu.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"Nincs elég beszélgetés a tömörítéshez (legalább 4 üzenet kell).\"\n     no_provider:           \"Nincs konfigurált szolgáltató — nem lehet tömöríteni.\"\n     nothing_to_do:         \"Még nincs mit tömöríteni (a teljes átirat még védett kontextus).\"\n+    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.\"\n     focus_line:            \"Fókusz: \\\"{topic}\\\"\"\n     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.\"\n     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.\""]},{"file":"locales/it.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"Conversazione insufficiente da comprimere (servono almeno 4 messaggi).\"\n     no_provider:           \"Nessun provider configurato — impossibile comprimere.\"\n     nothing_to_do:         \"Niente da comprimere per ora (la trascrizione è ancora tutta contesto protetto).\"\n+    aggressive_unsupported: \"--aggressive non è supportato; usa '/compress here [N]' per conservare solo gli scambi recenti, oppure /undo per rimuovere i turni.\"\n     focus_line:            \"Focus: \\\"{topic}\\\"\"\n     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.\"\n     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.\""]},{"file":"locales/ja.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"圧縮するための会話が不十分です (少なくとも 4 件のメッセージが必要)。\"\n     no_provider:           \"プロバイダーが構成されていません — 圧縮できません。\"\n     nothing_to_do:         \"まだ圧縮するものがありません (トランスクリプトはすべて保護されたコンテキストのままです)。\"\n+    aggressive_unsupported: \"--aggressive はサポートされていません。'/compress here [N]' で直近のやり取りだけを残すか、/undo でターンを削除してください。\"\n     focus_line:            \"フォーカス: \\\"{topic}\\\"\"\n     summary_failed:        \"⚠️ 要約の生成に失敗しました ({error})。{count} 件の履歴メッセージが削除され、プレースホルダーに置き換えられました。以前のコンテキストは復元できません。auxiliary.compression モデルの設定を確認してください。\"\n     aborted:               \"⚠️ 圧縮が中止されました ({error})。メッセージは削除されていません — 会話はそのままです。再試行するには /compress、新しいセッションを開始するには /reset を実行するか、auxiliary.compression モデル設定を確認してください。\""]},{"file":"locales/ko.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"압축할 대화가 충분하지 않습니다 (최소 4개의 메시지가 필요합니다).\"\n     no_provider:           \"구성된 제공자가 없습니다 -- 압축할 수 없습니다.\"\n     nothing_to_do:         \"아직 압축할 내용이 없습니다 (대화 내용이 모두 보호된 컨텍스트입니다).\"\n+    aggressive_unsupported: \"--aggressive는 지원되지 않습니다. '/compress here [N]'으로 최근 대화만 유지하거나 /undo로 턴을 삭제하세요.\"\n     focus_line:            \"초점: \\\"{topic}\\\"\"\n     summary_failed:        \"⚠️ 요약 생성에 실패했습니다 ({error}). 과거 메시지 {count}개가 제거되어 자리표시자로 대체되었으며, 이전 컨텍스트는 더 이상 복구할 수 없습니다. auxiliary.compression 모델 설정을 확인해 보세요.\"\n     aborted:               \"⚠️ 압축이 중단되었습니다 ({error}). 메시지가 삭제되지 않았으며 대화는 그대로 유지됩니다. 다시 시도하려면 /compress를 실행하거나, 새 세션을 시작하려면 /reset을 사용하거나, auxiliary.compression 모델 설정을 확인하세요.\""]},{"file":"locales/pt.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"Não há conversa suficiente para comprimir (são necessárias pelo menos 4 mensagens).\"\n     no_provider:           \"Nenhum fornecedor configurado — não é possível comprimir.\"\n     nothing_to_do:         \"Ainda não há nada para comprimir (a transcrição continua a ser todo o contexto protegido).\"\n+    aggressive_unsupported: \"--aggressive não é suportado; usa '/compress here [N]' para manter apenas as trocas recentes, ou /undo para remover turnos.\"\n     focus_line:            \"Foco: \\\"{topic}\\\"\"\n     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.\"\n     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.\""]},{"file":"locales/ru.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"Недостаточно беседы для сжатия (нужно минимум 4 сообщения).\"\n     no_provider:           \"Провайдер не настроен — сжатие невозможно.\"\n     nothing_to_do:         \"Пока нечего сжимать (стенограмма всё ещё полностью является защищённым контекстом).\"\n+    aggressive_unsupported: \"--aggressive не поддерживается; используйте '/compress here [N]', чтобы сохранить только недавние обмены, или /undo, чтобы удалить ходы.\"\n     focus_line:            \"Фокус: \\\"{topic}\\\"\"\n     summary_failed:        \"⚠️ Не удалось сгенерировать сводку ({error}). {count} историч. сообщений было удалено и заменено заполнителем; предыдущий контекст больше нельзя восстановить. Проверьте конфигурацию модели auxiliary.compression.\"\n     aborted:               \"⚠️ Сжатие прервано ({error}). Сообщения не были удалены — разговор не изменился. Запустите /compress для повторной попытки, /reset для новой сессии или проверьте конфигурацию модели auxiliary.compression.\""]},{"file":"locales/tr.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"Sıkıştırmak için yeterli konuşma yok (en az 4 mesaj gerekli).\"\n     no_provider:           \"Yapılandırılmış sağlayıcı yok — sıkıştırılamıyor.\"\n     nothing_to_do:         \"Henüz sıkıştırılacak bir şey yok (transkript hâlâ tamamen korunan bağlam).\"\n+    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.\"\n     focus_line:            \"Odak: \\\"{topic}\\\"\"\n     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.\"\n     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.\""]},{"file":"locales/uk.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"Недостатньо розмови для стиснення (потрібно щонайменше 4 повідомлення).\"\n     no_provider:           \"Постачальника не налаштовано — неможливо стиснути.\"\n     nothing_to_do:         \"Поки що немає що стискати (стенограма все ще є повністю захищеним контекстом).\"\n+    aggressive_unsupported: \"--aggressive не підтримується; використовуйте '/compress here [N]', щоб зберегти лише останні обміни, або /undo, щоб видалити ходи.\"\n     focus_line:            \"Фокус: \\\"{topic}\\\"\"\n     summary_failed:        \"⚠️ Не вдалося згенерувати зведення ({error}). {count} історичних повідомлень було видалено та замінено заповнювачем; попередній контекст більше не можна відновити. Перевірте конфігурацію моделі auxiliary.compression.\"\n     aborted:               \"⚠️ Стиснення скасовано ({error}). Жодне повідомлення не було видалено — розмова не змінилася. Виконайте /compress, щоб повторити спробу, /reset для нової сесії, або перевірте конфігурацію моделі auxiliary.compression.\""]},{"file":"locales/zh-hant.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"對話內容不足，無法壓縮（至少需要 4 則訊息）。\"\n     no_provider:           \"未設定提供方 — 無法壓縮。\"\n     nothing_to_do:         \"目前沒有可壓縮的內容（對話記錄仍全部為受保護的上下文）。\"\n+    aggressive_unsupported: \"不支援 --aggressive；請使用 '/compress here [N]' 只保留最近的對話，或使用 /undo 刪除回合。\"\n     focus_line:            \"聚焦：\\\"{topic}\\\"\"\n     summary_failed:        \"⚠️ 摘要產生失敗（{error}）。{count} 則歷史訊息已被移除並以佔位符取代；先前的上下文已無法復原。建議檢查 auxiliary.compression 模型設定。\"\n     aborted:               \"⚠️ 壓縮已中止 ({error})。未刪除任何訊息 — 對話保持不變。執行 /compress 重試，執行 /reset 開始新工作階段，或檢查你的 auxiliary.compression 模型設定。\""]},{"file":"locales/zh.yaml","hunks":["@@ -88,6 +88,7 @@ gateway:\n     not_enough:            \"对话内容不足，无法压缩（至少需要 4 条消息）。\"\n     no_provider:           \"未配置提供方 — 无法压缩。\"\n     nothing_to_do:         \"暂无可压缩内容（对话记录仍全部为受保护上下文）。\"\n+    aggressive_unsupported: \"不支持 --aggressive；请使用 '/compress here [N]' 只保留最近的对话，或使用 /undo 删除回合。\"\n     focus_line:            \"聚焦：\\\"{topic}\\\"\"\n     summary_failed:        \"⚠️ 摘要生成失败（{error}）。{count} 条历史消息已被移除并替换为占位符；之前的上下文已无法恢复。建议检查 auxiliary.compression 模型配置。\"\n     aborted:               \"⚠️ 压缩已中止 ({error})。未删除任何消息 — 对话保持不变。运行 /compress 重试，运行 /reset 开始新会话，或检查你的 auxiliary.compression 模型配置。\""]},{"file":"scripts/release.py","hunks":["@@ -45,6 +45,7 @@\n \n # Auto-extracted from noreply emails + manual overrides\n AUTHOR_MAP = {\n+    \"[redacted:email]\": \"Mibayy\",  # PR #3243 salvage (/compact alias + preview/aggressive flags for /compress)\n     \"[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)\n     \"r0gersm1th@users.noreply.github.com\": \"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)\n     \"[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)"]},{"file":"tests/cli/test_compress_flags.py","hunks":["@@ -0,0 +1,155 @@\n+\"\"\"Tests for /compress --preview/--dry-run/--aggressive flags and the\n+/compact alias (PR #3243 salvage).\n+\n+Covers the pure helpers in ``hermes_cli.partial_compress`` plus alias\n+resolution in the command registry. The CLI and gateway surfaces both\n+route through these helpers, so the flag semantics are pinned here once.\n+\"\"\"\n+\n+from hermes_cli.commands import COMMANDS, resolve_command\n+from hermes_cli.partial_compress import (\n+    DEFAULT_KEEP_LAST,\n+    extract_compress_flags,\n+    parse_partial_compress_args,\n+    summarize_compress_preview,\n+)\n+\n+\n+def _history(n_pairs: int) -> list[dict[str, str]]:\n+    h: list[dict[str, str]] = []\n+    for i in range(n_pairs):\n+        h.append({\"role\": \"user\", \"content\": f\"u{i}\"})\n+        h.append({\"role\": \"assistant\", \"content\": f\"a{i}\"})\n+    return h\n+\n+\n+# ── /compact alias resolution ─────────────────────────────────────────\n+\n+\n+def test_compact_resolves_to_compress():\n+    cmd = resolve_command(\"compact\")\n+    assert cmd is not None\n+    assert cmd.name == \"compress\"\n+    assert \"compact\" in cmd.aliases\n+\n+\n+def test_compact_alias_with_slash():\n+    cmd = resolve_command(\"/compact\")\n+    assert cmd is not None and cmd.name == \"compress\"\n+\n+\n+def test_compact_listed_in_flat_commands():\n+    assert \"/compact\" in COMMANDS\n+    assert \"alias for /compress\" in COMMANDS[\"/compact\"]\n+\n+\n+def test_compress_args_hint_documents_preview():\n+    cmd = resolve_command(\"compress\")\n+    assert cmd is not None\n+    assert \"--preview\" in (cmd.args_hint or \"\")\n+\n+\n+# ── extract_compress_flags ────────────────────────────────────────────\n+\n+\n+def test_no_flags_passthrough():\n+    rest, preview, aggressive = extract_compress_flags(\"here 3\")\n+    assert rest == \"here 3\"\n+    assert preview is False\n+    assert aggressive is False\n+\n+\n+def test_preview_flag_stripped():\n+    rest, preview, aggressive = extract_compress_flags(\"--preview\")\n+    assert rest == \"\"\n+    assert preview is True\n+    assert aggressive is False\n+\n+\n+def test_dry_run_is_preview():\n+    for form in (\"--dry-run\", \"--dryrun\", \"--DRY-RUN\"):\n+        _, preview, _ = extract_compress_flags(form)\n+        assert preview is True, form\n+\n+\n+def test_aggressive_flag_detected():\n+    rest, preview, aggressive = extract_compress_flags(\"--aggressive\")\n+    assert rest == \"\"\n+    assert preview is False\n+    assert aggressive is True\n+\n+\n+def test_flags_coexist_with_here_form():\n+    rest, preview, aggressive = extract_compress_flags(\"--preview here 4\")\n+    assert rest == \"here 4\"\n+    assert preview is True\n+    partial, keep, focus = parse_partial_compress_args(rest)\n+    assert partial is True and keep == 4 and focus is None\n+\n+\n+def test_flags_coexist_with_focus_topic():\n+    rest, preview, _ = extract_compress_flags(\"database schema --dry-run\")\n+    assert rest == \"database schema\"\n+    assert preview is True\n+    partial, _, focus = parse_partial_compress_args(rest)\n+    assert partial is False and focus == \"database schema\"\n+\n+\n+def test_aggressive_dry_run_combo():\n+    rest, preview, aggressive = extract_compress_flags(\"--aggressive --dry-run\")\n+    assert rest == \"\"\n+    assert preview is True and aggressive is True\n+\n+\n+def test_empty_args():\n+    rest, preview, aggressive = extract_compress_flags(\"\")\n+    assert rest == \"\" and preview is False and aggressive is False\n+\n+\n+# ── summarize_compress_preview ────────────────────────────────────────\n+\n+\n+def test_preview_full_compress_counts():\n+    hist = _history(5)\n+    report = summarize_compress_preview(hist, False, DEFAULT_KEEP_LAST, None, 1234)\n+    assert report[\"head_count\"] == 10\n+    assert report[\"tail_count\"] == 0\n+    assert report[\"total\"] == 10\n+    assert report[\"partial\"] is False\n+    joined = \"\\n\".join(report[\"lines\"])\n+    assert \"no changes made\" in joined.lower()\n+    assert \"10 of 10\" in joined\n+    assert \"1,234\" in joined\n+\n+\n+def test_preview_partial_boundary_counts():\n+    hist = _history(5)\n+    report = summarize_compress_preview(hist, True, 2, None, 999)\n+    # Keeping last 2 exchanges = 4 tail messages, 6 head messages.\n+    assert report[\"head_count\"] == 6\n+    assert report[\"tail_count\"] == 4\n+    assert report[\"partial\"] is True\n+    joined = \"\\n\".join(report[\"lines\"])\n+    assert \"last 2 exchange\" in joined\n+\n+\n+def test_preview_partial_degenerate_falls_back_to_full():\n+    hist = _history(2)  # keep_last=5 would swallow everything\n+    report = summarize_compress_preview(hist, True, 5, None, 100)\n+    assert report[\"partial\"] is False\n+    assert report[\"head_count\"] == 4\n+    joined = \"\\n\".join(report[\"lines\"])\n+    assert \"falling back to full compression\" in joined\n+\n+\n+def test_preview_includes_focus_topic():\n+    hist = _history(4)\n+    report = summarize_compress_preview(hist, False, DEFAULT_KEEP_LAST, \"db schema\", 50)\n+    assert 'Focus topic: \"db schema\"' in \"\\n\".join(report[\"lines\"])\n+\n+\n+def test_preview_is_side_effect_free():\n+    hist = _history(4)\n+    before = [dict(m) for m in hist]\n+    summarize_compress_preview(hist, True, 1, None, 10)\n+    assert hist == before"]},{"file":"tests/gateway/test_compress_preview.py","hunks":["@@ -0,0 +1,120 @@\n+\"\"\"Tests for gateway /compress --preview/--dry-run/--aggressive flags\n+(PR #3243 salvage).\n+\n+The preview path must return a report WITHOUT building an agent or\n+touching the transcript; --aggressive must return an explanatory\n+message rather than being mis-parsed as a focus topic.\n+\"\"\"\n+\n+from datetime import datetime\n+from unittest.mock import MagicMock\n+\n+import pytest\n+\n+from gateway.config import GatewayConfig, Platform, PlatformConfig\n+from gateway.platforms.base import MessageEvent\n+from gateway.session import SessionEntry, SessionSource, build_session_key\n+\n+\n+def _make_source() -> SessionSource:\n+    return SessionSource(\n+        platform=Platform.TELEGRAM,\n+        user_id=\"u1\",\n+        chat_id=\"c1\",\n+        user_name=\"tester\",\n+        chat_type=\"dm\",\n+    )\n+\n+\n+def _make_event(text: str) -> MessageEvent:\n+    return MessageEvent(text=text, source=_make_source(), message_id=\"m1\")\n+\n+\n+def _make_history(n_pairs: int = 3) -> list[dict[str, str]]:\n+    h: list[dict[str, str]] = []\n+    for i in range(n_pairs):\n+        h.append({\"role\": \"user\", \"content\": f\"u{i}\"})\n+        h.append({\"role\": \"assistant\", \"content\": f\"a{i}\"})\n+    return h\n+\n+\n+def _make_runner(history: list[dict[str, str]]):\n+    from gateway.run import GatewayRunner\n+\n+    runner = object.__new__(GatewayRunner)\n+    runner.config = GatewayConfig(\n+        platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token=\"***\")}\n+    )\n+    session_entry = SessionEntry(\n+        session_key=build_session_key(_make_source()),\n+        session_id=\"sess-1\",\n+        created_at=datetime.now(),\n+        updated_at=datetime.now(),\n+        platform=Platform.TELEGRAM,\n+        chat_type=\"dm\",\n+    )\n+    runner.session_store = MagicMock()\n+    runner.session_store.get_or_create_session.return_value = session_entry\n+    runner.session_store.load_transcript.return_value = history\n+    runner.session_store.rewrite_transcript = MagicMock()\n+    runner.session_store.update_session = MagicMock()\n+    runner.session_store._save = MagicMock()\n+    runner._session_db = None\n+    return runner\n+\n+\n[redacted:email]\n+async def test_preview_reports_without_mutating():\n+    runner = _make_runner(_make_history(3))\n+    result = await runner._handle_compress_command(_make_event(\"/compress --preview\"))\n+    assert \"no changes made\" in result.lower()\n+    assert \"6 of 6\" in result\n+    runner.session_store.rewrite_transcript.assert_not_called()\n+    runner.session_store.update_session.assert_not_called()\n+\n+\n[redacted:email]\n+async def test_dry_run_alias_matches_preview():\n+    runner = _make_runner(_make_history(3))\n+    result = await runner._handle_compress_command(_make_event(\"/compress --dry-run\"))\n+    assert \"no changes made\" in result.lower()\n+    runner.session_store.rewrite_transcript.assert_not_called()\n+\n+\n[redacted:email]\n+async def test_preview_with_here_boundary():\n+    runner = _make_runner(_make_history(4))\n+    result = await runner._handle_compress_command(\n+        _make_event(\"/compress --preview here 2\")\n+    )\n+    assert \"last 2 exchange\" in result\n+    assert \"4 of 8\" in result\n+    runner.session_store.rewrite_transcript.assert_not_called()\n+\n+\n[redacted:email]\n+async def test_aggressive_returns_unsupported_note_without_mutating():\n+    runner = _make_runner(_make_history(3))\n+    result = await runner._handle_compress_command(\n+        _make_event(\"/compress --aggressive\")\n+    )\n+    assert \"--aggressive is not supported\" in result\n+    runner.session_store.rewrite_transcript.assert_not_called()\n+\n+\n[redacted:email]\n+async def test_aggressive_dry_run_shows_preview_plus_note():\n+    runner = _make_runner(_make_history(3))\n+    result = await runner._handle_compress_command(\n+        _make_event(\"/compress --aggressive --dry-run\")\n+    )\n+    assert \"no changes made\" in result.lower()\n+    assert \"--aggressive is not supported\" in result\n+    runner.session_store.rewrite_transcript.assert_not_called()\n+\n+\n[redacted:email]\n+async def test_preview_still_requires_enough_history():\n+    runner = _make_runner(_make_history(1))  # only 2 messages\n+    result = await runner._handle_compress_command(_make_event(\"/compress --preview\"))\n+    assert \"not enough\" in result.lower()\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}