Skip to main content

hermes-auto-titler

❖ Communityv0.3.0★ 4

Topic-aware session title tracking for Hermes Agent. Hermes generates the first title by default; AutoTitler follows conversation changes and session close without changing the host title-generation switch.

Open in Hermes Desktop
hermes plugins install hermes-auto-titler

What it adds

Hooks 3

pre_llm_callon_session_endon_session_finalize

README

From the reviewed commit 42259f5 ↗; it updates when the author re-pins.

License: Apache-2.0Hermes AgentPython

Topic-aware session title tracking for Hermes Agent, with provenance-safe writes and long-horizon intent tracking.

Features · How it works · Install · Configuration · Commands · 简体中文


Hermes names a session from its opening exchange, but conversations evolve and titles don't. A chat that starts with "how do i fix the thing with…" can turn into an urgent Redis memory leak investigation and production deploy while the sidebar label stays anchored to the first message.

hermes-auto-titler maintains session labels across their full lifecycle. It periodically evaluates whether an auto-generated title still reflects the conversation's trajectory, updating the label as topics evolve while protecting user-authored titles.

┌─ SESSIONS ────────────────────────────────────────────────┐
│  ✦ Redis worker leak                          [AUTO]      │
│    was: how do i fix the thi…                             │
│  ✓ vLLM quant benchmarks                      [KEPT]      │
│    newest: "ok thanks!"                                   │
│  🔒 my research notes                         [USER]      │
│    never overwritten                                      │
│                                                           │
│  derived < llm < user                                     │
└───────────────────────────────────────────────────────────┘

✨ Features

Provenance-safe Enforces derived < llm < user. derived = Hermes' deterministic fallback from the first message; llm = model-generated; user = yours, never overwritten. Legacy titles without recorded provenance are protected.
Long-horizon intent tracking Presents indexed Opening, Sampled history, and Recent turns so the title model can see where evidence sits in the chat. Opening/Recent keep the final cleaned assistant reply for subject clarification. Compaction summaries act as fallback when raw history is unavailable and as secondary evidence when sparse sampling omits recovered turns.
Intent-aware capture Filters compaction handoffs, system noise, adjacent replay duplicates, and internal cron/subagent/background turns before they reach the title model.
Evidence-first judgment Infers the durable subject from conversation evidence before comparing against current or proposed titles. Explicit, repeated user goals carry highest weight; assistant responses provide context but cannot introduce new topics on their own.
First-turn takeover or coexistence first_title_mode: builtin (default) lets Hermes own initial titles; the plugin maintains multi-turn evolution without modifying host configuration. Set to plugin to take over evaluation from turn 1.
Conservative / aggressive policy conservative requires clear, durable mismatch before renaming. aggressive responds faster when the user explicitly abandons a goal or sustains a new direction, while still treating one-off subtasks and tool changes as noise.
N-round review gate rename_confirmations: 1 (default): a candidate title needs one follow-up endorsement before writeback. 0 for immediate updates; N for N consecutive endorsements, resetting if the candidate changes.
Optional rename cap max_renames_per_session: 0 (off). Set N to allow at most N automatic replacements per session. Initial naming and manual rename-now are exempt.
Custom prompt slot custom_instructions appends user-defined text to the system prompt — language preferences, prefix conventions, etc. Empty by default.
Call-efficient Turn-cadence gating, per-session throttling, provenance checks, in-flight dedup, and internal-turn filtering keep most foreground turns from triggering model calls.
Conservative surface cleanup Preserves literal identifiers (commands, paths, code symbols), adds CJK–Latin spacing, and only adjusts casing with direct conversation evidence.
Profile-isolated and auditable SessionDB connections are per-profile; all title calls log under task=hermes_auto_titler in usage metrics.

⚠️ Privacy / data flow — read before enabling The plugin sends selected conversation excerpts to the title model: indexed Opening turns, evenly spaced Sampled history turns, Recent turns, and attachment filename placeholders. Opening/Recent may include the final cleaned assistant reply for each selected user turn. Compaction summaries are fallback evidence when raw pre-compaction history is unavailable, and may remain as secondary compressed evidence when recovered raw history is sampled sparsely. Sampling limits are governed by opening_turns, recent_turns, preview_chars, include_all_user_messages, user_message_threshold, user_message_preview_chars, summary_preview_chars, and retitle_summary_chars. Model requests count toward your provider's usage and billing.

🔍 How it works

  1. First-title ownership — With first_title_mode: builtin (default), Hermes owns initial title generation; the plugin maintains multi-turn evolution without modifying host configuration. Set to plugin to evaluate from turn 1.
  2. Cadence and close handling — Hooks into on_session_end and on_session_finalize. Only completed foreground turns count toward every_n_turns (default 2); failed, interrupted, cron, subagent, and background turns are excluded. Periodic evaluations run asynchronously. Close hooks never start network work: they wait at most 100 ms for an existing worker, then atomically persist a typed finalize intent for the next normal process lifetime.
  3. Context construction — Builds three non-overlapping evidence regions with real positions such as User turn 17 of 43: Opening contains the initial user turns plus each turn's final cleaned assistant reply, Sampled history takes evenly spaced user turns from the middle, and Recent contains the latest user turns plus their final assistant replies. Raw compacted=1 history is preferred; a compaction summary is fallback evidence when raw history is unavailable and secondary evidence when sparse sampling omits recovered turns.
  4. Evidence-first evaluation — The auxiliary model outputs structured JSON. Conversation evidence appears before the current title to reduce anchoring bias. Explicit and repeated user intent carries the highest weight; assistant responses support but cannot introduce new topics. Structural overlap between sampled sections is discounted.
  5. Strategy — conservative keeps the existing title unless a significant, durable topic shift has occurred. aggressive adapts faster when the user explicitly abandons an earlier objective or sustains a new direction, but recent turns alone are still insufficient to rename.
  6. Multi-round confirmation — Under rename_confirmations: N, proposed llm → llm renames are held as pending until confirmed across N subsequent evaluations. A different candidate resets the counter. Initial titling, derived upgrades, and manual rename-now bypass this gate.
  7. Safe writeback — Provenance is re-validated immediately before writing. Pending candidates are invalidated if the underlying title changes. Duplicate-title suffixes respect width constraints, and writes carry llm provenance so user-set titles are never overwritten.
  • Why continuous maintenance instead of better first-message titling? An opening exchange cannot anticipate where a conversation leads. Long sessions need titles that evolve with the user's actual objectives.
  • Why Sampled history instead of full transcripts? The title model needs temporal coverage without every tool/action detail. Opening and Recent retain their distinct roles, while Sampled history jumps evenly through the middle of the user-turn timeline. Every selected long user message preserves both its beginning and end.
  • Why infer the subject before inspecting current titles? Existing titles work as comparison baselines but not as evidence. Analyzing conversation evidence first avoids anchoring on outdated labels.
  • Why queue on session close instead of calling the model? Hermes gives finalization a bounded shutdown window. Close hooks therefore make no network call, wait at most 100 ms for already-running work, and durably queue an epoch-tagged finalize intent. The retry worker processes it during a normal lifecycle without bypassing provenance or review gates.
  • Why require one review endorsement by default? A single rename trigger can reflect a temporary detour. Requiring one subsequent endorsement provides a defense against title flutter; set rename_confirmations: 0 for immediate updates, or increase N for stricter stability.
  • Known limitation: Hermes does not provide an atomic compare-and-swap API for session titles, leaving a narrow race window during concurrent writes. The plugin mitigates and detects these collisions.

📦 Install

Recommended — via the Hermes plugin installer:

hermes plugins install https://github.com/Vocllum/hermes-auto-titler --enable

This clones the repo into ~/.hermes/plugins/, generates config.yaml from the bundled template, and enables the plugin.

Manual alternative:

cd ~/.hermes/plugins
git clone https://github.com/Vocllum/hermes-auto-titler
cp hermes-auto-titler/config.yaml.example hermes-auto-titler/config.yaml

By default, the plugin uses Hermes' built-in title_generation auxiliary task, so the host owns the provider/model choice. To use a plugin-local route instead, set provider and/or model in the plugin config. The host must authorize the selected mode:

# ~/.hermes/config.yaml
plugins:
  enabled:
    - hermes-auto-titler
  entries:
    hermes-auto-titler:
      llm:
        allow_provider_override: true
        allow_model_override: true
        allow_task_override: true

Restart Hermes, then verify:

/autotitler status

Which model does it use?

Hermes auxiliary mode (default). Leave provider and model empty. The plugin calls Hermes' title_generation task, and Hermes resolves auxiliary.title_generation.provider / model.

Plugin custom mode (optional). Set provider and/or model in ~/.hermes/plugins/hermes-auto-titler/config.yaml:

provider: "your-provider"   # provider already configured in Hermes
model: "your-model"         # any model your Hermes setup can reach
  • Hermes auxiliary mode needs allow_task_override: true so the plugin can borrow the title_generation task.
  • Plugin custom mode needs allow_provider_override: true / allow_model_override: true. The plugin never stores a separate API key.
  • This channel is only for title evaluation; your main conversation keeps its own model.
  • /autotitler status shows the configured route. When it says (host default), check Hermes' auxiliary.title_generation config for the actual provider/model.

Model guidance. The plugin runs well on small, fast instruction-following models. Prioritize consistent JSON output, multilingual comprehension, and prompt adherence over heavy reasoning. Start with Hermes' normal auxiliary title route. Before deploying an ultra-light model, test scripts/review_sample.py against your own session history — if you see topic drift, invalid JSON, or degraded multilingual titles on long or compacted sessions, switch to a more capable model.

Requirements: Python ≥ 3.11 · a recent Hermes Agent.

Title ownership note: first_title_mode is a startup-level setting (default builtin): Hermes owns the first-turn title and the plugin starts from its multi-turn evolution. Set first_title_mode: plugin if you want the plugin to evaluate from turn 1. Neither mode ever writes host configuration, so disabling or deleting the plugin leaves Hermes' own title generator fully intact.

First-title diagnosis: /autotitler status inspects the host's auxiliary.title_generation.enabled setting. If first_title_mode: builtin but the host title generator is disabled (auxiliary.title_generation.enabled=false), /autotitler status reports a first-title mismatch with clear fix options:

  • Enable Hermes first-title: hermes config set auxiliary.title_generation.enabled true (requires Hermes restart).
  • Let the plugin handle first titles: /autotitler config first_title_mode plugin.

If host configuration cannot be read, /autotitler status displays unknown rather than falsely reporting healthy.

Install-layout note: Place the plugin directly at ~/.hermes/plugins/hermes-auto-titler/. If you symlink the package directory, config.yaml must live where the package physically resides (config resolves relative to the package).

⚙️ Configuration

Settings resolve per key, in this order: ctx.get_config() → host plugins.entries.hermes-auto-titler.settings (Hermes Desktop/CLI Plugins panel, declared by config_schema in plugin.yaml) → ~/.hermes/plugins/hermes-auto-titler/config.yaml → built-in defaults. The local file is optional and only needs the keys you want to override; a legacy 0.2 config.yaml keeps every key it explicitly set. Invalid values fall back to defaults in the YAML entry and are reported as errors by /autotitler config.

Key Default What it does
enabled true Master switch. Enabling after a disabled startup requires a restart (no hooks registered); disabling a loaded plugin takes effect immediately via the hook guard.
every_n_turns 2 Evaluate every N completed foreground turns.
first_title_mode builtin builtin = Hermes owns first-title generation; plugin = the plugin evaluates from turn 1. Neither mode writes host configuration. Changes take effect at plugin load (restart).
on_close true On close/finalize, wait at most 100 ms for existing work and persist a typed finalize intent; never start a network call from the close hook.
recent_turns / opening_turns 2 / 2 Non-overlapping real-user-turn anchors. Opening keeps user text only; Recent also keeps the last assistant reply for each selected turn.
ignore_model_messages false Exclude assistant messages from captured context (A/B testing).
preview_chars 120 Per-message budget for Opening/Recent evidence. Long user messages preserve both head and tail.
include_all_user_messages true Include Sampled history between Opening and Recent. The key name is retained for config compatibility.
user_message_threshold 40 Target user-turn evidence budget. Opening and Recent anchors are always retained, so very small values may be exceeded; remaining slots go to evenly spaced Sampled history. 0 = unlimited middle history.
user_message_preview_chars 300 Per-message Sampled history budget with head+tail extraction. 0 = unlimited.
summary_preview_chars 1200 Fallback compaction-summary budget when raw pre-compaction user history is unavailable.
retitle_summary_chars 12000 Compaction-summary budget for blind/manual/bulk regeneration; 0 falls back to preview_chars.
title_style concise concise = subject label · complete = short event/intent summary (wider display budget).
strategy conservative conservative = rename only on material durable mismatch; aggressive = follow explicit goal abandonment or sustained new direction sooner, ignoring one-off subtasks and tool changes.
provider / title_model "" / "" Both empty = Hermes title_generation auxiliary task; set either for a plugin custom route. title_model is the settings-panel name of the model override (model in config.yaml).
min_interval_minutes 5 Minimum gap between evaluations of one session.
max_title_length / max_display_width null / 40 null avoids hard character slicing; the prompt keeps titles brief and max_display_width caps display columns. complete style adds 12 columns.
rename_confirmations 1 Require one later endorsement before llm→llm writeback. 0 = immediate; N > 1 = N endorsements. Replacing the pending candidate restarts the count.
max_renames_per_session 0 Off by default. N > 0 caps automatic replacements per session. Initial naming and rename-now are exempt; upgrading derived counts as a replacement. Counter is process-local.
custom_instructions "" Text appended to the title-generation system prompt. E.g. "Always use English for titles" or "Prefix every title with [Project]". Empty = no effect.

🕹️ Commands

/autotitler status                                     # current config/state
/autotitler config <key> [value]                       # view/set config (persisted)
/autotitler rename-now [session_id]                    # blind-regenerate; bypasses review gate
/autotitler retitle-all [--dry-run] [--limit N] [--min-messages N]   # bulk blind regeneration

Most options take effect immediately. enabled requires a restart if hooks were not registered at startup; first_title_mode is a startup-level ownership switch.

rename-now performs blind regeneration: the current title is withheld from the model to eliminate anchoring, and the confirmation gate is bypassed. retitle-all --dry-run simulates bulk updates without API calls or data changes. Without --dry-run, user-set titles are preserved and only eligible automatic titles are regenerated.

💰 Cost

Input size per evaluation is bounded, not fixed. Opening and Recent use preview_chars=120 per selected message; the default 40-user-turn evidence budget is split across Opening, evenly spaced Sampled history, and Recent. Sampled history uses up to 300 characters per selected user message. A compaction summary can add up to 1,200 characters only on the fallback path.

The plugin requests max_tokens=64, but some OpenAI-compatible providers may not enforce output limits upstream. During benchmark runs, an unconstrained model produced 597 input / 1,639 output tokens due to internal reasoning before emitting JSON. Budget for your provider's actual token accounting, not a hard 64-token ceiling.

Cadence gating is the primary cost guard: turn-interval checks (every_n_turns), per-session cooldowns (min_interval_minutes), provenance validation, in-flight dedup, and background-turn filtering prevent redundant calls. Every call is logged under task=hermes_auto_titler in Hermes usage metrics.

A lightweight auxiliary model fits the intended cost profile, but verify against your own transcripts — especially long, compacted sessions.

🧪 Development

uv venv .venv && uv pip install --python .venv/bin/python pytest PyYAML
.venv/bin/python -m pytest tests/ -v
<your-hermes-checkout>/venv/bin/python scripts/integration_check.py
<your-hermes-checkout>/venv/bin/python scripts/review_sample.py --n 20
<your-hermes-checkout>/venv/bin/python scripts/review_sample.py --n 20 --strategy aggressive
<your-hermes-checkout>/venv/bin/python scripts/prompt_acceptance.py --n 8 --seed 17 --output /tmp/title-experiment.jsonl
<your-hermes-checkout>/venv/bin/python scripts/e2e_check.py <session_id>

review_sample.py runs in dry-run mode, mirroring the production context config (preview lengths, trajectory limits, summary budgets).

prompt_acceptance.py is an isolated experiment harness: it samples real non-user-titled sessions, replays chronological prefixes (e.g. turns 1, 2, 4, final), evaluates prompt variants on identical prefixes, and optionally scores titles with a separate LLM judge. Defaults to Hermes' PluginLlm bridge via the title_generation task; raw OpenAI-compatible endpoints can be configured via HERMES_AUTOTITLER_EXPERIMENT_* env vars. Strictly read-only against SessionDB — existing titles are hidden during generation and judging.

retitle_all.py --dry-run previews bulk regeneration without model calls or DB writes. Without --dry-run, user-set titles are preserved and only eligible automatic titles are updated.

📄 License

Apache-2.0

← Back to the catalog · catalog built Oct 8, 2026