feat(importers): project Hermes session-level token accounting - #339
Merged
Merged
Conversation
Hermes sessions have always reported zero cost. The importer derived usage
from messages.token_count, but Hermes stopped populating that column when it
moved token accounting onto the sessions row: in a live state.db it is NULL
across all 37,889 messages, so ClassifyUsage returned Unknown and no
session_usage row was ever written.
Read the five counters Hermes keeps per session instead. Two parts of the
mapping are easy to get wrong and both are pinned by tests:
InputTokens is the cache-inclusive sum of input + cache_read + cache_write.
Hermes stores uncached input alone, while prosa's canonical aggregate is the
grand total — the same shape the Claude Code importer builds, and exactly how
Hermes derives its own prompt_tokens.
reasoning_tokens stays out of OutputTokens. Hermes already counts it there,
which its own total_tokens property confirms by not adding it; folding it in
would double count. It survives as provenance only.
A row whose counters are all zero keeps classifying Unknown rather than
ExplicitZero, mirroring Hermes's own has_usage flag. The distinction matters:
the import policy drops ExplicitZero outright, so the literal reading would
have deleted the 17 sessions that carry no counters.
The projected JSONL gains a leading {"type":"session_usage","data":{…}} line
carrying the counters verbatim, following the envelope Grok Build already uses
for non-message rows. Without it the preserved raw could not explain the usage
prosa derives from it. Hermes is already authorized to project rather than
copy as a multi-session container, so this needs no new authorization.
The sessions query now uses the same NULL-placeholder projection the messages
query has, so a Hermes build predating the counters still imports and falls
back to token_count. That replaces the single-column parent_session_id probe,
leaving tableHasColumn unused.
ProjectionVersion 12 to 13 so existing rows re-project and re-push.
Measured against the maintainer's store: 860 of 950 Hermes sessions gain
usage, worth 943,411,653 tokens and $825.89 of previously invisible spend. The
90 sessions that defer to a fuller sibling transcript keep no usage — their
raw is a verbatim copy that cannot carry the counters, and merging the two
sources at projection time is the separate cut docs/sources/hermes.md already
anticipates under "Dual-source gap".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015UmSUq3v73JA3ZJr4nFhm9
The state.db schema block predated every token column. It now shows the five counters and estimated_cost_usd, with a mapping table onto prosa's canonical aggregate and the two rules that make it correct: input_tokens is uncached input alone, so prosa's InputTokens sums all three prompt columns, and reasoning_tokens is already inside output_tokens. Also records why all-zero counters stay Unknown, why prosa ignores Hermes's estimated_cost_usd, that older Hermes builds fall back to token_count, and the new leading session_usage line in the projection. The dual-source gap section now names its visible cost: a session that defers to a fuller sibling transcript gets no usage, because the counters live only on the state.db row and a transcript's raw is a verbatim copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015UmSUq3v73JA3ZJr4nFhm9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Hermes sessions have always reported zero cost. The importer derived usage
from
messages.token_count, but Hermes stopped populating that column when itmoved token accounting onto the
sessionsrow. In a livestate.dbit isNULL across all 37,889 messages, so
ClassifyUsagereturned Unknown and nosession_usagerow was ever written.This reads the five counters Hermes keeps per session instead.
The mapping
TokenUsageInputTokensinput_tokens + cache_read_tokens + cache_write_tokensCacheReadTokens,CachedTokenscache_read_tokensCacheCreationTokenscache_write_tokensOutputTokensoutput_tokensTwo parts are easy to get wrong and both are pinned by tests.
input_tokensholds uncached input alone, while prosa's canonical aggregate isthe cache-inclusive grand total. That is the same shape the Claude Code
importer builds, and exactly how Hermes derives its own
prompt_tokens.reasoning_tokensstays out ofOutputTokens. Hermes already counts it there,which its own
total_tokensconfirms by not adding it. Folding it in woulddouble count, so it survives as provenance only.
Two behaviours worth reviewing
A row whose counters are all zero keeps classifying Unknown rather than
ExplicitZero, mirroring Hermes's own
has_usageflag. The distinction is notcosmetic: the import policy drops ExplicitZero outright, so the literal reading
would have deleted the 17 sessions that carry no counters.
The projected JSONL gains a leading
{"type":"session_usage","data":{…}}linecarrying the counters verbatim, following the envelope Grok Build already uses
for non-message rows. Without it the preserved raw could not explain the usage
prosa derives from it. Hermes is already authorized to project rather than copy
as a multi-session container, so this needs no new authorization, only the
version bump.
Measured result
Run against the maintainer's real
~/.hermes/state.dbin an isolatedPROSA_HOME, so nothing touched the live store or the server:Every model involved was already priced, so no pricing work was needed.
Known limitation
90 of the 950 Hermes sessions still get no usage, worth a further 354,968,047
tokens. These are the ones that defer to a fuller sibling transcript: the
counters live only on the
state.dbrow, and a transcript-sourced session'sraw is a verbatim copy that cannot carry them. Giving them usage means merging
both sources at projection time, which is the separate cut
docs/sources/hermes.mdalready anticipates under "Dual-source gap" and whichwould extend the projection authorization to a third shape. Left out
deliberately; the doc now names this as the visible cost of that gap.
Operational note
ProjectionVersiongoes 12 to 13, so the next sync re-imports and re-pushesevery Hermes session, roughly 325 MB of raw across 940 sessions. That re-push
would happen with or without the new projection line, because the server's push
short-circuit requires the projection version to be current.
Verification
just cigreen end to end, plusjust qualityand commitlint. Five new testscover the mapping, the cache-inclusive sum, reasoning not being double counted,
zero counters staying admitted, and a Hermes build predating the columns still
importing through the
NULL ASplaceholders. The determinism test now carriescounters so the usage line's byte stability stays covered.
prosa showstillrenders a session whose raw leads with the new line.
🤖 Generated with Claude Code
https://claude.ai/code/session_015UmSUq3v73JA3ZJr4nFhm9