cctally 1.75.1 → 1.76.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.76.0] - 2026-07-20
9
+
10
+ ### Added
11
+ - `cctally doctor` now reports `db.reclaimable` when at least 25% of `cache.db` pages are free, with a direct `cctally db vacuum --db cache` remediation. (#315)
12
+
13
+ ### Fixed
14
+ - Codex dashboard Weekly cycles and `$ / 1%` accounting now keep the separately-metered GPT-5.3-Codex-Spark quota pool distinct from the standard seven-day Codex pool, preventing brief Spark sessions from creating phantom standard weeks.
15
+ - Dashboard Codex Weekly cost deltas now use the shared fractional-ratio contract instead of rendering 100× too large, and the Codex **$/1% Trend** table once again aligns Used%, $/1%, and vs-prior values under their matching columns.
16
+ - Claude dashboard quota now keeps updating automatically while Claude Code uses a bracketed context variant such as `opus[1m]`; context-window metadata no longer suppresses the valid account-wide 5-hour and 7-day observations supplied to the status line.
17
+ - Conversation-retention pruning now commits each whole Claude session or Codex conversation separately while retaining its maintenance locks, bounding first-prune WAL growth without exposing partially deleted conversations; a 9.37 GB production-shaped benchmark reduced peak WAL from 889.7 MiB to 28.8 MiB with identical deleted-row counts and FTS integrity. (#315)
18
+
8
19
  ## [1.75.1] - 2026-07-20
9
20
 
10
21
  ### Fixed
@@ -45,7 +45,7 @@ from _lib_quota import (
45
45
  quota_freshness,
46
46
  select_baseline,
47
47
  )
48
- from _lib_jsonl import CodexEntry
48
+ from _lib_jsonl import CodexEntry, codex_model_scoped_quota_pool
49
49
  from _lib_fmt import stable_sum
50
50
  from _lib_aggregators import _aggregate_codex_buckets
51
51
  from _lib_five_hour import _FIVE_HOUR_JITTER_FLOOR_SECONDS
@@ -103,6 +103,21 @@ class CodexWeeklyPeriod:
103
103
  used_percent: float | None = None
104
104
 
105
105
 
106
+ def _is_model_scoped_codex_quota(logical_limit_key: object) -> bool:
107
+ """Whether an interpreted native identity belongs outside standard quota."""
108
+ if not isinstance(logical_limit_key, str):
109
+ return False
110
+ try:
111
+ payload = json.loads(logical_limit_key)
112
+ except (json.JSONDecodeError, TypeError):
113
+ return False
114
+ return (
115
+ isinstance(payload, dict)
116
+ and isinstance(payload.get("modelPool"), str)
117
+ and bool(payload["modelPool"].strip())
118
+ )
119
+
120
+
106
121
  def _resolve_codex_weekly_cycle(
107
122
  observations: Iterable[object],
108
123
  now_utc: dt.datetime,
@@ -113,6 +128,8 @@ def _resolve_codex_weekly_cycle(
113
128
  for history in build_history(tuple(observations)):
114
129
  if history.identity.window_minutes != 10_080:
115
130
  continue
131
+ if _is_model_scoped_codex_quota(history.identity.logical_limit_key):
132
+ continue
116
133
  baseline = select_baseline(history.observations, now_utc)
117
134
  if baseline is None or baseline.resets_at <= now_utc:
118
135
  continue
@@ -161,7 +178,8 @@ def _codex_weekly_periods(
161
178
  placeholders = ",".join("?" for _ in roots)
162
179
  try:
163
180
  rows = stats_conn.execute(
164
- "SELECT source_root_key, resets_at_utc, nominal_start_at_utc, current_percent "
181
+ "SELECT source_root_key, logical_limit_key, resets_at_utc, "
182
+ "nominal_start_at_utc, current_percent "
165
183
  "FROM quota_window_blocks "
166
184
  "WHERE source='codex' AND window_minutes=10080 "
167
185
  f"AND source_root_key IN ({placeholders}) AND orphaned_at IS NULL "
@@ -174,7 +192,9 @@ def _codex_weekly_periods(
174
192
 
175
193
  raw_boundaries: list[tuple[dt.datetime, dt.datetime, set[str], list[float]]] = []
176
194
 
177
- for root_key, resets_at_raw, start_at_raw, current_percent in rows:
195
+ for root_key, logical_limit_key, resets_at_raw, start_at_raw, current_percent in rows:
196
+ if _is_model_scoped_codex_quota(logical_limit_key):
197
+ continue
178
198
  try:
179
199
  start_at = dt.datetime.fromisoformat(str(start_at_raw).replace("Z", "+00:00"))
180
200
  resets_at = dt.datetime.fromisoformat(str(resets_at_raw).replace("Z", "+00:00"))
@@ -1833,6 +1853,8 @@ def _build_codex_native_weekly_view(
1833
1853
  labels: dict[str, str] = {}
1834
1854
  periods_by_bucket: dict[str, CodexWeeklyPeriod] = {}
1835
1855
  for entry in entries:
1856
+ if codex_model_scoped_quota_pool(getattr(entry, "model", None)) is not None:
1857
+ continue
1836
1858
  timestamp = getattr(entry, "timestamp").astimezone(UTC)
1837
1859
  root_key = str(getattr(entry, "source_root_key", "") or "")
1838
1860
  period = next((
@@ -385,10 +385,21 @@ def doctor_gather_state(
385
385
 
386
386
  cache_entries_count = None
387
387
  cache_last_entry_at = None
388
+ cache_db_page_count = None
389
+ cache_db_freelist_count = None
388
390
  try:
389
391
  if _cctally_core.CACHE_DB_PATH.exists():
390
392
  conn = sqlite3.connect(str(_cctally_core.CACHE_DB_PATH))
391
393
  try:
394
+ try:
395
+ row = conn.execute("PRAGMA page_count").fetchone()
396
+ if row and row[0] is not None:
397
+ cache_db_page_count = int(row[0])
398
+ row = conn.execute("PRAGMA freelist_count").fetchone()
399
+ if row and row[0] is not None:
400
+ cache_db_freelist_count = int(row[0])
401
+ except sqlite3.Error:
402
+ pass
392
403
  row = conn.execute(
393
404
  "SELECT COUNT(*), MAX(timestamp_utc) FROM session_entries"
394
405
  ).fetchone()
@@ -863,6 +874,9 @@ def doctor_gather_state(
863
874
  locks_held=locks_held,
864
875
  # #297: cache.db WAL size backstop (gathered outside the deep branch).
865
876
  cache_db_wal_bytes=cache_db_wal_bytes,
877
+ # #315: read-only cache free-page evidence for the reclaim hint.
878
+ cache_db_page_count=cache_db_page_count,
879
+ cache_db_freelist_count=cache_db_freelist_count,
866
880
  codex_quota_windows=codex_quota_windows,
867
881
  codex_hook_roots=codex_hook_roots,
868
882
  codex_lifecycle_activity_24h=codex_lifecycle_activity_24h,
@@ -41,10 +41,12 @@ from _lib_quota import (
41
41
  source_path_key,
42
42
  )
43
43
  from _lib_json_envelope import stamp_schema_version
44
+ from _lib_jsonl import _codex_logical_limit_key, codex_model_scoped_quota_pool
44
45
 
45
46
 
46
47
  UTC = dt.timezone.utc
47
48
  _DASHBOARD_PROJECTION_CERTIFICATE_KEY = "codex_quota_projection_certificate"
49
+ _CODEX_QUOTA_INTERPRETATION_VERSION = 2
48
50
 
49
51
 
50
52
  @dataclass(frozen=True)
@@ -134,6 +136,11 @@ def load_codex_quota_projection_certificate(
134
136
  if row is None:
135
137
  return None
136
138
  payload = json.loads(str(row[0]))
139
+ if (
140
+ int(payload["interpretationVersion"])
141
+ != _CODEX_QUOTA_INTERPRETATION_VERSION
142
+ ):
143
+ return None
137
144
  sequence = int(payload["sequence"])
138
145
  signatures = {
139
146
  str(root_key): str(signature)
@@ -169,6 +176,7 @@ def _store_codex_quota_projection_certificate(
169
176
  conn.rollback()
170
177
  return
171
178
  payload = json.dumps({
179
+ "interpretationVersion": _CODEX_QUOTA_INTERPRETATION_VERSION,
172
180
  "sequence": sequence,
173
181
  "signatures": dict(sorted(signatures.items())),
174
182
  }, sort_keys=True, separators=(",", ":"))
@@ -262,14 +270,51 @@ def load_codex_quota_observations(
262
270
  previous_row_factory = conn.row_factory
263
271
  try:
264
272
  conn.row_factory = sqlite3.Row
273
+
274
+ def has_columns(table: str, required: set[str]) -> bool:
275
+ columns = {
276
+ str(row[1]) for row in conn.execute(
277
+ f"PRAGMA table_info({table})"
278
+ )
279
+ }
280
+ return required <= columns
281
+
282
+ has_conversation_events = has_columns(
283
+ "codex_conversation_events",
284
+ {"source_path", "line_offset", "record_type", "payload_json"},
285
+ )
286
+ has_session_entries = has_columns(
287
+ "codex_session_entries", {"source_path", "line_offset", "model"},
288
+ )
289
+ if has_conversation_events:
290
+ model_expr = """
291
+ (SELECT json_extract(events.payload_json, '$.payload.model')
292
+ FROM codex_conversation_events AS events
293
+ WHERE events.source_path=quota_window_snapshots.source_path
294
+ AND events.line_offset<=quota_window_snapshots.line_offset
295
+ AND events.record_type IN ('turn_context','session_meta')
296
+ AND json_type(events.payload_json, '$.payload.model')='text'
297
+ ORDER BY events.line_offset DESC LIMIT 1) AS observed_model
298
+ """
299
+ elif has_session_entries:
300
+ model_expr = """
301
+ (SELECT entries.model
302
+ FROM codex_session_entries AS entries
303
+ WHERE entries.source_path=quota_window_snapshots.source_path
304
+ AND entries.line_offset<=quota_window_snapshots.line_offset
305
+ ORDER BY entries.line_offset DESC LIMIT 1) AS observed_model
306
+ """
307
+ else:
308
+ model_expr = "NULL AS observed_model"
265
309
  sql = """
266
310
  SELECT source, source_root_key, source_path, line_offset,
267
311
  captured_at_utc, observed_slot, logical_limit_key, limit_id,
268
312
  limit_name, window_minutes, used_percent, resets_at_utc,
269
- plan_type, individual_limit_json, reached_type
313
+ plan_type, individual_limit_json, reached_type,
314
+ {model_expr}
270
315
  FROM quota_window_snapshots
271
316
  WHERE source='codex' AND source_root_key IS NOT NULL
272
- """
317
+ """.format(model_expr=model_expr)
273
318
  params: list[object] = []
274
319
  if requested is not None:
275
320
  if not requested:
@@ -317,10 +362,17 @@ def load_codex_quota_observations(
317
362
  if any(row[name] is None or not str(row[name]).strip() for name in required_text):
318
363
  continue
319
364
  try:
365
+ logical_limit_key = str(row["logical_limit_key"])
366
+ if codex_model_scoped_quota_pool(row["observed_model"]) is not None:
367
+ logical_limit_key = _codex_logical_limit_key(
368
+ str(row["source_root_key"]), row["limit_id"],
369
+ str(row["observed_slot"]), int(row["window_minutes"]),
370
+ str(row["observed_model"]),
371
+ )
320
372
  identity = QuotaWindowIdentity(
321
373
  source=str(row["source"]),
322
374
  source_root_key=str(row["source_root_key"]),
323
- logical_limit_key=str(row["logical_limit_key"]),
375
+ logical_limit_key=logical_limit_key,
324
376
  observed_slot=str(row["observed_slot"]),
325
377
  window_minutes=int(row["window_minutes"]),
326
378
  limit_id=row["limit_id"],
@@ -1205,8 +1205,6 @@ def _fork_persist(parent_lock_fd: int) -> None:
1205
1205
 
1206
1206
  def _statusline_persist(parsed, *, sync_for_test: bool = False) -> None:
1207
1207
  """Spool an eligible session candidate then reduce it opportunistically."""
1208
- if _lib_statusline.is_alternate_pool_model_id(parsed.model_id):
1209
- return
1210
1208
  candidate = _candidate_from_input(parsed, received_at=int(time.time()))
1211
1209
  if candidate is None:
1212
1210
  return
@@ -25,14 +25,16 @@ The FTS5 indexes over ``conversation_messages`` (``conversation_fts``) and
25
25
  ``conversation_ai_titles`` (``conversation_title_fts``) are external-content and
26
26
  maintained by AFTER-DELETE triggers, which are logically correct on subset
27
27
  deletes. Whole groups are deleted so those triggers keep the index consistent.
28
- The kernel runs inside the caller's open transaction (the orchestrator owns the
29
- flocks, ``BEGIN IMMEDIATE``, and commit).
28
+ The kernel normally runs inside the caller's open transaction. The orchestrator
29
+ supplies a post-group boundary that commits each whole conversation separately
30
+ while retaining every flock for the full pass (#315), bounding WAL growth.
30
31
  """
31
32
  from __future__ import annotations
32
33
 
33
34
  import datetime as dt
34
35
  import fcntl
35
36
  import sqlite3
37
+ from collections.abc import Callable
36
38
  from dataclasses import dataclass
37
39
 
38
40
  import _cctally_core
@@ -71,12 +73,25 @@ def _cutoff_iso(cutoff_utc: dt.datetime) -> str:
71
73
 
72
74
 
73
75
  def prune_conversation_transcripts(
74
- conn: sqlite3.Connection, *, cutoff_utc: dt.datetime
76
+ conn: sqlite3.Connection,
77
+ *,
78
+ cutoff_utc: dt.datetime,
79
+ after_group: "Callable[[], None] | None" = None,
75
80
  ) -> PruneStats:
76
- """Prune every transcript group whose latest activity is before ``cutoff_utc``."""
81
+ """Prune every transcript group whose latest activity is before ``cutoff_utc``.
82
+
83
+ ``after_group`` runs only after every table and FTS posting owned by one
84
+ session/conversation has been deleted. Direct kernel callers leave it unset
85
+ and retain their caller-managed transaction; the orchestrator uses it for
86
+ #315's whole-conversation intermediate commits.
87
+ """
77
88
  cutoff = _cutoff_iso(cutoff_utc)
78
- claude_sessions, claude_messages = _prune_claude(conn, cutoff)
79
- codex_conversations, codex_events = _prune_codex(conn, cutoff)
89
+ claude_sessions, claude_messages = _prune_claude(
90
+ conn, cutoff, after_group=after_group
91
+ )
92
+ codex_conversations, codex_events = _prune_codex(
93
+ conn, cutoff, after_group=after_group
94
+ )
80
95
  return PruneStats(
81
96
  claude_sessions=claude_sessions,
82
97
  claude_messages=claude_messages,
@@ -116,7 +131,12 @@ def _prunable_null_identity_paths(
116
131
  return [row[0] for row in conn.execute(sql, (cutoff,))]
117
132
 
118
133
 
119
- def _prune_claude(conn: sqlite3.Connection, cutoff: str) -> tuple[int, int]:
134
+ def _prune_claude(
135
+ conn: sqlite3.Connection,
136
+ cutoff: str,
137
+ *,
138
+ after_group: "Callable[[], None] | None" = None,
139
+ ) -> tuple[int, int]:
120
140
  sessions = 0
121
141
  messages = 0
122
142
  for session_id in _prunable_groups(
@@ -140,6 +160,8 @@ def _prune_claude(conn: sqlite3.Connection, cutoff: str) -> tuple[int, int]:
140
160
  (session_id,),
141
161
  )
142
162
  sessions += 1
163
+ if after_group is not None:
164
+ after_group()
143
165
  for source_path in _prunable_null_identity_paths(
144
166
  conn, "conversation_messages", "session_id", cutoff
145
167
  ):
@@ -166,6 +188,8 @@ def _prune_claude(conn: sqlite3.Connection, cutoff: str) -> tuple[int, int]:
166
188
  )
167
189
  messages += cur.rowcount if cur.rowcount and cur.rowcount > 0 else 0
168
190
  sessions += 1
191
+ if after_group is not None:
192
+ after_group()
169
193
  return sessions, messages
170
194
 
171
195
 
@@ -235,9 +259,11 @@ def _maybe_prune_conversation_retention(
235
259
  two provider flocks are taken in a FIXED order (Claude then Codex),
236
260
  non-blocking, so a rebuild/reingest mid-flight makes the prune skip this
237
261
  cycle rather than race between candidate selection and deletion. The prune of
238
- both providers and the throttle stamp run in ONE ``BEGIN IMMEDIATE``
239
- transaction, so the stamp is written only after both provider phases succeed;
240
- any failure rolls back the whole thing (no stamp retried next cycle).
262
+ each whole session/conversation runs in its own ``BEGIN IMMEDIATE``
263
+ transaction (#315), while every flock remains held for the full pass. The
264
+ throttle stamp is committed only after both provider phases succeed. A
265
+ failure rolls back the active group but preserves completed groups, writes no
266
+ stamp, and therefore retries the remainder next cycle.
241
267
 
242
268
  ``conn`` must hold no provider flock and no open transaction (the caller
243
269
  guarantees this — the dashboard opens a dedicated cache connection; the
@@ -274,7 +300,15 @@ def _maybe_prune_conversation_retention(
274
300
  cutoff = now_utc - dt.timedelta(days=int(retention_days))
275
301
  conn.execute("BEGIN IMMEDIATE")
276
302
  try:
277
- stats = prune_conversation_transcripts(conn, cutoff_utc=cutoff)
303
+ def commit_group() -> None:
304
+ conn.commit()
305
+ conn.execute("BEGIN IMMEDIATE")
306
+
307
+ stats = prune_conversation_transcripts(
308
+ conn,
309
+ cutoff_utc=cutoff,
310
+ after_group=commit_group,
311
+ )
278
312
  _stamp_retention_prune(conn, now_utc)
279
313
  conn.commit()
280
314
  except Exception:
@@ -354,7 +388,12 @@ def _delete_codex_conversation_derived(
354
388
  )
355
389
 
356
390
 
357
- def _prune_codex(conn: sqlite3.Connection, cutoff: str) -> tuple[int, int]:
391
+ def _prune_codex(
392
+ conn: sqlite3.Connection,
393
+ cutoff: str,
394
+ *,
395
+ after_group: "Callable[[], None] | None" = None,
396
+ ) -> tuple[int, int]:
358
397
  conversations = 0
359
398
  events = 0
360
399
  for conversation_key in _prunable_groups(
@@ -369,6 +408,8 @@ def _prune_codex(conn: sqlite3.Connection, cutoff: str) -> tuple[int, int]:
369
408
  )
370
409
  events += cur.rowcount if cur.rowcount and cur.rowcount > 0 else 0
371
410
  conversations += 1
411
+ if after_group is not None:
412
+ after_group()
372
413
  for source_path in _prunable_null_identity_paths(
373
414
  conn, "codex_conversation_events", "conversation_key", cutoff
374
415
  ):
@@ -381,4 +422,6 @@ def _prune_codex(conn: sqlite3.Connection, cutoff: str) -> tuple[int, int]:
381
422
  )
382
423
  events += cur.rowcount if cur.rowcount and cur.rowcount > 0 else 0
383
424
  conversations += 1
425
+ if after_group is not None:
426
+ after_group()
384
427
  return conversations, events
@@ -200,6 +200,12 @@ class DoctorState:
200
200
  # DOCTOR_WAL_WARN_BYTES (2x the WAL cap) — only when the journal_size_limit
201
201
  # + forced-checkpoint machinery has genuinely failed to contain the WAL.
202
202
  cache_db_wal_bytes: Optional[int] = None
203
+ # #315: read-only PRAGMA page_count/freelist_count evidence. The pure
204
+ # db.reclaimable check warns when free pages reach 25% of cache.db and
205
+ # points at the already-guarded explicit vacuum command. None means the
206
+ # cache was absent or unreadable; the check degrades to OK.
207
+ cache_db_page_count: Optional[int] = None
208
+ cache_db_freelist_count: Optional[int] = None
203
209
  # #294 S2: root-qualified physical Codex quota freshness, per-root native
204
210
  # hook state, and lifecycle activity are gathered by _cctally_doctor.
205
211
  codex_quota_windows: Optional[list[dict]] = None
@@ -1725,6 +1731,48 @@ def _check_db_wal_size(s: DoctorState) -> CheckResult:
1725
1731
  )
1726
1732
 
1727
1733
 
1734
+ # #315: conservative advisory threshold. A quarter of cache.db being free is
1735
+ # large enough to make an explicit, guarded VACUUM useful without nagging for
1736
+ # ordinary page churn. This is a ratio, so it remains page-size independent.
1737
+ DOCTOR_RECLAIMABLE_WARN_RATIO = 0.25
1738
+
1739
+
1740
+ def _check_db_reclaimable(s: DoctorState) -> CheckResult:
1741
+ """Surface cache free pages without mutating or auto-vacuuming the DB."""
1742
+ page_count = s.cache_db_page_count
1743
+ freelist_count = s.cache_db_freelist_count
1744
+ ratio = None
1745
+ if (
1746
+ isinstance(page_count, int)
1747
+ and not isinstance(page_count, bool)
1748
+ and page_count > 0
1749
+ and isinstance(freelist_count, int)
1750
+ and not isinstance(freelist_count, bool)
1751
+ and 0 <= freelist_count <= page_count
1752
+ ):
1753
+ ratio = freelist_count / page_count
1754
+ details = {
1755
+ "cache_db_page_count": page_count,
1756
+ "cache_db_freelist_count": freelist_count,
1757
+ "cache_db_free_ratio": ratio,
1758
+ "warn_ratio": DOCTOR_RECLAIMABLE_WARN_RATIO,
1759
+ }
1760
+ if ratio is not None and ratio >= DOCTOR_RECLAIMABLE_WARN_RATIO:
1761
+ return CheckResult(
1762
+ id="db.reclaimable", title="Reclaimable cache space",
1763
+ severity="warn",
1764
+ summary=f"high — {ratio * 100:.1f}% of cache.db pages are free",
1765
+ remediation=(
1766
+ "Run `cctally db vacuum --db cache` to reclaim disk space."
1767
+ ),
1768
+ details=details,
1769
+ )
1770
+ return CheckResult(
1771
+ id="db.reclaimable", title="Reclaimable cache space", severity="ok",
1772
+ summary="below threshold", remediation=None, details=details,
1773
+ )
1774
+
1775
+
1728
1776
  # Each entry is (category_id, category_title, ((check_id, evaluator_fn_name), ...)).
1729
1777
  # The dotted check_id is the stable JSON-contract ID (spec §5.2) AND the
1730
1778
  # fingerprint identity-slice key (spec §5.5). When an evaluator raises,
@@ -1759,6 +1807,7 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
1759
1807
  ("db.migrations.pending", "_check_db_migrations_pending"),
1760
1808
  ("db.lock_state", "_check_db_lock_state"),
1761
1809
  ("db.wal_size", "_check_db_wal_size"),
1810
+ ("db.reclaimable", "_check_db_reclaimable"),
1762
1811
  )),
1763
1812
  ("data", "Data", (
1764
1813
  ("data.latest_snapshot_age", "_check_data_latest_snapshot_age"),
package/bin/_lib_jsonl.py CHANGED
@@ -584,20 +584,37 @@ def _first_valid_reset_at(
584
584
 
585
585
  def _codex_logical_limit_key(
586
586
  source_root_key: str | None, limit_id: str | None, observed_slot: str,
587
- window_minutes: int,
587
+ window_minutes: int, model: str | None = None,
588
588
  ) -> str:
589
- return _codex_canonical_json({
589
+ payload = {
590
590
  "limitId": limit_id,
591
591
  "observedSlot": observed_slot,
592
592
  "source": "codex",
593
593
  "sourceRootKey": source_root_key,
594
594
  "windowMinutes": window_minutes,
595
- })
595
+ }
596
+ if (model_pool := codex_model_scoped_quota_pool(model)) is not None:
597
+ payload["modelPool"] = model_pool
598
+ return _codex_canonical_json(payload)
599
+
600
+
601
+ def codex_model_scoped_quota_pool(model: object) -> str | None:
602
+ """Return the native model pool when Codex documents it as separate.
603
+
604
+ GPT Codex Spark runs against its own allowance and does not consume the
605
+ standard Codex quota. Native payloads currently reuse ``limit_id=codex``
606
+ and the same slot/duration as the standard pool, so the sticky rollout
607
+ model is the only retained discriminator.
608
+ """
609
+ if not isinstance(model, str):
610
+ return None
611
+ normalized = model.strip().lower()
612
+ return normalized if "-codex-spark" in normalized else None
596
613
 
597
614
 
598
615
  def _codex_quota_observations(
599
616
  obj: dict[str, Any], payload: dict[str, Any], path_str: str, line_offset: int,
600
- source_root_key: str | None,
617
+ source_root_key: str | None, model: str | None,
601
618
  ) -> tuple[CodexQuotaObservation, ...]:
602
619
  captured_at = _parse_codex_timestamp(obj.get("timestamp"))
603
620
  if captured_at is None:
@@ -638,7 +655,7 @@ def _codex_quota_observations(
638
655
  captured_at_utc=_format_codex_timestamp(captured_at),
639
656
  observed_slot=slot,
640
657
  logical_limit_key=_codex_logical_limit_key(
641
- source_root_key, limit_id, slot, window_minutes
658
+ source_root_key, limit_id, slot, window_minutes, model
642
659
  ),
643
660
  limit_id=limit_id,
644
661
  limit_name=_first_valid_string(
@@ -867,7 +884,7 @@ def _iter_codex_fused_records_with_offsets(
867
884
  filename_session_id_warned,
868
885
  )
869
886
  quotas = _codex_quota_observations(
870
- obj, payload, path_str, line_offset, source_root_key
887
+ obj, payload, path_str, line_offset, source_root_key, state.model
871
888
  )
872
889
  yield CodexFusedEmission(
873
890
  line_offset=line_offset,
@@ -191,32 +191,6 @@ def parse_statusline_stdin(raw: "bytes | str") -> "StatuslineInput | ParseError"
191
191
  )
192
192
 
193
193
 
194
- # ---- Pool-identity guard (persist-only; spec 2026-07-17 #311 D1) ----------
195
-
196
-
197
- _ALTERNATE_POOL_MODEL_ID_RE = re.compile(r"\[[^\]]+\]$")
198
-
199
-
200
- def is_alternate_pool_model_id(model_id) -> bool:
201
- """True iff ``model_id`` is a bracketed variant id (e.g.
202
- ``claude-opus-4-8[1m]``) that reports a SEPARATE rate-limit pool.
203
-
204
- Such a session's stdin ``rate_limits`` describes a DIFFERENT usage pool
205
- on the same account; persisting it poisons the default-pool DB (the 7d
206
- HWM clamp latches the foreign high value and dedup then freezes tracking
207
- — see the #311 spec). The persist feeder skips it.
208
-
209
- Matches ANY trailing bracket suffix, not just the literal ``[1m]``: a
210
- future variant that turns out to share the default pool would merely lose
211
- one redundant writer (fail-safe toward data purity), whereas matching
212
- only ``[1m]`` would let the next variant poison the DB again. Missing /
213
- ``None`` / non-string / no-suffix ids return ``False`` (persist proceeds —
214
- only a positive variant match skips). Never raises."""
215
- if not isinstance(model_id, str) or not model_id:
216
- return False
217
- return _ALTERNATE_POOL_MODEL_ID_RE.search(model_id) is not None
218
-
219
-
220
194
  # ---- Segment 1: model -----------------------------------------------------
221
195
 
222
196