fix(oneshot): --usage-file ledger reports auxiliary LLM spend
`hermes -z --usage-file` copied only the main-loop result, so title generation, vision, compression, web_extract and background-review calls — recorded per task in session_model_usage — never reached the pipeline ledger the flag advertises as "so pipelines can always account for spend". The Insights page already folds those rows in (#23270); the ledger is now consistent with it. - SessionDB.auxiliary_usage_by_task(session_id): per-task sums over the session's compression lineage (aux calls bill to the id the turn started with while compression mints child ids mid-turn). - oneshot snapshots aux usage before the turn and attaches the delta after it, so a resumed session's earlier runs are not re-billed. - The report gains `auxiliary` (totals + `by_task`) and `total_including_auxiliary`; every existing key keeps its main-loop meaning. - The auto-title upgrade runs on a daemon thread and can still be writing when the turn returns: title_generator tracks in-flight upgrade threads and oneshot joins them (bounded) before reading — no sleep, no eager read. Fixes #112848. Direction shared with #112852 (@KoNit-K), which folded aux into the headline counters; this keeps them backward compatible instead. Co-authored-by: KoNit-K <124019182+KoNit-K@users.noreply.github.com>
This commit is contained in:
@@ -9,6 +9,9 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
import weakref
|
||||
from contextlib import suppress
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
@@ -19,6 +22,18 @@ from agent.message_content import flatten_message_text
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# In-flight stage-2 upgrade threads. They bill their aux usage to the session from a daemon thread,
|
||||
# so a process that reads the ledger right before exit (``-z --usage-file``) must be able to join
|
||||
# them (bounded) instead of racing the write (#112848).
|
||||
_UPGRADE_THREADS: "weakref.WeakSet[threading.Thread]" = weakref.WeakSet()
|
||||
|
||||
|
||||
def wait_for_title_upgrades(timeout: float = 10.0) -> None:
|
||||
"""Bounded join of the auto-title threads still running; never raises."""
|
||||
deadline = time.monotonic() + timeout
|
||||
for thread in list(_UPGRADE_THREADS):
|
||||
thread.join(max(0.0, deadline - time.monotonic()))
|
||||
|
||||
# (task_name, exception) -> None; surfaces auxiliary failures so silent drops don't pile up as NULL titles.
|
||||
FailureCallback = Callable[[str, BaseException], None]
|
||||
# (title, source) -> None; source is the persisted provenance (``derived`` / ``llm``). Consumers paying a
|
||||
@@ -528,9 +543,11 @@ def maybe_auto_title(
|
||||
# profile whose turn this is: a bare Thread starts with an empty context and lands on the launch
|
||||
# profile under multiplex, titling X's session with the default profile's model and billing its key.
|
||||
from agent.memory_provider import spawn_context_thread
|
||||
spawn_context_thread(
|
||||
upgrade = spawn_context_thread(
|
||||
auto_title_session, name="auto-title",
|
||||
args=(session_db, session_id, user_message),
|
||||
kwargs=dict(failure_callback=failure_callback, main_runtime=main_runtime, title_callback=title_callback,
|
||||
runtime_validator=runtime_validator),
|
||||
).start()
|
||||
)
|
||||
_UPGRADE_THREADS.add(upgrade)
|
||||
upgrade.start()
|
||||
|
||||
Reference in New Issue
Block a user