feat(commands): /compact alias + --preview/--dry-run flags for /compress (#3243 salvage)
Salvaged from PR #3243 by @Mibayy, reimplemented against current main (the original diff targeted a removed gateway/run.py handler). - /compact is now a first-class alias of /compress (CLI, gateway, Telegram/Slack/Discord command lists, autocomplete) — also fixes the dangling '/compact' references in gateway error messages (gateway/run.py context-exhausted banners). - --preview / --dry-run: report what WOULD be compressed (message counts, token estimate, 'here [N]' boundary) without touching the transcript. Flags coexist with the existing 'here [N]' / focus-topic args on both the CLI and gateway surfaces via shared pure helpers in hermes_cli/partial_compress.py. - --aggressive (LLM-free hard truncation) is intentionally NOT implemented: it would need its own transcript-persistence branch outside the guarded _compress_context rotation machinery (#44794 data-loss class). The flag is recognized and returns an explanatory message pointing at '/compress here [N]' and /undo instead of being mis-parsed as a focus topic. - locales: gateway.compress.aggressive_unsupported added to all 16 catalogs (parity test enforced). - release.py: AUTHOR_MAP entry for contributor credit.
This commit is contained in:
parent
fb74ddf7fe
commit
ce9aa869fc
23 changed files with 449 additions and 2 deletions
35
cli.py
35
cli.py
|
|
@ -9314,9 +9314,11 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||
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 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
|
|||
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:
|
||||
|
|
|
|||
|
|
@ -3044,13 +3044,44 @@ class GatewaySlashCommandsMixin:
|
|||
# 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 = estimate_request_tokens_rough(_pv_msgs)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -88,8 +88,8 @@ COMMAND_REGISTRY: list[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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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 モデル設定を確認してください。"
|
||||
|
|
|
|||
|
|
@ -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 모델 설정을 확인하세요."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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 模型設定。"
|
||||
|
|
|
|||
|
|
@ -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 模型配置。"
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json"
|
|||
|
||||
# Auto-extracted from noreply emails + manual overrides
|
||||
AUTHOR_MAP = {
|
||||
"louis@letsfive.io": "Mibayy", # PR #3243 salvage (/compact alias + preview/aggressive flags for /compress)
|
||||
"ai-lab@foxmail.com": "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)
|
||||
"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)
|
||||
"louis@letsfive.io": "Mibayy", # PR #3296 salvage (status: provider label honors config.yaml model.base_url, not just OPENAI_BASE_URL env)
|
||||
|
|
|
|||
155
tests/cli/test_compress_flags.py
Normal file
155
tests/cli/test_compress_flags.py
Normal file
|
|
@ -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
|
||||
120
tests/gateway/test_compress_preview.py
Normal file
120
tests/gateway/test_compress_preview.py
Normal file
|
|
@ -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
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue