Skip to content

Repository files navigation

hermes-agent-session-handoff

tests license: MIT python: 3.8+ Hermes Agent

简体中文 · 日本語 · Español · فارسی

A long-session handoff for Hermes Agent: when a chat approaches the token limit that ends it, this measures how close it really is, exports the whole session to disk, and continues in a fresh chat that reads the parent transcript in full before doing anything else.

Hermes compacts a full session in place — your conversation is replaced by a summary and work continues in a diminished chat. You usually notice because the model starts re-asking things you settled two hours ago. This moves the work before that happens, and it recovers what earlier compactions already took out.

Two Python scripts, one plugin, no dependencies.


Install

Requires Python 3.8+ and Hermes Agent. Nothing else.

git clone https://github.com/IRDcode/hermes-agent-session-handoff
cd hermes-agent-session-handoff
python install.py --dry-run     # see every change first
python install.py

Then restart Hermes — plugins are discovered once per process, so the watcher is not active in a chat that is already open. Check what it measures:

python install.py --check

To remove everything:

python install.py --uninstall

Your session exports under <hermes home>/handoffs/ are left alone — they are your data. config.yaml is edited through hermes config set, never hand-written.

What gets installed

path what it is
<hermes home>/skills/productivity/long-session-handoff/ the procedure the model follows, plus three scripts
<hermes home>/plugins/session-weight-watch/ the detector — one pre_llm_call hook
<hermes home>/runtime/session-weight-watch.json anti-nag state and the learned token overhead
<hermes home>/handoffs/ the exports, and chains.json linking parent to child

config.yaml gains one line: session-weight-watch in plugins.enabled. Nothing else is touched — in particular, your compression settings are not changed. This hands a session off before the limit is reached; it does not change what happens when the limit is reached.

How it differs from default behaviour

plain Hermes with this installed
when the session fills compaction fires; the conversation is replaced by a summary you are offered a handoff first, well before that point
what the next chat knows whatever the summary captured, written by the agent that was already losing track the parent's own transcript, read in full and verified by counts
context already dropped archived in state.db, but nothing reads it back recovered into 05-dropped-context.md
how full is it, really? not surfaced per-chat session_weight.py prints tokens against the exact wall
finding the continuation later scroll the session list chains.json records parent, child, weight at migration, and whether the read was verified

The measurement

python "$HERMES_HOME/skills/productivity/long-session-handoff/scripts/session_weight.py"
WINDOW
  context window   1,000,000   (source context_length_cache.yaml)
  threshold        0.50 configured -> 0.50 effective
  compression      enabled

WALL -- the token count past which this session stops growing
  500,000 tokens, then Hermes COMPACTS IN PLACE:
  the conversation is summarised, and the replaced turns stay on
  disk (active=0, compacted=1) -- searchable and recoverable.

NOW
  live rows        251   (archived on disk: 1)
  context          227,037 / 500,000  [#####.....] 45%
  token source     provider (usage delta)
  tool calls       137      api calls 115
  active time      1.6h   (elapsed span 1.8h, idle included)

VERDICT  STAY HERE  (below gate (45% < 62%))

Every input is named, so a wrong number is visible rather than silent. Verified against Hermes' own log: window 1,000,000 at threshold 0.50 gives 500,000, and Hermes logged ~501,079 tokens >= 500,000 threshold when it fired.

Two floors that surprise people

compaction fires at   window x threshold   (floored at 64,000 tokens)
provider refuses at   window - reserved output      (compression disabled)

A window under 512,000 compacts at 75%, not at your configured 50%. So a 200,000 window compacts at 150,000 — and a 64,000 window triggers at 85%, because the 64,000 floor would otherwise equal the whole window and never be reachable. session_weight.py mirrors both, and --json gives you every input.

When it triggers

signal threshold
context vs the wall ≥ 62% counts, ≥ 85% hand off, ≥ 95% act without asking twice
live rows (active = 1) > 400
tool calls ≥ 250
active working time > 4 h

Nothing is offered below 62% of the wall, whatever else tripped. Rows, tool calls and hours are proxies for context pressure, invented for tools that could not measure context directly. Here it can be measured, so they only sharpen urgency once context itself says moving is due.

That gate is not a guess. Re-scoring 21 real firings of the pre-gate detector, 6 sat below 40% of the wall. The worst had 1,685 rows and 5.2 active hours while its live context was 157,759 of a 500,000 wall — 32%. Migrating there abandons ~340,000 tokens of paid-for headroom to save nothing.

The offer appears at most once per escalation band — 200 more live rows, or another tenth of the wall — with a 15-minute floor, and never inside a subagent, a cron run, or a delegated child.

Three measurement bugs this tool shipped and fixed

Stated plainly, because a tool that measures things should be honest about having been wrong.

1. Counting rows instead of context. Hermes archives rather than deletes: compacted turns stay in state.db with active = 0, compacted = 1. Measured on real sessions, total rows ran 2 to 4 times the live set (worst case 3,389 on disk against 832 live). Scoring the total is what produced every false alarm. Only active = 1 is context.

2. Scoring elapsed time as working time. A chat left open overnight reported a 15.98 hour span of which one gap was 14.43 hours of sleep — 1.55 hours of actual work. Active time is the sum of gaps under 10 minutes.

3. Counting compaction markers by substring. content LIKE '%CONTEXT COMPACTION%' matches the agent's own tool output the moment anyone greps for it. Measured in one database: 35 tool rows contain that string purely because a session read a file quoting it, and one session scored 3 compactions having never been compacted once. Compaction is now counted structurally, from archived rows.

And one that is not fully solvable: the chars/4 estimate is about 28% low on its own, because tool schemas ride every request and appear in neither the database nor the message list. Measured live, the provider reported 297,239 prompt tokens where the estimate gave 214,715 (0.72x). The fix is to difference sessions.input_tokens / api_call_count between two turns — that is the provider's own count, exact — and learn the per-session overhead. It needs two turns to learn; the first turn is deliberately conservative.

The handoff itself

measure  ->  ask  ->  export  ->  create the continuation  ->  it reads the parent

One command does all of it:

python "$SKILL_DIR/scripts/handoff.py" <parent_session_id> \
    --brief ".hermes/handoff/my-project-continuation-2.md" \
    --task "the next open item, stated concretely"

The export writes five files: every user message verbatim and deduped, every substantial assistant message, the full transcript with tool payloads clipped, an index of counts, and — when the session was ever compacted — the turns that compaction removed, recovered from disk.

The continuation is then created and seeded headlessly: it reads the export and must reply with counts that match the index. If they do not match, the read was partial and the handoff is not done. That read happens in a session nobody is waiting on, so the expensive part of a migration costs you no wall-clock time.

Then the child is named after its parent, the parent's model, provider, endpoint and iteration budget are pinned onto its row, and the lineage is recorded. With --task, it continues the work itself.

--dry-run prints every step and changes nothing.

Four Hermes behaviours worth knowing even if you install nothing

Each of these was measured, not assumed.

hermes sessions rename strips the ZERO WIDTH NON-JOINER. The title sanitiser removes U+200B–U+200F, which includes ZWNJ — a letter-level character in Persian, Arabic and Urdu. Renaming to تست‌های انتقال stores تستهای انتقال: a spelling error in your own project name. Writing sessions.title directly preserves it.

-m on hermes chat does not persist. The flag applies to that process; the stored sessions.model keeps whatever the config default resolved to. Pin it by writing the row, then read it back.

--max-turns N freezes max_iterations = N on the session row. 25 is right for "read and report" and then permanently starves the real work. Rewrite it after seeding.

parent_session_id hides a session from the list. That column marks implementation-detail children (subagent runs, compression continuations), and Hermes' listing query filters them out: 51 of 52 rows carrying it were invisible to hermes sessions list. Handoff lineage therefore lives in model_config._handoff_from and in chains.json, not in that column.

Compatibility

Hermes versions. Nothing here patches or wraps Hermes. It reads the documented pre_llm_call hook contract, the session store's own schema, and the window that Hermes itself discovered and cached. Where an exact number is needed, it is taken from evidence rather than asserted:

  1. The window comes from context_length_cache.yaml — what Hermes measured by probing your endpoint.
  2. The compaction threshold is recomputed from your config.yaml with Hermes' own arithmetic, including both floors.
  3. Token counts prefer Hermes' own estimator when importable, and prefer the provider's reported prompt_tokens over any estimate.

--explain output names every source, so drift is visible instead of silent. If Hermes moves the arithmetic, the reported window source tells you which layer answered.

Operating systems. Pure Python, no dependencies, no compiled parts. Paths go through pathlib, HERMES_HOME and --profile are honoured everywhere, sqlite URIs are percent-encoded (a home directory containing # silently breaks the naive form), and every entry point forces UTF-8 on its own streams so a legacy Windows console cannot turn a successful export into a traceback.

Verify it on your own machine:

python tests/test_session_weight.py    # arithmetic, the gate, the three traps
python tests/test_handoff.py           # export, naming, plugin hook, chain log
python tests/test_compat.py            # syntax floor, entry points, other Pythons

test_compat.py finds every other Python installed on your machine and re-runs the suites under each, so a version difference shows up as a failure rather than a surprise later.

How it all fits together is in docs/HOW-IT-WORKS.md; if something looks wrong, start with docs/TROUBLESHOOTING.md; the contribution rules are in CONTRIBUTING.md.

Works with weak models too

The procedure does not assume a frontier model. Everything that has to be exact — measuring the wall, exporting the transcript, naming the continuation, pinning the model, recording the chain — is done by scripts with printed output, not by the model's judgement. The model's job is to ask permission, read the export, and report the counts.

Continuation names follow the language of the parent's title, so a chain stays readable in its own script:

Refactor auth          ->  Refactor auth (cont. 2)
ربات ثبت‌نام            ->  ربات ثبت‌نام — ادامه ۲
تحليل البيانات          ->  تحليل البيانات (الجزء ٢)
重构登录流程              ->  重构登录流程(续 2)
ログイン改修              ->  ログイン改修(続き 2)

Limitations

  • The compaction arithmetic mirrors Hermes 0.20.x and is verified against its own log output. A future release could move it; the reported window source is how you would notice.
  • The provider-anchored token count needs two turns in a session to learn its overhead. Before that, the estimate is ~28% low and the tool errs toward staying put.
  • The token estimate treats images at a flat per-image cost, matching Hermes.
  • A handoff starts a new session, so its prompt cache starts cold. For a session near the wall that is a good trade; it is still a cost.
  • The seeding read costs one headless turn of your own model. --no-kickoff keeps it to exactly that one turn.
  • Tested on Windows with Python 3.11 and 3.14. CI runs all three suites on Linux, macOS and Windows (Python 3.8, 3.11, 3.13), plus a real install/uninstall cycle on Linux and Windows.

Also for Claude Code

The same idea, built first for Claude Code, lives at claude-code-session-handoff. It additionally diagnoses a configuration trap in which autoCompactWindow is silently clamped to the model ceiling, so a session configured for a million tokens gets compacted at 167,000.

The Hermes version here is the better one. Hermes archives compacted turns instead of discarding them, so this port can recover context that was already lost (05-dropped-context.md), read the provider's own token counts out of the session store, and fire from a real plugin hook rather than a shell hook.

Contributing

Bug reports welcome, especially "the numbers were wrong on my setup" — include the output of session_weight.py --session-id <id> --json. If a Hermes release moves the arithmetic, that is the report that fixes it fastest.

Before opening a PR, run all three suites — the full checklist is in CONTRIBUTING.md.

Security

SECURITY.md documents exactly what this reads, what it writes, and what it sends over the network (nothing). Worth a look before installing anything that touches your session files.

License and credit

MIT — see LICENSE. Free to use, modify, and redistribute, including commercially. The one condition is that the copyright notice and licence text travel with it, so a fork still says where it came from.

If you use the approach or the findings, a link back is appreciated. CITATION.cff is there so GitHub's "Cite this repository" button produces something correct.

Authored by IRDkiya. Not affiliated with Nous Research; Hermes Agent is their project.

About

Long-session handoff for Hermes Agent: measure how close a chat is to the token limit that ends it, export the whole session, and continue in a fresh chat that reads the parent transcript in full. Recovers context that compaction already removed.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages