feat(gateway): add /context command for a detailed context-window view

A dedicated /context (alias /ctx) gateway slash command that gives a full
context-window view with:

- Usage gauge: visual bar + fraction + percentage + headroom
- Auto-compression threshold and how far away it is
- Compression count and how much the last one freed
- Cumulative session throughput (explicitly labelled as throughput,
  NOT context size — each call re-sends the window)
- Cascading fallback: running agent → cached agent → SessionStore metadata
  → rough transcript estimate

Not included (per current-main design):
- Cache reporting removed: commit 446b8e239 intentionally removed cache
  reporting from user-facing surfaces because providers that omit cached-token
  details produce misleading values
- Sync DB calls replaced with async_session_store (current main requires
  AsyncSessionStore with await)

Also rewords the /status tokens line from 'Cumulative API tokens (re-sent
each call)' to 'Lifetime tokens billed: ... (not your current context size;
use /context)' to reduce the recurring confusion that the cumulative figure
is the current context window.

Fixes salvation of PR #52184 (salvage commit replaces a 12K-commit-behind
fork branch with a fresh implementation against current main, incorporating
reviewer feedback from @whoislikemiha and the hermes-sweeper).
This commit is contained in:
CharlesMcquade 2026-07-14 22:47:05 -05:00 committed by Teknium
parent 3dccc45032
commit 4fe2fecf54
20 changed files with 654 additions and 22 deletions

View file

@ -11168,6 +11168,9 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
if event.get_command() == "status":
return await self._handle_status_command(event)
if event.get_command() in {"context", "ctx"}:
return await self._handle_context_command(event)
# Resolve the command once for all early-intercept checks below.
from hermes_cli.commands import (
ACTIVE_SESSION_BYPASS_COMMANDS as _DEDICATED_HANDLERS,
@ -11743,6 +11746,9 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
return format_status_text()
if canonical == "context":
return await self._handle_context_command(event)
if canonical == "agents":
return await self._handle_agents_command(event)

View file

@ -56,6 +56,19 @@ logger = logging.getLogger("gateway.run")
_RESET_CLEANUP_TIMEOUT_S = 30.0
def _clean_str(value: Any) -> str:
"""Strip and return a non-empty string value, or empty string."""
return value.strip() if isinstance(value, str) and value.strip() else ""
def _int_value(value: Any) -> int:
"""Safely coerce to int."""
try:
return int(value)
except (TypeError, ValueError):
return 0
def _model_switch_skew_guard() -> Optional[str]:
"""Refuse a model switch when the gateway is running stale code.
@ -701,6 +714,180 @@ class GatewaySlashCommandsMixin:
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()[:12]
return f"sha256:{digest}"
async def _handle_context_command(self, event: MessageEvent) -> str:
"""Handle /context — the dedicated context-window view.
/status shows a one-line ``used / total`` summary; this command is the
deep view: a usage gauge, auto-compression threshold and headroom,
compression count and last savings, and cumulative throughput the last
clearly labelled as throughput, NOT context size.
Resolves from the running agent (mid-turn), then the cached agent
(between turns), then the SessionStore/SessionDB metadata for a gauge
even when no agent is resident. Falls back to a transcript estimate only
as a last resort.
"""
from gateway.run import _AGENT_PENDING_SENTINEL
source = event.source
session_key = self._session_key_for_source(source)
session_entry = await self.async_session_store.get_or_create_session(source)
# Try running agent first (mid-turn), then cached agent (between turns).
agent = self._running_agents.get(session_key)
if not agent or agent is _AGENT_PENDING_SENTINEL:
cache_lock = getattr(self, "_agent_cache_lock", None)
cache = getattr(self, "_agent_cache", None)
if cache_lock is not None and cache is not None:
try:
with cache_lock:
cached = cache.get(session_key)
if cached:
agent = cached[0]
except Exception:
agent = None
has_agent = bool(agent) and agent is not _AGENT_PENDING_SENTINEL
ctx = getattr(agent, "context_compressor", None) if has_agent else None
# Resolve current-context size + window with cascading fallbacks.
# used : compressor.last_prompt_tokens → SessionStore.last_prompt_tokens
# model : agent.model → SessionDB row model
# window: compressor.context_length → get_model_context_length(model)
used = 0
context_length = 0
if ctx is not None:
used = getattr(ctx, "last_prompt_tokens", 0) or 0
context_length = getattr(ctx, "context_length", 0) or 0
model_name = _clean_str(getattr(agent, "model", "")) if has_agent else ""
if not used:
used = _int_value(getattr(session_entry, "last_prompt_tokens", 0))
if not model_name and self._session_db:
try:
row = await self._session_db.get_session(session_entry.session_id) or {}
if isinstance(row, dict):
model_name = _clean_str(row.get("model", ""))
except Exception:
model_name = ""
if not context_length and model_name:
try:
from agent.model_metadata import get_model_context_length
context_length = _int_value(
await asyncio.to_thread(get_model_context_length, model_name)
)
except Exception:
context_length = 0
# Gauge path: real current-context figure
if used > 0 and context_length > 0:
pct = min(100.0, used / context_length * 100)
headroom = max(0, context_length - used)
BAR_WIDTH = 24
filled = int(round(pct / 100 * BAR_WIDTH))
bar = "" * max(0, filled) + "" * max(0, BAR_WIDTH - filled)
lines = [
t("gateway.context.header"),
"",
t("gateway.context.model", model=model_name or "?"),
t("gateway.context.window", total=f"{context_length:,}"),
t(
"gateway.context.in_use",
used=f"{used:,}",
total=f"{context_length:,}",
pct=f"{pct:.0f}",
),
t("gateway.context.bar", bar=bar),
t("gateway.context.headroom", headroom=f"{headroom:,}"),
]
# Full view — compression / throughput need the live agent.
if ctx is not None:
threshold = getattr(ctx, "threshold_tokens", 0) or 0
threshold_pct = (getattr(ctx, "threshold_percent", 0) or 0) * 100
lines.append("")
if threshold > 0:
if used >= threshold:
lines.append(
t(
"gateway.context.over_threshold",
threshold=f"{threshold:,}",
threshold_pct=f"{threshold_pct:.0f}",
)
)
else:
lines.append(
t(
"gateway.context.threshold",
threshold=f"{threshold:,}",
threshold_pct=f"{threshold_pct:.0f}",
to_go=f"{threshold - used:,}",
)
)
compressions = getattr(ctx, "compression_count", 0) or 0
lines.append(t("gateway.context.compressions", count=compressions))
if compressions:
savings = getattr(ctx, "_last_compression_savings_pct", None)
if savings is not None:
lines.append(
t("gateway.context.last_savings", savings=f"{savings:.0f}")
)
api_calls = getattr(agent, "session_api_calls", 0) or 0
input_tokens = getattr(agent, "session_input_tokens", 0) or 0
output_tokens = getattr(agent, "session_output_tokens", 0) or 0
reasoning_tokens = getattr(agent, "session_reasoning_tokens", 0) or 0
total_tokens = getattr(agent, "session_total_tokens", 0) or 0
lines.append("")
lines.append(
t("gateway.context.totals_header", calls=api_calls)
)
lines.append(
t(
"gateway.context.totals_line",
input=f"{input_tokens:,}",
output=f"{output_tokens:,}",
reasoning=f"{reasoning_tokens:,}",
)
)
lines.append(t("gateway.context.total_billed", total=f"{total_tokens:,}"))
lines.append(t("gateway.context.throughput_note"))
else:
lines.append("")
lines.append(t("gateway.context.detail_after_first"))
return "\n".join(lines)
# Last resort: rough estimate from transcript
history = await self.async_session_store.load_transcript(session_entry.session_id)
if history:
from agent.model_metadata import estimate_messages_tokens_rough
msgs = [
m
for m in history
if m.get("role") in {"user", "assistant"} and m.get("content")
]
approx = estimate_messages_tokens_rough(msgs)
return "\n".join(
[
t("gateway.context.header"),
"",
t(
"gateway.context.estimated",
count=f"{approx:,}",
messages=len(msgs),
),
t("gateway.context.detail_after_first"),
]
)
return t("gateway.context.no_data")
def _gateway_session_origin_for_id(self, session_id: str) -> Optional[SessionSource]:
"""Best-effort origin lookup for gateway session IDs."""
lookup = getattr(type(self.session_store), "lookup_by_session_id", None)

View file

@ -120,6 +120,8 @@ COMMAND_REGISTRY: list[CommandDef] = [
CommandDef("status", "Show session, model, token, and context info", "Session"),
CommandDef("egress", "Show Docker egress proxy status", "Session",
args_hint="[status]", subcommands=("status",)),
CommandDef("context", "Show detailed context window view with usage gauge, compression stats, and throughput", "Session",
aliases=("ctx",)),
CommandDef("whoami", "Show your slash command access (admin / user)", "Info"),
CommandDef("profile", "Show active profile name and home directory", "Info"),
CommandDef("sethome", "Set this chat as the home channel", "Session",

View file

@ -282,6 +282,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "Kon nie tuiste-kanaal stoor nie: {error}"
success: "✅ Tuiste-kanaal gestel op **{name}** (ID: {chat_id}).\nKron-take en kruisplatform-boodskappe sal hier afgelewer word."
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Hermes Gateway Status**"
@ -299,7 +317,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**Kumulatiewe API-tokens (elke oproep weer gestuur):** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**Agent loop:** {state}"
state_yes: "Ja ⚡"
state_no: "Nee"

View file

@ -282,6 +282,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "Home-Kanal konnte nicht gespeichert werden: {error}"
success: "✅ Home-Kanal auf **{name}** (ID: {chat_id}) gesetzt.\nCron-Jobs und plattformübergreifende Nachrichten werden hierher geliefert."
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Hermes-Gateway-Status**"
@ -299,7 +317,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Modell:** `{model}` ({provider})"
context: "**Kontext:** {used} / {total} ({pct}%)"
context_used: "**Kontext:** ~{used} Tokens"
tokens: "**Kumulierte API-Tokens (bei jedem Aufruf erneut gesendet):** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**Agent läuft:** {state}"
state_yes: "Ja ⚡"
state_no: "Nein"

View file

@ -295,6 +295,25 @@ gateway:
save_failed: "Failed to save home channel: {error}"
success: "✅ Home channel set to **{name}** (ID: {chat_id}).\nCron jobs and cross-platform messages will be delivered here."
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Hermes Gateway Status**"
matrix_scope_header: "**Matrix scope:**"
@ -311,7 +330,7 @@ gateway:
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**Cumulative API tokens (re-sent each call):** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**Agent Running:** {state}"
state_yes: "Yes ⚡"
state_no: "No"

View file

@ -279,6 +279,24 @@ gateway:
set_home:
save_failed: "No se pudo guardar el canal principal: {error}"
success: "✅ Canal principal establecido en **{name}** (ID: {chat_id}).\nLas tareas cron y los mensajes entre plataformas se entregarán aquí."
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Estado de Hermes Gateway**"
@ -296,7 +314,7 @@ gateway:
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**Tokens de API acumulados (reenviados en cada llamada):** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**Agente activo:** {state}"
state_yes: "Sí ⚡"
state_no: "No"

View file

@ -282,6 +282,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "Impossible d'enregistrer le canal principal : {error}"
success: "✅ Canal principal défini sur **{name}** (ID : {chat_id}).\nLes tâches cron et les messages multi-plateformes seront livrés ici."
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **État de Hermes Gateway**"
@ -299,7 +317,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**Jetons :** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**Agent en cours :** {state}"
state_yes: "Oui ⚡"
state_no: "Non"

View file

@ -286,6 +286,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "Theip ar shábháil chainéil bhaile: {error}"
success: "✅ Cainéal baile socraithe go **{name}** (ID: {chat_id}).\nSeachadfar tascanna cron agus teachtaireachtaí trasardáin anseo."
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Stádas Hermes Gateway**"
@ -303,7 +321,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**Comharthaí:** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**Gníomhaire ag rith:** {state}"
state_yes: "Tá ⚡"
state_no: "Níl"

View file

@ -282,6 +282,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "Nem sikerült menteni a kezdőcsatornát: {error}"
success: "✅ Kezdőcsatorna beállítva: **{name}** (ID: {chat_id}).\nA cron-feladatok és a platformok közötti üzenetek ide érkeznek."
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Hermes Gateway állapot**"
@ -299,7 +317,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**Tokenek:** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**Ügynök fut:** {state}"
state_yes: "Igen ⚡"
state_no: "Nem"

View file

@ -282,6 +282,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "Salvataggio del canale home non riuscito: {error}"
success: "✅ Canale home impostato su **{name}** (ID: {chat_id}).\nI cron job e i messaggi cross-platform verranno consegnati qui."
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Stato del Gateway Hermes**"
@ -299,7 +317,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**Token:** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**Agente in esecuzione:** {state}"
state_yes: "Sì ⚡"
state_no: "No"

View file

@ -282,6 +282,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "ホームチャンネルを保存できませんでした: {error}"
success: "✅ ホームチャンネルを **{name}** (ID: {chat_id}) に設定しました。\nCron ジョブとプラットフォーム間メッセージはここに配信されます。"
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Hermes ゲートウェイ状態**"
@ -299,7 +317,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**トークン:** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**エージェント実行中:** {state}"
state_yes: "はい ⚡"
state_no: "いいえ"

View file

@ -282,6 +282,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "홈 채널 저장에 실패했습니다: {error}"
success: "✅ 홈 채널이 **{name}**(ID: {chat_id})(으)로 설정되었습니다.\n크론 작업과 플랫폼 간 메시지가 여기로 전달됩니다."
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Hermes 게이트웨이 상태**"
@ -299,7 +317,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**토큰:** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**에이전트 실행 중:** {state}"
state_yes: "예 ⚡"
state_no: "아니오"

View file

@ -282,6 +282,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "Falha ao guardar o canal principal: {error}"
success: "✅ Canal principal definido como **{name}** (ID: {chat_id}).\nAs tarefas cron e mensagens entre plataformas serão entregues aqui."
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Estado do Hermes Gateway**"
@ -299,7 +317,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**Tokens de API cumulativos (reenviados a cada chamada):** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**Agente em execução:** {state}"
state_yes: "Sim ⚡"
state_no: "Não"

View file

@ -282,6 +282,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "Не удалось сохранить главный канал: {error}"
success: "✅ Главный канал установлен на **{name}** (ID: {chat_id}).\nCron-задачи и межплатформенные сообщения будут доставляться сюда."
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Состояние Hermes Gateway**"
@ -299,7 +317,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**Токены:** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**Агент активен:** {state}"
state_yes: "Да ⚡"
state_no: "Нет"

View file

@ -282,6 +282,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "Ana kanal kaydedilemedi: {error}"
success: "✅ Ana kanal **{name}** (ID: {chat_id}) olarak ayarlandı.\nCron işleri ve platformlar arası mesajlar buraya iletilecek."
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Hermes Gateway Durumu**"
@ -299,7 +317,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**Token:** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**Aracı çalışıyor:** {state}"
state_yes: "Evet ⚡"
state_no: "Hayır"

View file

@ -282,6 +282,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "Не вдалося зберегти головний канал: {error}"
success: "✅ Головний канал встановлено на **{name}** (ID: {chat_id}).\nCron-завдання та міжплатформні повідомлення доставлятимуться сюди."
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Стан Hermes Gateway**"
@ -299,7 +317,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**Токени:** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**Агент активний:** {state}"
state_yes: "Так ⚡"
state_no: "Ні"

View file

@ -282,6 +282,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "無法儲存主頻道:{error}"
success: "✅ 主頻道已設定為 **{name}**ID{chat_id})。\n排程任務和跨平台訊息將傳送至此處。"
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Hermes 閘道狀態**"
@ -299,7 +317,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**Token 數:** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**代理執行中:** {state}"
state_yes: "是 ⚡"
state_no: "否"

View file

@ -282,6 +282,24 @@ Future messages in this room will use that transcript until `/reset` or another
set_home:
save_failed: "无法保存主频道:{error}"
success: "✅ 主频道已设置为 **{name}**ID{chat_id})。\n定时任务和跨平台消息将发送到此处。"
context:
header: "🧠 **Context Window**"
model: "Model: `{model}`"
window: "Window: {total} tokens"
in_use: "In use: {used} / {total} ({pct}%)"
bar: "{bar}"
headroom: "Headroom to limit: {headroom} tokens"
threshold: "Auto-compresses at: {threshold} ({threshold_pct}%) — {to_go} to go"
over_threshold: "⚠️ **Over auto-compression threshold ({threshold}, {threshold_pct}%)**"
compressions: "Compressions this session: {count}"
last_savings: "Last compression freed: {savings}% of context"
totals_header: "Session totals (cumulative across {calls} API calls)"
totals_line: "Input {input} · Output {output} · Reasoning {reasoning}"
total_billed: "Total billed: {total}"
throughput_note: "_Throughput, not context size — each call re-sends the window above._"
estimated: "Estimated context: ~{count} tokens across {messages} messages"
detail_after_first: "_(Full compression and throughput stats available after the first agent response)_"
no_data: "No context data available yet. Send a message to start a session."
status:
header: "📊 **Hermes 网关状态**"
@ -299,7 +317,7 @@ Future messages in this room will use that transcript until `/reset` or another
model_provider: "**Model:** `{model}` ({provider})"
context: "**Context:** {used} / {total} ({pct}%)"
context_used: "**Context:** ~{used} tokens"
tokens: "**Token 数:** {tokens}"
tokens: "**Lifetime tokens billed:** {tokens} _(not your current context size; use `/context`)_"
agent_running: "**代理运行中:** {state}"
state_yes: "是 ⚡"
state_no: "否"

View file

@ -100,7 +100,7 @@ async def test_status_command_reports_running_agent_without_interrupt(monkeypatc
result = await runner._handle_message(_make_event("/status"))
assert "**Session ID:** `sess-1`" in result
assert "**Cumulative API tokens (re-sent each call):** 321" in result
assert "**Lifetime tokens billed:** 321" in result
assert "**Agent Running:** Yes ⚡" in result
assert "**Title:**" not in result
running_agent.interrupt.assert_not_called()
@ -153,7 +153,7 @@ async def test_status_command_reads_token_totals_from_session_db():
result = await runner._handle_message(_make_event("/status"))
# 1000 + 250 + 500 + 100 + 50 = 1,900
assert "**Cumulative API tokens (re-sent each call):** 1,900" in result
assert "**Lifetime tokens billed:** 1,900" in result
@pytest.mark.asyncio
@ -174,7 +174,7 @@ async def test_status_command_tokens_zero_when_session_db_row_missing():
result = await runner._handle_message(_make_event("/status"))
assert "**Cumulative API tokens (re-sent each call):** 0" in result
assert "**Lifetime tokens billed:** 0" in result
@pytest.mark.asyncio
@ -212,8 +212,7 @@ async def test_status_command_includes_live_agent_model_and_context():
assert "**Model:** `openai/gpt-test` (openai)" in result
assert "**Context:** 12,345 / 100,000 (12%)" in result
assert "**Cumulative API tokens (re-sent each call):** 1,250" in result
assert "1,250 (cumulative)" not in result
assert "**Lifetime tokens billed:** 1,250" in result
@pytest.mark.asyncio
@ -245,7 +244,7 @@ async def test_status_command_includes_persisted_model_and_context_when_agent_no
assert "**Model:** `openai/gpt-persisted` (openai-codex)" in result
assert "**Context:** 24,000 / 272,000 (9%)" in result
assert "**Cumulative API tokens (re-sent each call):** 2,500" in result
assert "**Lifetime tokens billed:** 2,500" in result
@pytest.mark.asyncio
@ -821,3 +820,152 @@ async def test_post_delivery_callback_generation_snapshot_happens_after_bind():
assert fired == []
assert session_key in adapter._post_delivery_callbacks
assert adapter._post_delivery_callbacks[session_key][0] == 2
# ── /context command tests ────────────────────────────────────────────────
def _stub_agent(**overrides) -> SimpleNamespace:
"""Build a stub agent with the attributes _handle_context_command reads."""
props = dict(
model="openai/gpt-test",
context_compressor=SimpleNamespace(
last_prompt_tokens=47_231,
context_length=200_000,
threshold_tokens=100_000,
threshold_percent=0.5,
compression_count=2,
_last_compression_savings_pct=63.0,
),
session_api_calls=47,
session_input_tokens=410_000,
session_output_tokens=38_000,
session_reasoning_tokens=12_000,
session_total_tokens=3_158_641,
session_cache_read_tokens=2_900_000,
session_cache_write_tokens=48_000,
)
props.update(overrides)
return SimpleNamespace(**props)
@pytest.mark.asyncio
async def test_context_command_live_agent():
"""/context with a live running agent shows the full view: gauge,
compression, and throughput but NOT cache stats."""
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 = _make_runner(session_entry)
session_key = session_entry.session_key
agent = _stub_agent()
runner._running_agents[session_key] = agent
result = await runner._handle_context_command(_make_event("/context"))
assert "🧠 **Context Window**" in result
assert "Model: `openai/gpt-test`" in result
assert "Window: 200,000 tokens" in result
assert "In use: 47,231 / 200,000 (24%)" in result
assert "Headroom to limit: 152,769 tokens" in result
# Compression section
assert "Auto-compresses at: 100,000 (50%)" in result
assert "Compressions this session: 2" in result
assert "Last compression freed: 63% of context" in result
# Throughput — NOT cache
assert "Session totals (cumulative across 47 API calls)" in result
assert "Input 410,000" in result
assert "Output 38,000" in result
assert "Reasoning 12,000" in result
assert "Total billed: 3,158,641" in result
assert "each call re-sends the window above" in result
# Cache stats must NOT appear (removed per design)
assert "Cache read" not in result
assert "Cache write" not in result
assert "Cache hit" not in result
assert "Hit rate" not in result
@pytest.mark.asyncio
async def test_context_command_over_threshold():
"""When used >= threshold, the over-threshold warning is shown."""
session_entry = SessionEntry(
session_key=build_session_key(_make_source()),
session_id="sess-2",
created_at=datetime.now(),
updated_at=datetime.now(),
platform=Platform.TELEGRAM,
chat_type="dm",
)
runner = _make_runner(session_entry)
session_key = session_entry.session_key
agent = _stub_agent(
context_compressor=SimpleNamespace(
last_prompt_tokens=150_000,
context_length=200_000,
threshold_tokens=100_000,
threshold_percent=0.5,
compression_count=5,
_last_compression_savings_pct=40.0,
)
)
runner._running_agents[session_key] = agent
result = await runner._handle_context_command(_make_event("/context"))
assert "⚠️" in result
assert "Over auto-compression threshold" in result
@pytest.mark.asyncio
async def test_context_command_no_agent_transcript_fallback():
"""When no agent is resident and session_entry has no last_prompt_tokens,
/context falls back to a transcript estimate."""
session_entry = SessionEntry(
session_key=build_session_key(_make_source()),
session_id="sess-3",
created_at=datetime.now(),
updated_at=datetime.now(),
platform=Platform.TELEGRAM,
chat_type="dm",
last_prompt_tokens=0, # No live context data
)
runner = _make_runner(session_entry)
# Stub the transcript so estimate_messages_tokens_rough has something to work with
runner.session_store.load_transcript.return_value = [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there!"},
{"role": "user", "content": "What's my balance?"},
{"role": "assistant", "content": "Your balance is $1,000."},
]
result = await runner._handle_context_command(_make_event("/context"))
assert "🧠 **Context Window**" in result
assert "Estimated context:" in result
assert "4 messages" in result
@pytest.mark.asyncio
async def test_context_command_no_data():
"""When there's no agent, no session data, and no transcript,
/context returns the no-data message."""
session_entry = SessionEntry(
session_key=build_session_key(_make_source()),
session_id="sess-4",
created_at=datetime.now(),
updated_at=datetime.now(),
platform=Platform.TELEGRAM,
chat_type="dm",
last_prompt_tokens=0,
)
runner = _make_runner(session_entry)
runner.session_store.load_transcript.return_value = []
result = await runner._handle_context_command(_make_event("/context"))
assert "No context data available yet" in result