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.
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.pyThen 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 --checkTo remove everything:
python install.py --uninstallYour session exports under <hermes home>/handoffs/ are left alone — they are
your data. config.yaml is edited through hermes config set, never hand-written.
| 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.
| 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 |
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.
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.
| 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.
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.
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.
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.
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:
- The window comes from
context_length_cache.yaml— what Hermes measured by probing your endpoint. - The compaction threshold is recomputed from your
config.yamlwith Hermes' own arithmetic, including both floors. - Token counts prefer Hermes' own estimator when importable, and prefer the
provider's reported
prompt_tokensover 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 Pythonstest_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.
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)
- 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-kickoffkeeps 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.
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.
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.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.
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.