lewtun HF Staff OpenAI Codex commited on
Commit
cbf5d33
·
unverified ·
1 Parent(s): e6f00a2

Enforce premium quota across sessions daily (#287)

Browse files

Co-authored-by: OpenAI Codex <codex@openai.com>

agent/core/session_persistence.py CHANGED
@@ -175,6 +175,7 @@ class MongoSessionStore(NoopSessionStore):
175
  turn_count: int = 0,
176
  pending_approval: list[dict[str, Any]] | None = None,
177
  claude_counted: bool = False,
 
178
  premium_user_billed: bool = False,
179
  notification_destinations: list[str] | None = None,
180
  auto_approval_enabled: bool = False,
@@ -207,6 +208,7 @@ class MongoSessionStore(NoopSessionStore):
207
  "turn_count": turn_count,
208
  "pending_approval": pending_approval or [],
209
  "claude_counted": claude_counted,
 
210
  "premium_user_billed": premium_user_billed,
211
  "notification_destinations": notification_destinations or [],
212
  "auto_approval_enabled": auto_approval_enabled,
@@ -230,6 +232,7 @@ class MongoSessionStore(NoopSessionStore):
230
  turn_count: int = 0,
231
  pending_approval: list[dict[str, Any]] | None = None,
232
  claude_counted: bool = False,
 
233
  premium_user_billed: bool = False,
234
  created_at: datetime | None = None,
235
  notification_destinations: list[str] | None = None,
@@ -255,6 +258,7 @@ class MongoSessionStore(NoopSessionStore):
255
  turn_count=turn_count,
256
  pending_approval=pending_approval,
257
  claude_counted=claude_counted,
 
258
  premium_user_billed=premium_user_billed,
259
  notification_destinations=notification_destinations,
260
  auto_approval_enabled=auto_approval_enabled,
 
175
  turn_count: int = 0,
176
  pending_approval: list[dict[str, Any]] | None = None,
177
  claude_counted: bool = False,
178
+ claude_counted_day: str | None = None,
179
  premium_user_billed: bool = False,
180
  notification_destinations: list[str] | None = None,
181
  auto_approval_enabled: bool = False,
 
208
  "turn_count": turn_count,
209
  "pending_approval": pending_approval or [],
210
  "claude_counted": claude_counted,
211
+ "claude_counted_day": claude_counted_day,
212
  "premium_user_billed": premium_user_billed,
213
  "notification_destinations": notification_destinations or [],
214
  "auto_approval_enabled": auto_approval_enabled,
 
232
  turn_count: int = 0,
233
  pending_approval: list[dict[str, Any]] | None = None,
234
  claude_counted: bool = False,
235
+ claude_counted_day: str | None = None,
236
  premium_user_billed: bool = False,
237
  created_at: datetime | None = None,
238
  notification_destinations: list[str] | None = None,
 
258
  turn_count=turn_count,
259
  pending_approval=pending_approval,
260
  claude_counted=claude_counted,
261
+ claude_counted_day=claude_counted_day,
262
  premium_user_billed=premium_user_billed,
263
  notification_destinations=notification_destinations,
264
  auto_approval_enabled=auto_approval_enabled,
backend/routes/agent.py CHANGED
@@ -207,21 +207,23 @@ async def _enforce_premium_model_quota(
207
 
208
  Runs at *message-submit* time, not session-create time — so spinning up a
209
  premium-model session to look around doesn't burn quota. The
210
- ``claude_counted`` flag on ``AgentSession`` guards against re-counting the
211
- same session; the stored field name is kept for persistence compatibility.
 
212
 
213
  Subsidizes the daily allowance (free = 2 for default premium, pro = 20
214
  across premium models), organization-billed through the HF Router. Opus and
215
  GPT-5.5 are pro-only before quota is charged. Past the allowance, premium
216
  router models flip the session to ``premium_user_billed`` so the call bills
217
  the user's own HF token instead of blocking. No-ops when the model isn't
218
- premium or when this session's billing has already been decided.
219
  """
220
  model_name = agent_session.session.config.model_name
221
  if not _is_premium_model(model_name):
222
  return
223
  _reject_model_unavailable_for_plan(model_name, user)
224
- if agent_session.claude_counted:
 
225
  return
226
  user_id = user["user_id"]
227
  plan = user.get("plan", "free")
@@ -241,7 +243,12 @@ async def _enforce_premium_model_quota(
241
  # Past the subsidized allowance on a user-billable model: bill the
242
  # user's own HF (OAuth) token for this session instead of blocking.
243
  agent_session.session.premium_user_billed = True
 
 
 
 
244
  agent_session.claude_counted = True
 
245
  await session_manager.persist_session_snapshot(agent_session)
246
 
247
 
@@ -909,8 +916,8 @@ async def chat_sse(
909
  approvals = body.get("approvals")
910
 
911
  # Gate user-message sends against the daily premium-model quota. Approvals are
912
- # continuations of an in-progress turn the session was already charged
913
- # on its first message, so we skip the gate there.
914
  if text is not None and not approvals:
915
  try:
916
  await _enforce_premium_model_quota(user, agent_session)
 
207
 
208
  Runs at *message-submit* time, not session-create time — so spinning up a
209
  premium-model session to look around doesn't burn quota. The
210
+ ``claude_counted_day`` flag on ``AgentSession`` guards against re-counting
211
+ the same session on the same day, while still counting old sessions when
212
+ they are used again on a later day.
213
 
214
  Subsidizes the daily allowance (free = 2 for default premium, pro = 20
215
  across premium models), organization-billed through the HF Router. Opus and
216
  GPT-5.5 are pro-only before quota is charged. Past the allowance, premium
217
  router models flip the session to ``premium_user_billed`` so the call bills
218
  the user's own HF token instead of blocking. No-ops when the model isn't
219
+ premium or when this session's billing has already been decided for today.
220
  """
221
  model_name = agent_session.session.config.model_name
222
  if not _is_premium_model(model_name):
223
  return
224
  _reject_model_unavailable_for_plan(model_name, user)
225
+ quota_day = user_quotas.current_quota_day()
226
+ if agent_session.claude_counted and agent_session.claude_counted_day == quota_day:
227
  return
228
  user_id = user["user_id"]
229
  plan = user.get("plan", "free")
 
243
  # Past the subsidized allowance on a user-billable model: bill the
244
  # user's own HF (OAuth) token for this session instead of blocking.
245
  agent_session.session.premium_user_billed = True
246
+ else:
247
+ # A session that overflowed on a previous day can use today's
248
+ # subsidized allowance again if quota is available.
249
+ agent_session.session.premium_user_billed = False
250
  agent_session.claude_counted = True
251
+ agent_session.claude_counted_day = quota_day
252
  await session_manager.persist_session_snapshot(agent_session)
253
 
254
 
 
916
  approvals = body.get("approvals")
917
 
918
  # Gate user-message sends against the daily premium-model quota. Approvals are
919
+ # continuations of an in-progress turn, so the relevant quota decision was
920
+ # made when that user message was submitted.
921
  if text is not None and not approvals:
922
  try:
923
  await _enforce_premium_model_quota(user, agent_session)
backend/session_manager.py CHANGED
@@ -6,7 +6,7 @@ import logging
6
  import os
7
  import uuid
8
  from dataclasses import dataclass, field
9
- from datetime import datetime, timedelta
10
  from pathlib import Path
11
  from typing import Any, Optional
12
 
@@ -113,9 +113,28 @@ class AgentSession:
113
  is_reaping: bool = False
114
  broadcaster: Any = None
115
  title: str | None = None
116
- # True once this session has been counted against the user's daily premium
117
- # quota. The field name is kept for persistence compatibility.
118
  claude_counted: bool = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
 
121
  class SessionCapacityError(Exception):
@@ -600,6 +619,7 @@ class SessionManager:
600
  agent_session.session
601
  ),
602
  claude_counted=agent_session.claude_counted,
 
603
  premium_user_billed=getattr(
604
  agent_session.session, "premium_user_billed", False
605
  ),
@@ -696,6 +716,13 @@ class SessionManager:
696
  premium_user_billed=bool(meta.get("premium_user_billed", False)),
697
  claude_counted=bool(meta.get("claude_counted")),
698
  )
 
 
 
 
 
 
 
699
  event_queue: asyncio.Queue = asyncio.Queue()
700
  submission_queue: asyncio.Queue = asyncio.Queue()
701
  tool_router, session = await asyncio.to_thread(
@@ -779,6 +806,7 @@ class SessionManager:
779
  is_active=True,
780
  is_processing=False,
781
  claude_counted=claude_counted,
 
782
  title=meta.get("title"),
783
  )
784
  started = await self._start_agent_session(
@@ -1505,10 +1533,8 @@ class SessionManager:
1505
  agent_session.session.notification_destinations
1506
  ),
1507
  "auto_approval": self._auto_approval_summary(agent_session.session),
1508
- "premium_user_billed": getattr(
1509
- agent_session.session, "premium_user_billed", False
1510
- ),
1511
- "premium_quota_counted": agent_session.claude_counted,
1512
  }
1513
 
1514
  def set_notification_destinations(
@@ -1562,6 +1588,10 @@ class SessionManager:
1562
  else:
1563
  created_at_str = str(created_at or datetime.utcnow().isoformat())
1564
  pending = self._pending_docs_for_api(row.get("pending_approval") or [])
 
 
 
 
1565
  results.append(
1566
  {
1567
  "session_id": str(sid),
@@ -1575,8 +1605,9 @@ class SessionManager:
1575
  "title": row.get("title"),
1576
  "premium_user_billed": bool(
1577
  row.get("premium_user_billed", False)
 
1578
  ),
1579
- "premium_quota_counted": bool(row.get("claude_counted", False)),
1580
  "notification_destinations": row.get(
1581
  "notification_destinations"
1582
  )
 
6
  import os
7
  import uuid
8
  from dataclasses import dataclass, field
9
+ from datetime import UTC, datetime, timedelta
10
  from pathlib import Path
11
  from typing import Any, Optional
12
 
 
113
  is_reaping: bool = False
114
  broadcaster: Any = None
115
  title: str | None = None
116
+ # True once this session has ever been counted against premium quota.
117
+ # claude_counted_day decides whether it has already consumed today's cap.
118
  claude_counted: bool = False
119
+ claude_counted_day: str | None = None
120
+
121
+
122
+ def _quota_day_today() -> str:
123
+ return datetime.now(UTC).date().isoformat()
124
+
125
+
126
+ def _quota_counted_today(agent_session: AgentSession) -> bool:
127
+ return (
128
+ agent_session.claude_counted
129
+ and agent_session.claude_counted_day == _quota_day_today()
130
+ )
131
+
132
+
133
+ def _premium_user_billed_today(agent_session: AgentSession) -> bool:
134
+ return bool(
135
+ getattr(agent_session.session, "premium_user_billed", False)
136
+ and _quota_counted_today(agent_session)
137
+ )
138
 
139
 
140
  class SessionCapacityError(Exception):
 
619
  agent_session.session
620
  ),
621
  claude_counted=agent_session.claude_counted,
622
+ claude_counted_day=agent_session.claude_counted_day,
623
  premium_user_billed=getattr(
624
  agent_session.session, "premium_user_billed", False
625
  ),
 
716
  premium_user_billed=bool(meta.get("premium_user_billed", False)),
717
  claude_counted=bool(meta.get("claude_counted")),
718
  )
719
+ claude_counted_day = (
720
+ str(meta.get("claude_counted_day"))
721
+ if meta.get("claude_counted_day")
722
+ else None
723
+ )
724
+ if not claude_counted:
725
+ claude_counted_day = None
726
  event_queue: asyncio.Queue = asyncio.Queue()
727
  submission_queue: asyncio.Queue = asyncio.Queue()
728
  tool_router, session = await asyncio.to_thread(
 
806
  is_active=True,
807
  is_processing=False,
808
  claude_counted=claude_counted,
809
+ claude_counted_day=claude_counted_day,
810
  title=meta.get("title"),
811
  )
812
  started = await self._start_agent_session(
 
1533
  agent_session.session.notification_destinations
1534
  ),
1535
  "auto_approval": self._auto_approval_summary(agent_session.session),
1536
+ "premium_user_billed": _premium_user_billed_today(agent_session),
1537
+ "premium_quota_counted": _quota_counted_today(agent_session),
 
 
1538
  }
1539
 
1540
  def set_notification_destinations(
 
1588
  else:
1589
  created_at_str = str(created_at or datetime.utcnow().isoformat())
1590
  pending = self._pending_docs_for_api(row.get("pending_approval") or [])
1591
+ quota_counted_today = (
1592
+ bool(row.get("claude_counted", False))
1593
+ and row.get("claude_counted_day") == _quota_day_today()
1594
+ )
1595
  results.append(
1596
  {
1597
  "session_id": str(sid),
 
1605
  "title": row.get("title"),
1606
  "premium_user_billed": bool(
1607
  row.get("premium_user_billed", False)
1608
+ and quota_counted_today
1609
  ),
1610
+ "premium_quota_counted": quota_counted_today,
1611
  "notification_destinations": row.get(
1612
  "notification_destinations"
1613
  )
backend/user_quotas.py CHANGED
@@ -7,10 +7,10 @@ in-process dict remains the fallback for local/dev/test runs.
7
  The public names still say ``claude`` because this quota bucket originally
8
  only covered Claude and the persisted session field uses that name.
9
 
10
- Unit: first premium-model submit in a session, not raw messages. A user who
11
- sends with an allowed premium model in a new session consumes one quota point;
12
- switching an already-counted session back to a premium model doesn't
13
- (`AgentSession.claude_counted` guards that). Model-level plan gates live in
14
  ``backend.routes.agent``; this module only tracks the per-plan daily cap.
15
 
16
  Cap tiers:
@@ -40,6 +40,11 @@ def _today() -> str:
40
  return datetime.now(UTC).date().isoformat()
41
 
42
 
 
 
 
 
 
43
  def daily_cap_for(plan: str | None) -> int:
44
  """Return the daily Claude-session cap for the given plan."""
45
  return CLAUDE_PRO_DAILY if plan == "pro" else CLAUDE_FREE_DAILY
 
7
  The public names still say ``claude`` because this quota bucket originally
8
  only covered Claude and the persisted session field uses that name.
9
 
10
+ Unit: first premium-model submit per session per UTC day, not raw messages. A
11
+ user who sends with an allowed premium model in any session consumes one quota
12
+ point for that day; continuing the same session on the same day doesn't
13
+ (`AgentSession.claude_counted_day` guards that). Model-level plan gates live in
14
  ``backend.routes.agent``; this module only tracks the per-plan daily cap.
15
 
16
  Cap tiers:
 
40
  return datetime.now(UTC).date().isoformat()
41
 
42
 
43
+ def current_quota_day() -> str:
44
+ """Return the UTC date key used for today's premium-model quota bucket."""
45
+ return _today()
46
+
47
+
48
  def daily_cap_for(plan: str | None) -> int:
49
  """Return the daily Claude-session cap for the given plan."""
50
  return CLAUDE_PRO_DAILY if plan == "pro" else CLAUDE_FREE_DAILY
frontend/src/components/SessionChat.tsx CHANGED
@@ -94,13 +94,13 @@ export default function SessionChat({ sessionId, isActive, onSessionDead }: Sess
94
  // account (past the subsidized daily allowance). Re-read after each turn,
95
  // since the backend flips it at submit time. Only premium-model sessions can
96
  // ever be user-billed, so skip the fetch for free models.
97
- const [premiumBilled, setPremiumBilled] = useState(false);
98
- const [premiumQuotaCounted, setPremiumQuotaCounted] = useState(false);
99
  const onPremiumModel = isPremiumPath(sessionMeta?.model ?? undefined);
100
  useEffect(() => {
101
  if (!isActive || !onPremiumModel) {
102
- setPremiumBilled(false);
103
- setPremiumQuotaCounted(false);
104
  return;
105
  }
106
  if (busy) return;
@@ -119,9 +119,9 @@ export default function SessionChat({ sessionId, isActive, onSessionDead }: Sess
119
  };
120
  }, [busy, isActive, onPremiumModel, sessionId]);
121
 
122
- const sessionPremiumBilled = premiumBilled || Boolean(sessionMeta?.premiumUserBilled);
123
  const sessionPremiumQuotaCounted =
124
- premiumQuotaCounted || Boolean(sessionMeta?.premiumQuotaCounted);
125
  const premiumBillingNotice =
126
  sessionPremiumBilled ||
127
  (isActive &&
 
94
  // account (past the subsidized daily allowance). Re-read after each turn,
95
  // since the backend flips it at submit time. Only premium-model sessions can
96
  // ever be user-billed, so skip the fetch for free models.
97
+ const [premiumBilled, setPremiumBilled] = useState<boolean | null>(null);
98
+ const [premiumQuotaCounted, setPremiumQuotaCounted] = useState<boolean | null>(null);
99
  const onPremiumModel = isPremiumPath(sessionMeta?.model ?? undefined);
100
  useEffect(() => {
101
  if (!isActive || !onPremiumModel) {
102
+ setPremiumBilled(null);
103
+ setPremiumQuotaCounted(null);
104
  return;
105
  }
106
  if (busy) return;
 
119
  };
120
  }, [busy, isActive, onPremiumModel, sessionId]);
121
 
122
+ const sessionPremiumBilled = premiumBilled ?? Boolean(sessionMeta?.premiumUserBilled);
123
  const sessionPremiumQuotaCounted =
124
+ premiumQuotaCounted ?? Boolean(sessionMeta?.premiumQuotaCounted);
125
  const premiumBillingNotice =
126
  sessionPremiumBilled ||
127
  (isActive &&
frontend/src/store/sessionStore.ts CHANGED
@@ -252,10 +252,14 @@ export const useSessionStore = create<SessionStore>()(
252
  {
253
  name: 'hf-agent-sessions',
254
  partialize: (state) => ({
255
- // Reset the transient isProcessing flag so a stale `true` from a
256
- // previous session can't trigger a reactivating hydration on the next
257
- // cold load — it's always re-derived from the live GET /sessions list.
258
- sessions: state.sessions.map((s) => ({ ...s, isProcessing: false })),
 
 
 
 
259
  activeSessionId: state.activeSessionId,
260
  }),
261
  }
 
252
  {
253
  name: 'hf-agent-sessions',
254
  partialize: (state) => ({
255
+ // Reset transient session flags so cold-load state is re-derived from
256
+ // the live GET /sessions list and current daily quota window.
257
+ sessions: state.sessions.map((s) => ({
258
+ ...s,
259
+ isProcessing: false,
260
+ premiumUserBilled: false,
261
+ premiumQuotaCounted: false,
262
+ })),
263
  activeSessionId: state.activeSessionId,
264
  }),
265
  }
tests/unit/test_agent_model_gating.py CHANGED
@@ -24,6 +24,7 @@ def _reset_quota_store():
24
  def _premium_session(model: str = agent.DEFAULT_PREMIUM_MODEL_ID):
25
  return SimpleNamespace(
26
  claude_counted=False,
 
27
  session=SimpleNamespace(
28
  config=SimpleNamespace(model_name=model),
29
  premium_user_billed=False,
@@ -198,11 +199,66 @@ async def test_premium_quota_charges_without_user_billing_inside_allowance(monke
198
  )
199
 
200
  assert agent_session.claude_counted is True
 
201
  assert agent_session.session.premium_user_billed is False
202
  assert persisted == [agent_session]
203
  assert await agent.user_quotas.get_claude_used_today("u1") == 1
204
 
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  @pytest.mark.asyncio
207
  async def test_free_user_gets_two_subsidized_premium_sessions_then_user_billing(
208
  monkeypatch,
@@ -227,6 +283,7 @@ async def test_free_user_gets_two_subsidized_premium_sessions_then_user_billing(
227
  third = _premium_session()
228
  await agent._enforce_premium_model_quota({"user_id": "g1", "plan": "free"}, third)
229
  assert third.session.premium_user_billed is True
 
230
  assert await agent.user_quotas.get_claude_used_today("g1") == 2
231
 
232
 
@@ -249,6 +306,7 @@ async def test_free_model_does_not_consume_premium_quota(monkeypatch):
249
  )
250
 
251
  assert agent_session.claude_counted is False
 
252
  assert await agent.user_quotas.get_claude_used_today("u1") == 0
253
 
254
 
@@ -274,6 +332,7 @@ async def test_free_user_cannot_spend_quota_on_pro_only_premium_model(monkeypatc
274
  assert exc_info.value.status_code == 403
275
  assert exc_info.value.detail["error"] == "model_requires_pro"
276
  assert agent_session.claude_counted is False
 
277
  assert await agent.user_quotas.get_claude_used_today("u1") == 0
278
 
279
 
@@ -321,6 +380,7 @@ async def test_pro_user_uses_pro_premium_quota(monkeypatch):
321
  agent_session,
322
  )
323
  assert agent_session.claude_counted is True
 
324
  assert agent_session.session.premium_user_billed is False
325
  assert await agent.user_quotas.get_claude_used_today("pro-user") == index + 1
326
 
 
24
  def _premium_session(model: str = agent.DEFAULT_PREMIUM_MODEL_ID):
25
  return SimpleNamespace(
26
  claude_counted=False,
27
+ claude_counted_day=None,
28
  session=SimpleNamespace(
29
  config=SimpleNamespace(model_name=model),
30
  premium_user_billed=False,
 
199
  )
200
 
201
  assert agent_session.claude_counted is True
202
+ assert agent_session.claude_counted_day == agent.user_quotas.current_quota_day()
203
  assert agent_session.session.premium_user_billed is False
204
  assert persisted == [agent_session]
205
  assert await agent.user_quotas.get_claude_used_today("u1") == 1
206
 
207
 
208
+ @pytest.mark.asyncio
209
+ async def test_premium_quota_counts_same_session_once_per_day(monkeypatch):
210
+ async def fake_persist_session_snapshot(_agent_session):
211
+ return None
212
+
213
+ monkeypatch.setattr(
214
+ agent.session_manager,
215
+ "persist_session_snapshot",
216
+ fake_persist_session_snapshot,
217
+ )
218
+
219
+ agent_session = _premium_session()
220
+
221
+ await agent._enforce_premium_model_quota(
222
+ {"user_id": "u1", "plan": "free"},
223
+ agent_session,
224
+ )
225
+ await agent._enforce_premium_model_quota(
226
+ {"user_id": "u1", "plan": "free"},
227
+ agent_session,
228
+ )
229
+
230
+ assert agent_session.claude_counted is True
231
+ assert agent_session.claude_counted_day == agent.user_quotas.current_quota_day()
232
+ assert await agent.user_quotas.get_claude_used_today("u1") == 1
233
+
234
+
235
+ @pytest.mark.asyncio
236
+ async def test_premium_quota_counts_stale_session_again_today(monkeypatch):
237
+ async def fake_persist_session_snapshot(_agent_session):
238
+ return None
239
+
240
+ monkeypatch.setattr(
241
+ agent.session_manager,
242
+ "persist_session_snapshot",
243
+ fake_persist_session_snapshot,
244
+ )
245
+
246
+ agent_session = _premium_session()
247
+ agent_session.claude_counted = True
248
+ agent_session.claude_counted_day = "2000-01-01"
249
+ agent_session.session.premium_user_billed = True
250
+
251
+ await agent._enforce_premium_model_quota(
252
+ {"user_id": "u1", "plan": "free"},
253
+ agent_session,
254
+ )
255
+
256
+ assert agent_session.claude_counted is True
257
+ assert agent_session.claude_counted_day == agent.user_quotas.current_quota_day()
258
+ assert agent_session.session.premium_user_billed is False
259
+ assert await agent.user_quotas.get_claude_used_today("u1") == 1
260
+
261
+
262
  @pytest.mark.asyncio
263
  async def test_free_user_gets_two_subsidized_premium_sessions_then_user_billing(
264
  monkeypatch,
 
283
  third = _premium_session()
284
  await agent._enforce_premium_model_quota({"user_id": "g1", "plan": "free"}, third)
285
  assert third.session.premium_user_billed is True
286
+ assert third.claude_counted_day == agent.user_quotas.current_quota_day()
287
  assert await agent.user_quotas.get_claude_used_today("g1") == 2
288
 
289
 
 
306
  )
307
 
308
  assert agent_session.claude_counted is False
309
+ assert agent_session.claude_counted_day is None
310
  assert await agent.user_quotas.get_claude_used_today("u1") == 0
311
 
312
 
 
332
  assert exc_info.value.status_code == 403
333
  assert exc_info.value.detail["error"] == "model_requires_pro"
334
  assert agent_session.claude_counted is False
335
+ assert agent_session.claude_counted_day is None
336
  assert await agent.user_quotas.get_claude_used_today("u1") == 0
337
 
338
 
 
380
  agent_session,
381
  )
382
  assert agent_session.claude_counted is True
383
+ assert agent_session.claude_counted_day == agent.user_quotas.current_quota_day()
384
  assert agent_session.session.premium_user_billed is False
385
  assert await agent.user_quotas.get_claude_used_today("pro-user") == index + 1
386
 
tests/unit/test_session_manager_persistence.py CHANGED
@@ -757,6 +757,7 @@ async def test_list_sessions_dev_uses_store_dev_visibility():
757
  "created_at": datetime.now(UTC),
758
  "premium_user_billed": True,
759
  "claude_counted": True,
 
760
  "auto_approval_enabled": True,
761
  "auto_approval_cost_cap_usd": 5.0,
762
  "auto_approval_estimated_spend_usd": 2.0,
@@ -786,3 +787,44 @@ async def test_list_sessions_dev_uses_store_dev_visibility():
786
  "estimated_spend_usd": 2.0,
787
  "remaining_usd": 3.0,
788
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
757
  "created_at": datetime.now(UTC),
758
  "premium_user_billed": True,
759
  "claude_counted": True,
760
+ "claude_counted_day": datetime.now(UTC).date().isoformat(),
761
  "auto_approval_enabled": True,
762
  "auto_approval_cost_cap_usd": 5.0,
763
  "auto_approval_estimated_spend_usd": 2.0,
 
787
  "estimated_spend_usd": 2.0,
788
  "remaining_usd": 3.0,
789
  }
790
+
791
+
792
+ def test_get_session_info_marks_stale_premium_quota_as_unused_today():
793
+ manager = _manager_with_store(NoopSessionStore())
794
+ agent_session = _runtime_agent_session("s1", user_id="alice")
795
+ agent_session.claude_counted = True
796
+ agent_session.claude_counted_day = "2000-01-01"
797
+ agent_session.session.premium_user_billed = True
798
+ manager.sessions["s1"] = agent_session
799
+
800
+ info = manager.get_session_info("s1")
801
+
802
+ assert info is not None
803
+ assert info["premium_user_billed"] is False
804
+ assert info["premium_quota_counted"] is False
805
+
806
+
807
+ @pytest.mark.asyncio
808
+ async def test_list_sessions_marks_stale_premium_quota_as_unused_today():
809
+ class ListStore(NoopSessionStore):
810
+ enabled = True
811
+
812
+ async def list_sessions(self, user_id: str, **_: Any) -> list[dict[str, Any]]:
813
+ return [
814
+ {
815
+ "session_id": "s1",
816
+ "user_id": user_id,
817
+ "model": "m",
818
+ "created_at": datetime.now(UTC),
819
+ "premium_user_billed": True,
820
+ "claude_counted": True,
821
+ "claude_counted_day": "2000-01-01",
822
+ }
823
+ ]
824
+
825
+ manager = _manager_with_store(ListStore())
826
+
827
+ sessions = await manager.list_sessions(user_id="alice")
828
+
829
+ assert sessions[0]["premium_user_billed"] is False
830
+ assert sessions[0]["premium_quota_counted"] is False