Repository navigation
Fix session analytics accuracy: dedupe usage, current pricing, active time #106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4ee623d
64b27ff
4ea0152
e994853
4e98db7
93a2a75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,8 +10,7 @@ | |
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| # v20/v21 are taken by PR #106 (session analytics accuracy); v22 is the orphan repair. | ||
| SCHEMA_VERSION = 22 | ||
| SCHEMA_VERSION = 23 | ||
|
|
||
| SCHEMA_SQL = """ | ||
| -- Schema versioning | ||
|
|
@@ -35,6 +34,11 @@ | |
| cache_creation_tokens INTEGER DEFAULT 0, | ||
| cache_read_tokens INTEGER DEFAULT 0, | ||
| total_cost REAL DEFAULT 0, | ||
| subagent_cost REAL DEFAULT 0, | ||
| subagent_input_tokens INTEGER DEFAULT 0, | ||
| subagent_output_tokens INTEGER DEFAULT 0, | ||
| subagent_cache_read_tokens INTEGER DEFAULT 0, | ||
| subagent_cache_creation_tokens INTEGER DEFAULT 0, | ||
| initial_prompt TEXT, | ||
| git_branch TEXT, | ||
| models_used TEXT, | ||
|
|
@@ -426,6 +430,9 @@ def ensure_schema(conn: sqlite3.Connection) -> None: | |
| # v13 incremental block below (ALTER is not idempotent). | ||
| conn.executescript(_SHELLS_CRON_SCHEMA_SQL) | ||
|
|
||
| # Same cross-branch guard for sessions columns added after v19. | ||
| columns_added = _ensure_session_columns(conn) | ||
|
|
||
| # Check current version | ||
| try: | ||
| row = conn.execute("SELECT MAX(version) FROM schema_version").fetchone() | ||
|
|
@@ -799,6 +806,18 @@ def ensure_schema(conn: sqlite3.Connection) -> None: | |
| logger.info("Migrating → v22: remove orphaned child rows (issue #107)") | ||
| _delete_orphan_rows(conn) | ||
|
|
||
| if current_version < 23: | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One v23 block for the re-pricing reindex. The column adds moved out of the version gate into |
||
| logger.info( | ||
| "Migrating → v23: re-index costs " | ||
| "(per-response usage dedup, current pricing, active time)" | ||
| ) | ||
| has_sessions = conn.execute( | ||
| "SELECT 1 FROM sqlite_master WHERE type='table' AND name='sessions'" | ||
| ).fetchone() | ||
| # _ensure_session_columns already forced the reindex if it added columns | ||
| if has_sessions and not columns_added: | ||
| conn.execute("UPDATE sessions SET jsonl_mtime = jsonl_mtime - 1") | ||
|
|
||
| # Record version | ||
| conn.execute( | ||
| "INSERT OR REPLACE INTO schema_version (version) VALUES (?)", | ||
|
|
@@ -812,6 +831,31 @@ def ensure_schema(conn: sqlite3.Connection) -> None: | |
| logger.info("Schema version %d applied successfully", SCHEMA_VERSION) | ||
|
|
||
|
|
||
| # Sessions columns added after v19; also in SCHEMA_SQL for fresh installs. | ||
| _SESSION_COLUMNS = [ | ||
| ("subagent_cost", "REAL DEFAULT 0"), | ||
| ("subagent_input_tokens", "INTEGER DEFAULT 0"), | ||
| ("subagent_output_tokens", "INTEGER DEFAULT 0"), | ||
| ("subagent_cache_read_tokens", "INTEGER DEFAULT 0"), | ||
| ("subagent_cache_creation_tokens", "INTEGER DEFAULT 0"), | ||
| ] | ||
|
|
||
|
|
||
| def _ensure_session_columns(conn: sqlite3.Connection) -> bool: | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Runs on every startup before the version check, like the tickets and shells tables, so a DB stamped higher by another branch (e.g. v24 from #85) still gets these columns. Adding a column also forces one reindex so the new values get filled in. |
||
| """Add missing sessions columns regardless of schema version; True if any were added.""" | ||
| existing = {r[1] for r in conn.execute("PRAGMA table_info(sessions)").fetchall()} | ||
| if not existing: | ||
| return False | ||
| missing = [(name, ddl) for name, ddl in _SESSION_COLUMNS if name not in existing] | ||
| for name, ddl in missing: | ||
| conn.execute(f"ALTER TABLE sessions ADD COLUMN {name} {ddl}") | ||
| if missing: | ||
| logger.info("Added sessions columns %s; forcing a reindex", [n for n, _ in missing]) | ||
| conn.execute("UPDATE sessions SET jsonl_mtime = jsonl_mtime - 1") | ||
| conn.commit() | ||
| return bool(missing) | ||
|
|
||
|
|
||
| # Child rows whose parent is gone; (table, parent-existence subquery). | ||
| _ORPHAN_RULES = [ | ||
| ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| """Active time: the sum of gaps between messages, with each idle gap capped.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from datetime import datetime | ||
| from typing import Optional | ||
|
|
||
| # Idle gaps between messages count toward active duration only up to this cap | ||
| ACTIVE_IDLE_CAP_SECONDS = 30 * 60 | ||
|
|
||
|
|
||
| class ActiveTimeAccumulator: | ||
| """Feed message timestamps in file order; `seconds` is the active time so far.""" | ||
|
|
||
| def __init__(self) -> None: | ||
| self.seconds = 0.0 | ||
| self._latest: Optional[datetime] = None # active time only moves forward | ||
|
|
||
| def add(self, timestamp: Optional[datetime]) -> None: | ||
| if timestamp is None: | ||
| return | ||
| if self._latest is None: | ||
| self._latest = timestamp | ||
| elif timestamp > self._latest: | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Active time advances from the max timestamp seen, so an out-of-order line can't add a negative gap or double-count one. Session and Agent now share this, which is what makes subagent durations active time too. |
||
| gap = (timestamp - self._latest).total_seconds() | ||
| self.seconds += min(gap, ACTIVE_IDLE_CAP_SECONDS) | ||
| self._latest = timestamp | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correction to my earlier note: Agent metadata is cached by file path, so the old code wasn't re-parsing subagent files. This listing now feeds the cost, the token totals and the invocation rows; the change is about one listing, not about parse count.