
    +j,                     :   U d Z ddlZddlZddlZddlmZmZmZ  ej        e	          Z
dddddd	Zeeef         ed
<   dZddee         dee         fdZ	 ddedee         deej                 fdZdeej                 deeef         fdZ G d d          ZdS )u  
LightClaw — per-turn token usage tracker.

Hermes stores token counts in its SQLite ``state.db`` ``sessions`` table,
but the columns are session-cumulative. We mirror openclaw's per-turn
``usage`` frame by computing ``current_cumulative − turn_start_baseline``:
the whole-turn total (every LLM call of one question summed).

Note: openclaw reports only the last LLM call's usage; we report the
whole-turn sum. The wire format (openclaw ``UnifiedUsage``, camelCase) is
identical, so front-end / ai-server need zero hermes-specific code.

Baselines live in memory keyed by ``chat_id``. If lost mid-turn (e.g.
process restart), we skip the frame rather than over-report the session total.
    N)DictOptionalTupleinput_tokensoutput_tokenscache_read_tokenscache_write_tokensreasoning_tokens)inputTokensoutputTokenscachedInputTokenscacheWriteTokensreasoningTokens_TOKEN_COLUMNShermessessions_dirreturnc                 f   g }| rqt           j                            t           j                            |                     }|r3|                    t           j                            |d                     t           j                            d          x}r3|                    t           j                            |d                     |                    t           j                            d                     	 t          t          j
        d                    D ]}|                    d| d           n# t          $ r Y nw xY w|                    d           t                      }|D ]@}|r<||vr8|                    |           t           j                            |          r|c S AdS )	u  Locate the Hermes ``state.db`` — never hardcoding a path or username.

    Resolution order (most authoritative first), kept consistent with the
    rest of the plugin (``adapter._sessions_dir`` /
    ``history._default_sessions_dir``) and the install scripts:

      1. Sibling of *sessions_dir*: ``<HERMES_HOME>/sessions`` →
         ``<HERMES_HOME>/state.db``.  This is the path the adapter already
         resolved at runtime (config ``extra.sessions_dir`` →
         ``LIGHTCLAW_SESSIONS_DIR`` → ``$HERMES_HOME/sessions``), so it
         honours custom install locations automatically.
      2. ``$HERMES_HOME/state.db`` — the canonical env var used by every
         ``hermes_*.sh`` script and by the adapter.
      3. ``~/.hermes/state.db`` for the current user.
      4. Last-resort scan of real home dirs (``/home/*/.hermes`` and
         ``/root/.hermes``), mirroring ``hermes_install.sh``'s own probe —
         covers container setups where the gateway runs under a user that
         differs from ``$HOME``, without ever assuming ``ubuntu``.

    Returns the first existing file, or ``None`` (caller treats that as
    "no usage data available" and skips — never blocks the main path).
    zstate.dbHERMES_HOMEz~/.hermes/state.dbz/homez/home/z/.hermes/state.dbz/root/.hermes/state.dbN)ospathdirnamenormpathappendjoinenvironget
expandusersortedlistdirOSErrorsetaddisfile)r   
candidatesparenthermes_homeentryseenr   s          A/home/agentuser/.hermes/plugins/lightclawbot/src/usage_tracker.py_resolve_state_db_pathr+   '   s   . J  @!1!1,!?!?@@ 	@bgll6:>>??? jnn]333{ A"',,{J??@@@ bg(()=>>???BJw//00 	A 	AE?u???@@@@	A   .///D   	D$$HHTNNNw~~d## 4s   ==D; ;
EEchat_idc                 z   t          |          }|st                              d           dS d}	 t          j        |          }	 t          j        |_        |                                }|                    d| dt          |           f           |
                                }|-|                    d| d           |
                                }||                                 S # |                                 w xY w# t          j        $ r&}t                              d|           Y d}~dS d}~ww xY w)u   Read the latest ``sessions`` row for *chat_id* (falling back to the
    absolute-latest row). Returns ``None`` on any error — usage is
    best-effort and must never break the outbound path.z%[lightclaw] usage: state.db not foundNz[input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, modelzSELECT zA FROM sessions WHERE user_id = ? ORDER BY started_at DESC LIMIT 1z/ FROM sessions ORDER BY started_at DESC LIMIT 1z,[lightclaw] usage: sessions query failed: %s)r+   loggerinfosqlite3connectRowrow_factorycursorexecutestrfetchonecloseErrorwarning)r,   r   db_pathcolsconncurrowexcs           r*   _read_session_rowrA   _   s\    %\22G ;<<<t	 	
w''	&{D++--CKKE$ E E EW  
 ,,..C{7d 7 7 7   llnnJJLLLLDJJLLLL=   EsKKKttttts0   D BC, D ,DD D:D55D:r?   c                 $   d t           D             }| |S t          |                                           }t                                           D ]?\  }}||v r6| |         }t	          |t
          t          f          rt          |          ||<   @|S )uB   Extract cumulative token counters from a row (missing/NULL → 0).c                     i | ]}|d S )r    ).0fields     r*   
<dictcomp>z$_row_to_counters.<locals>.<dictcomp>   s    EEEUqEEE    )r   r"   keysitems
isinstanceintfloat)r?   countersrI   rF   columnvalues         r*   _row_to_countersrQ      s    EEnEEEH
{sxxzz??D'--// - -vT>>KE%#u.. -"%e**OrH   c                       e Zd ZdZddee         ddfdZdeddfdZdedeee	ee
f                  ef         fdZdedee	ee
f                  fd	Zdeddfd
ZdS )SessionUsageTrackerzATracks per-turn token deltas against session-cumulative counters.Nr   r   c                 0    || _         i | _        i | _        d S N)_sessions_dir
_baselines_models)selfr   s     r*   __init__zSessionUsageTracker.__init__   s     -95713rH   r,   c                     t          || j                  }t          |          | j        |<   d}|+dt	          |                                          v r|d         }|| j        |<   dS )zRecord cumulative counters as the baseline for a new turn (called
        at inbound). An unreadable DB yields an all-zero baseline, correct
        for a fresh session's first turn.Nmodel)rA   rV   rQ   rW   r"   rI   rX   )rY   r,   r?   r\   s       r*   snapshot_baselinez%SessionUsageTracker.snapshot_baseline   sg      );<<#3C#8#8 ?w#chhjj//99LE %WrH   c                    | j                             |          }|t                              d|           dS t	          || j                  }|t                              d|           dS t          |          }i }t          D ]<}|                    |d          |                    |d          z
  }|dk    r|nd||<   =|d         }|d         }	|dk    r|	dk    rdS ||	||	z   t          d	}
d
D ]}||         dk    r||         |
|<   | j	                            |          }t          |t                    r|r||
d<   |
dfS )u  Classify the round that just finished and (when real) return its usage.

        The cumulative token counters in Hermes' ``state.db`` are the only
        deterministic signal we have, so the verdict is driven entirely by
        whether — and by how much — they moved against the turn-start
        baseline. The return is ``(usage, reason)`` where ``reason`` is one of:

          * ``"usage"`` — baseline present, DB read OK, **delta > 0**. A real
            conversation turn ran an LLM call; ``usage`` is the openclaw
            ``UnifiedUsage`` object (whole-turn ``current − baseline``).
          * ``"no_llm"`` — baseline present, DB read OK, **delta == 0**. The
            counters did not move, so no LLM call ran. These are framework
            *command* rounds (``/new`` / ``/approve`` / ``/always``) that are
            never stored as transcript turns; the caller MUST NOT write a
            usage sidecar entry — a phantom line shifts the ordinal
            sidecar↔turn join and drops a real turn's usage on history reload.
            ``usage`` is ``None``.
          * ``"unknown"`` — we could **not measure** this turn: the baseline
            was lost (mid-turn restart) or the ``state.db`` row could not be
            read this instant. This is NOT proof the turn was free; it may be
            a real turn we simply failed to measure. The caller should write a
            placeholder to hold this turn's slot rather than silently dropping
            it. ``usage`` is ``None``.

        Crucially this distinguishes *measured zero* (``no_llm``) from
        *could-not-measure* (``unknown``): collapsing them — as a plain
        baseline check would — risks misreading a real turn that hit a
        transient DB read failure as a free command round.
        Nzi[lightclaw] usage unknown: no baseline for chat_id=%s (inbound snapshot missing, likely mid-turn restart))Nunknownzd[lightclaw] usage unknown: state.db row unreadable for chat_id=%s (cannot measure this turn's delta)r   r   r   )Nno_llm)r   r   totalTokensprovider)r   r   r   r\   usage)rW   r   r.   r/   rA   rV   rQ   r   	_PROVIDERrX   rK   r6   )rY   r,   baseliner?   currentdeltarF   diffr   r   rc   r\   s               r*   classify_turnz!SessionUsageTracker.classify_turn   s   D ?&&w//KKF  
 #?
  );<<;KK@  
 #?"3'' "# 	3 	3E;;ua((8<<q+A+AAD#'!8844E%LL]+n- 1!!3!3!> ()'-7!	$
 $
 R 	, 	,EU|a$U|e   ))eS!! 	#e 	#"E'Ng~rH   c                 8    |                      |          d         S )zReturn the per-turn usage object, or ``None`` if not emittable.

        Thin wrapper over :meth:`classify_turn` for callers that only need the
        usage value and not the reason it was (or wasn't) produced.
        r   )ri   rY   r,   s     r*   compute_turn_usagez&SessionUsageTracker.compute_turn_usage  s     !!'**1--rH   c                 r    | j                             |d           | j                            |d           dS )z/Drop any cached baseline / model for *chat_id*.N)rW   poprX   rk   s     r*   clearzSessionUsageTracker.clear  s8    GT***$'''''rH   rU   )__name__
__module____qualname____doc__r   r6   rZ   r]   r   r   objectri   rl   ro   rD   rH   r*   rS   rS      s        KK4 4Xc] 4d 4 4 4 4	& 	& 	& 	& 	& 	&WW	xS&[)*C/	0W W W Wr.# .(4V;L2M . . . .(S (T ( ( ( ( ( (rH   rS   rU   )rs   loggingr   r0   typingr   r   r   	getLoggerrp   r.   r   r6   __annotations__rd   r+   r2   rA   rL   rQ   rS   rD   rH   r*   <module>ry      s}       				  ( ( ( ( ( ( ( ( ( (		8	$	$ ))-.," "S#X    	5 5# 5(3- 5 5 5 5r 15& && (&gk& & & &R(7;/ DcN    {( {( {( {( {( {( {( {( {( {(rH   