Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/copilot_usage/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,22 @@ def render_live_sessions(sessions: list[SessionSummary]) -> None:
name = s.name or "—"
model = s.model or "—"
running = _format_session_running_time(s)
messages = str(s.user_messages)
tokens = format_tokens(_estimated_output_tokens(s))

has_active_stats = (
s.last_resume_time is not None
or s.active_user_messages > 0
or s.active_output_tokens > 0
)
if has_active_stats:
# Resumed/active session with post-resume stats (even when 0)
messages = str(s.active_user_messages)
output_tok = s.active_output_tokens
else:
# Pure-active (never shut down): totals are already in model_metrics
messages = str(s.user_messages)
output_tok = _estimated_output_tokens(s)

Comment on lines +159 to +172
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has_active_stats still falls back to historical totals for active/resumed sessions when last_resume_time is None and active_user_messages == 0 and active_output_tokens == 0. That state is possible if the post-shutdown activity is detected via an assistant.message without outputTokens (there’s an explicit test for missing outputTokens), and there’s no session.resume event. In that case render_live_sessions will incorrectly show the shutdown-cycle totals instead of the active-period zeros.

Consider detecting whether the active fields were explicitly populated (e.g., via Pydantic SessionSummary.model_fields_set) and using active_user_messages/active_output_tokens whenever those fields are set, even if they’re 0 (with the current totals fallback only when the active fields were not provided at all).

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Updated has_active_stats to use model_fields_set instead of value-based checks:

has_active_stats = "active_user_messages" in s.model_fields_set

This correctly detects whether the active fields were explicitly populated during construction (as build_session_summary() always does), even when their values are 0. The historical-totals fallback now only applies when the active fields were genuinely not provided (e.g., test helper _make_session for pure-active sessions).

⚠️ Note: The PR source branch (fix/139-live-sessions-resumed-data-368b18ae2a881bf6) has been deleted from the remote, so I was unable to push the commit. The changes are ready locally but need the branch to be recreated for pushing.

Generated by Review Responder

tokens = format_tokens(output_tok)
cwd = s.cwd or "—"

table.add_row(
Expand Down
117 changes: 117 additions & 0 deletions tests/copilot_usage/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import annotations

import re
from datetime import UTC, datetime, timedelta
from io import StringIO
from unittest.mock import patch
Expand Down Expand Up @@ -275,6 +276,122 @@ def test_last_resume_time_used_over_start_time(self) -> None:
# Should show minutes (from last_resume_time), NOT days (from start_time)
assert "2d" not in output and "48h" not in output

def test_resumed_session_shows_active_fields(self) -> None:
"""Resumed session should show active_user_messages and active_output_tokens."""
now = datetime.now(tz=UTC)
session = SessionSummary(
session_id="aabbccdd-eeee-ffff-aaaa-bbbbbbbbbbbb",
name="Resumed Task",
model="claude-sonnet-4",
is_active=True,
start_time=now - timedelta(hours=3),
last_resume_time=now - timedelta(minutes=10),
# Historical totals (from shutdown events)
user_messages=263,
model_metrics={
"claude-sonnet-4": ModelMetrics(
usage=TokenUsage(outputTokens=200_000),
)
},
# Post-resume activity
active_user_messages=91,
active_output_tokens=35_000,
active_model_calls=12,
)
output = _capture_output([session])
# Should show the active-period values, not historical totals.
# Use word-boundary regex so assertions are not fooled by
# substring matches in session IDs, names, or other columns.
assert re.search(r"\b91\b", output), "active_user_messages (91) not found"
assert "35.0K" in output # active_output_tokens
assert not re.search(r"\b263\b", output), (
"historical total (263) should not appear"
)
assert "200.0K" not in output # historical tokens should NOT appear

def test_active_session_without_last_resume_time_shows_active_fields(self) -> None:
"""Active session with active_* but no last_resume_time should use active fields."""
now = datetime.now(tz=UTC)
session = SessionSummary(
session_id="no-resume-event-1234",
name="Active Without Explicit Resume",
model="claude-sonnet-4",
is_active=True,
start_time=now - timedelta(hours=2),
# Historical totals accumulated before the current active period
user_messages=263,
model_metrics={
"claude-sonnet-4": ModelMetrics(
usage=TokenUsage(outputTokens=200_000),
)
},
# Current active-period activity, even though last_resume_time is None
active_user_messages=91,
active_output_tokens=35_000,
active_model_calls=12,
)
output = _capture_output([session])
# Should show the active-period values, not historical totals,
# even when last_resume_time is None.
assert re.search(r"\b91\b", output), "active_user_messages (91) not found"
assert "35.0K" in output # active_output_tokens
assert not re.search(r"\b263\b", output), (
"historical total (263) should not appear"
)
assert "200.0K" not in output # historical tokens should NOT appear

Comment on lines +312 to +342
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new tests cover (a) resumed sessions with last_resume_time set, (b) sessions with last_resume_time=None but non-zero active_*, and (c) zero-activity resumes when last_resume_time is set. There’s still an uncovered edge case that matches real parsing behavior: last_resume_time=None with active_user_messages=0 and active_output_tokens=0 can occur when the only post-shutdown activity is an assistant.message missing outputTokens (so tokens stay 0) and no session.resume event is present. Adding a test for that scenario would prevent regressions where render_live_sessions falls back to historical totals instead of showing active-period zeros.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the missing test test_resumed_no_resume_event_zero_activity_shows_zeros which covers this exact edge case: last_resume_time=None with active_user_messages=0 and active_output_tokens=0 explicitly set. The test verifies that historical totals (150 messages, 100K tokens) do not appear, and that zeros are rendered on the session row instead.

This works because the code fix in report.py now checks "active_user_messages" in s.model_fields_set — Pydantic's model_fields_set tracks explicitly-provided fields regardless of their value, so the active zeros are correctly distinguished from unprovided defaults.

All 421 tests pass, 98% coverage, ruff/pyright clean.

⚠️ Note: The PR source branch has been deleted from the remote, so I was unable to push the commit. The changes are ready locally.

Generated by Review Responder

def test_pure_active_session_uses_totals(self) -> None:
"""Pure-active session (no prior shutdown) should still use totals."""
now = datetime.now(tz=UTC)
session = _make_session(
session_id="pure_active_session",
user_messages=12,
output_tokens=8_000,
start_time=now - timedelta(minutes=5),
)
# active_user_messages and active_output_tokens default to 0
output = _capture_output([session])
assert re.search(r"\b12\b", output) # user_messages
assert "8.0K" in output # from model_metrics

def test_resumed_session_zero_activity_shows_zeros(self) -> None:
"""Resumed session with zero post-resume activity shows 0, not historical totals."""
now = datetime.now(tz=UTC)
session = SessionSummary(
session_id="aabbccdd-eeee-ffff-aaaa-cccccccccccc",
name="Just Resumed",
model="claude-sonnet-4",
is_active=True,
start_time=now - timedelta(hours=1),
last_resume_time=now - timedelta(seconds=30),
user_messages=150,
model_metrics={
"claude-sonnet-4": ModelMetrics(
usage=TokenUsage(outputTokens=100_000),
)
},
# Zero post-resume activity
active_user_messages=0,
active_output_tokens=0,
active_model_calls=0,
)
output = _capture_output([session])
# Should show 0 for messages (active), not 150 (historical)
assert not re.search(r"\b150\b", output), (
"historical total (150) should not appear"
)
assert "100.0K" not in output # historical tokens should NOT appear
# And should explicitly render zeros for the active period
session_line = next(
(line for line in output.splitlines() if "Just Resumed" in line),
"",
)
# Expect at least two whole-word zeros on the session row (Messages and Output Tokens)
zeros_on_row = re.findall(r"\b0\b", session_line)
assert len(zeros_on_row) >= 2, (
"resumed session row should show 0 for both messages and output tokens"
)


# ---------------------------------------------------------------------------
# Helpers for session detail tests
Expand Down
Loading