Blackbox: subagent cost rollup in /cost session + per-turn retention sweep - #10
Merged
Merged
Conversation
…sweep
Two follow-ups to the turn-telemetry plugin:
1. Subagent cost rollup. delegate_tool records each subagent as its own turn
stamped with the PARENT's channel (platform/chat_id) and is_subagent=1.
They were silently folded into the session total with no visibility.
- store.subagent_rollup(platform, chat_id): sums cost/tokens of a channel's
subagent turns, counts unpriced (cost_usd IS NULL) separately so the
display can show an honest '+N unpriced' instead of undercounting, and
lists the models seen. Keyed by channel (not parent_turn_id, which holds
the parent SESSION KEY and isn't linkable to a parent turn row).
- store.session_rollup now also returns subagent_count/subagent_usd as a
subset of the (already subagent-inclusive) total.
- /cost session appends: '↳ Subagents: $X across N turn(s)[, +U unpriced][models]'
only when the channel has subagent turns.
2. Per-turn retention sweep. _on_session_end calls store.sweep(retention_days)
after recording. sweep() is self-throttling (a last_sweep_date sentinel
makes it a no-op after the first call each UTC day), so it costs one indexed
SELECT/day and keeps the store bounded. Guarded so a sweep failure never
blocks recording/alerting.
Verified empirically against the live store: a real telegram session renders
'↳ Subagents: $0 across 1 turn(s), +1 unpriced [claude-opus-4-8]' (the one
unpriced row predates the pricing fix). 73 blackbox+pricing tests green
(added: subagent_rollup channel/unpriced/isolation, session split, sweep
called with configured days, sweep-failure-doesn't-block, /cost session
subagent display), ruff clean.
🔎 Lint report:
|
Kyzcreig
added a commit
that referenced
this pull request
Jun 5, 2026
…sweep (#10) Two follow-ups to the turn-telemetry plugin: 1. Subagent cost rollup. delegate_tool records each subagent as its own turn stamped with the PARENT's channel (platform/chat_id) and is_subagent=1. They were silently folded into the session total with no visibility. - store.subagent_rollup(platform, chat_id): sums cost/tokens of a channel's subagent turns, counts unpriced (cost_usd IS NULL) separately so the display can show an honest '+N unpriced' instead of undercounting, and lists the models seen. Keyed by channel (not parent_turn_id, which holds the parent SESSION KEY and isn't linkable to a parent turn row). - store.session_rollup now also returns subagent_count/subagent_usd as a subset of the (already subagent-inclusive) total. - /cost session appends: '↳ Subagents: $X across N turn(s)[, +U unpriced][models]' only when the channel has subagent turns. 2. Per-turn retention sweep. _on_session_end calls store.sweep(retention_days) after recording. sweep() is self-throttling (a last_sweep_date sentinel makes it a no-op after the first call each UTC day), so it costs one indexed SELECT/day and keeps the store bounded. Guarded so a sweep failure never blocks recording/alerting. Verified empirically against the live store: a real telegram session renders '↳ Subagents: $0 across 1 turn(s), +1 unpriced [claude-opus-4-8]' (the one unpriced row predates the pricing fix). 73 blackbox+pricing tests green (added: subagent_rollup channel/unpriced/isolation, session split, sweep called with configured days, sweep-failure-doesn't-block, /cost session subagent display), ruff clean. Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
Kyzcreig
pushed a commit
that referenced
this pull request
Sep 2, 2026
…ared registry A gateway process opened state.db from ~12 call sites, each minting its own writer connection, self._lock, close-time WAL checkpoint, and token-writer thread. With N independent writers on one WAL file, one connection's close-time checkpoint could race another's growth — the lost/reordered-page-write signature across 11+ incidents (NousResearch#90837). Adds hermes_state_registry.py: a process-wide, per-path, refcounted shared registry owning the writer boundary. - acquire(path): same resolved path returns the same instance (one writer connection, one lock, one token-writer thread) for every long-lived in-process caller (gateway runner, SessionStore, per-agent lazy recall, cron per-job, mirror, channel_directory, slash_commands, shutdown_flush, session_search, react_to_message, delegate, mcp_serve, auto_archive, tui_gateway). - close() on a shared instance is a NO-OP — the registry owns the lifecycle, so one caller's close can never tear down a writer other callers still hold. - Generation-aware retirement on inode change: a replaced state.db RETIRES the live generation (never lent again) but keeps it alive for existing holders; release is object-keyed so holders of the old generation drain it independently of the new one. The old generation's own write path still fails with the typed StateDbReplacedError (existing protection, unchanged). - Replacement-open failure leaves NO registry entry for the path — the next acquire retries fresh, never hands out a closed stale object. - All teardown runs OUTSIDE the registry lock: a final release's WAL checkpoint can never stall acquisition for every state.db. - close_shared_session_dbs() at gateway shutdown drains every generation (live + retired) as the final safety net. CLI one-shots, recovery flows, and read-only cross-profile opens keep using SessionDB() directly with their own close() — only long-lived in-process sites route through the registry. References NousResearch#90837 (root-cause tracker stays open: the #10 EOF signature and the WAL-lifecycle A/B verdict remain under investigation there).
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.
Two follow-ups to Blackbox turn telemetry
1. Subagent cost rollup
delegate_toolrecords each subagent as its own turn, stamped with the parent's channel (platform/chat_id) andis_subagent=1. Until now they were silently folded into the session total with no visibility into how much spend came from delegation.store.subagent_rollup(platform, chat_id)— sums cost/tokens of a channel's subagent turns, counts unpriced (cost_usd IS NULL) separately so the display shows an honest+N unpricedinstead of undercounting, and lists the models seen. Keyed by channel, notparent_turn_id(which holds the parent session key and isn't linkable to a parent turn row).store.session_rollupnow also returnssubagent_count/subagent_usdas a subset of the (already subagent-inclusive) total./cost sessionappends↳ Subagents: $X across N turn(s)[, +U unpriced][models]— only when the channel actually has subagent turns (back-compatible; the plain line is unchanged otherwise).2. Per-turn retention sweep
_on_session_endcallsstore.sweep(retention_days)after recording.sweep()is self-throttling (alast_sweep_datesentinel makes it a no-op after the first call each UTC day), so it costs one indexed SELECT/day and keeps the store from growing unbounded. Guarded so a sweep failure never blocks recording/alerting.Verification
↳ Subagents: $0 across 1 turn(s), +1 unpriced [claude-opus-4-8](the one unpriced row predates the pricing fix).