Skip to content

Blackbox: subagent cost rollup in /cost session + per-turn retention sweep - #10

Merged
Kyzcreig merged 1 commit into
mainfrom
feat/blackbox-subagent-rollup-and-sweep
Jun 4, 2026
Merged

Kyzcreig merged 1 commit into
mainfrom
feat/blackbox-subagent-rollup-and-sweep

Conversation

@Kyzcreig

@Kyzcreig Kyzcreig commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Two follow-ups to Blackbox turn telemetry

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. 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 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 actually has subagent turns (back-compatible; the plain line is unchanged otherwise).

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 from growing unbounded. Guarded so a sweep failure never blocks recording/alerting.

Verification

  • Empirical, 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 aggregation, unpriced count, channel/main isolation, blank-channel), session split, sweep-called-with-configured-days, sweep-failure-doesn't-block-recording, /cost session subagent display. Ruff clean.

…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.
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

🔎 Lint report: feat/blackbox-subagent-rollup-and-sweep vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9830 on HEAD, 9830 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5116 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@Kyzcreig
Kyzcreig merged commit e536105 into main Jun 4, 2026
23 checks passed
@Kyzcreig
Kyzcreig deleted the feat/blackbox-subagent-rollup-and-sweep branch June 4, 2026 12:34
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).
@Kyzcreig
Kyzcreig restored the feat/blackbox-subagent-rollup-and-sweep branch September 21, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant