cctally 1.75.1 → 1.77.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 +22 -0
- package/bin/_cctally_cache.py +918 -143
- package/bin/_cctally_core.py +12 -1
- package/bin/_cctally_dashboard.py +49 -13
- package/bin/_cctally_dashboard_conversation.py +40 -17
- package/bin/_cctally_dashboard_sources.py +43 -10
- package/bin/_cctally_db.py +207 -5
- package/bin/_cctally_doctor.py +51 -7
- package/bin/_cctally_parser.py +1 -1
- package/bin/_cctally_quota.py +54 -3
- package/bin/_cctally_statusline.py +0 -2
- package/bin/_cctally_transcript.py +4 -4
- package/bin/_cctally_tui.py +6 -34
- package/bin/_lib_codex_conversation_query.py +19 -3
- package/bin/_lib_conversation_query.py +8 -3
- package/bin/_lib_conversation_retention.py +58 -15
- package/bin/_lib_doctor.py +102 -4
- package/bin/_lib_jsonl.py +23 -6
- package/bin/_lib_statusline.py +0 -26
- package/bin/cctally +3 -0
- package/dashboard/static/assets/index-CvcCeA4L.css +1 -0
- package/dashboard/static/assets/index-DLNXAevv.js +87 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
- package/dashboard/static/assets/index-fZRxsSf5.js +0 -80
- package/dashboard/static/assets/index-yftBNnLR.css +0 -1
|
@@ -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
|
|
29
|
-
|
|
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,
|
|
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(
|
|
79
|
-
|
|
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(
|
|
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
|
-
|
|
239
|
-
transaction,
|
|
240
|
-
|
|
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
|
|
@@ -251,7 +277,7 @@ def _maybe_prune_conversation_retention(
|
|
|
251
277
|
core.APP_DIR.mkdir(parents=True, exist_ok=True)
|
|
252
278
|
except OSError:
|
|
253
279
|
return None
|
|
254
|
-
maint_fh = open(core.
|
|
280
|
+
maint_fh = open(core.CONVERSATIONS_LOCK_MAINTENANCE_PATH, "w")
|
|
255
281
|
try:
|
|
256
282
|
try:
|
|
257
283
|
fcntl.flock(maint_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
@@ -259,13 +285,13 @@ def _maybe_prune_conversation_retention(
|
|
|
259
285
|
return None # another prune holds it; skip cleanly, do NOT stamp
|
|
260
286
|
if not force and not _retention_due(conn, now_utc):
|
|
261
287
|
return None
|
|
262
|
-
claude_fh = open(core.
|
|
288
|
+
claude_fh = open(core.CONVERSATIONS_LOCK_PATH, "w")
|
|
263
289
|
try:
|
|
264
290
|
try:
|
|
265
291
|
fcntl.flock(claude_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
266
292
|
except (BlockingIOError, OSError):
|
|
267
293
|
return None # a Claude sync is mid-flight; retry next cycle
|
|
268
|
-
codex_fh = open(core.
|
|
294
|
+
codex_fh = open(core.CONVERSATIONS_LOCK_CODEX_PATH, "w")
|
|
269
295
|
try:
|
|
270
296
|
try:
|
|
271
297
|
fcntl.flock(codex_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
package/bin/_lib_doctor.py
CHANGED
|
@@ -200,6 +200,16 @@ 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
|
|
209
|
+
# #320: the independently large transcript store needs the same reclaim
|
|
210
|
+
# visibility, with remediation targeted at its own VACUUM surface.
|
|
211
|
+
conversations_db_page_count: Optional[int] = None
|
|
212
|
+
conversations_db_freelist_count: Optional[int] = None
|
|
203
213
|
# #294 S2: root-qualified physical Codex quota freshness, per-root native
|
|
204
214
|
# hook state, and lifecycle activity are gathered by _cctally_doctor.
|
|
205
215
|
codex_quota_windows: Optional[list[dict]] = None
|
|
@@ -1229,21 +1239,22 @@ def _check_data_conversation_sessions_rollup(s: DoctorState) -> CheckResult:
|
|
|
1229
1239
|
row per distinct ``conversation_messages.session_id`` (#217 S1 / U9).
|
|
1230
1240
|
|
|
1231
1241
|
OK when the two counts are equal, when either is ``None`` (pre-rollup or an
|
|
1232
|
-
unreadable
|
|
1242
|
+
unreadable conversations.db — consistent with the kernel's graceful-degrade
|
|
1243
|
+
posture),
|
|
1233
1244
|
OR when a sync/reingest/backfill is in progress. WARN ONLY on a mismatch
|
|
1234
1245
|
observed in a QUIESCENT cache.
|
|
1235
1246
|
|
|
1236
|
-
False-WARN avoidance (Codex P2): ``
|
|
1247
|
+
False-WARN avoidance (Codex P2): ``sync_claude_conversations`` commits
|
|
1237
1248
|
``conversation_messages`` per file *before* the ``conversation_sessions``
|
|
1238
1249
|
recompute, and resumable reingest commits per file before its rollup
|
|
1239
1250
|
completes — so an unsynchronized read can transiently mismatch. The
|
|
1240
1251
|
in-progress signal (``conv_rollup_sync_in_progress``) is set by
|
|
1241
|
-
``doctor_gather_state`` from a NON-BLOCKING ``
|
|
1252
|
+
``doctor_gather_state`` from a NON-BLOCKING ``conversations.db.lock`` flock probe
|
|
1242
1253
|
(lock held ⇒ a writer is mid-flight) AND the presence of any pending
|
|
1243
1254
|
``cache_meta`` reingest/split/backfill flag; if either says in-progress, this
|
|
1244
1255
|
stays OK. Doctor remains read-only and never blocks on the lock.
|
|
1245
1256
|
|
|
1246
|
-
Informational only (no remediation): the next
|
|
1257
|
+
Informational only (no remediation): the next conversation sync re-derives
|
|
1247
1258
|
the rollup via its incremental DELETE+INSERT.
|
|
1248
1259
|
"""
|
|
1249
1260
|
rollup = s.conv_sessions_rollup_count
|
|
@@ -1725,6 +1736,91 @@ def _check_db_wal_size(s: DoctorState) -> CheckResult:
|
|
|
1725
1736
|
)
|
|
1726
1737
|
|
|
1727
1738
|
|
|
1739
|
+
# #315: conservative advisory threshold. A quarter of cache.db being free is
|
|
1740
|
+
# large enough to make an explicit, guarded VACUUM useful without nagging for
|
|
1741
|
+
# ordinary page churn. This is a ratio, so it remains page-size independent.
|
|
1742
|
+
DOCTOR_RECLAIMABLE_WARN_RATIO = 0.25
|
|
1743
|
+
|
|
1744
|
+
|
|
1745
|
+
def _check_db_reclaimable(s: DoctorState) -> CheckResult:
|
|
1746
|
+
"""Surface cache free pages without mutating or auto-vacuuming the DB."""
|
|
1747
|
+
page_count = s.cache_db_page_count
|
|
1748
|
+
freelist_count = s.cache_db_freelist_count
|
|
1749
|
+
ratio = None
|
|
1750
|
+
if (
|
|
1751
|
+
isinstance(page_count, int)
|
|
1752
|
+
and not isinstance(page_count, bool)
|
|
1753
|
+
and page_count > 0
|
|
1754
|
+
and isinstance(freelist_count, int)
|
|
1755
|
+
and not isinstance(freelist_count, bool)
|
|
1756
|
+
and 0 <= freelist_count <= page_count
|
|
1757
|
+
):
|
|
1758
|
+
ratio = freelist_count / page_count
|
|
1759
|
+
details = {
|
|
1760
|
+
"cache_db_page_count": page_count,
|
|
1761
|
+
"cache_db_freelist_count": freelist_count,
|
|
1762
|
+
"cache_db_free_ratio": ratio,
|
|
1763
|
+
"warn_ratio": DOCTOR_RECLAIMABLE_WARN_RATIO,
|
|
1764
|
+
}
|
|
1765
|
+
if ratio is not None and ratio >= DOCTOR_RECLAIMABLE_WARN_RATIO:
|
|
1766
|
+
return CheckResult(
|
|
1767
|
+
id="db.reclaimable", title="Reclaimable cache space",
|
|
1768
|
+
severity="warn",
|
|
1769
|
+
summary=f"high — {ratio * 100:.1f}% of cache.db pages are free",
|
|
1770
|
+
remediation=(
|
|
1771
|
+
"Run `cctally db vacuum --db cache` to reclaim disk space."
|
|
1772
|
+
),
|
|
1773
|
+
details=details,
|
|
1774
|
+
)
|
|
1775
|
+
return CheckResult(
|
|
1776
|
+
id="db.reclaimable", title="Reclaimable cache space", severity="ok",
|
|
1777
|
+
summary="below threshold", remediation=None, details=details,
|
|
1778
|
+
)
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def _check_db_conversations_reclaimable(s: DoctorState) -> CheckResult:
|
|
1782
|
+
"""Surface transcript-store free pages without mutating the DB (#320)."""
|
|
1783
|
+
page_count = s.conversations_db_page_count
|
|
1784
|
+
freelist_count = s.conversations_db_freelist_count
|
|
1785
|
+
ratio = None
|
|
1786
|
+
if (
|
|
1787
|
+
isinstance(page_count, int)
|
|
1788
|
+
and not isinstance(page_count, bool)
|
|
1789
|
+
and page_count > 0
|
|
1790
|
+
and isinstance(freelist_count, int)
|
|
1791
|
+
and not isinstance(freelist_count, bool)
|
|
1792
|
+
and 0 <= freelist_count <= page_count
|
|
1793
|
+
):
|
|
1794
|
+
ratio = freelist_count / page_count
|
|
1795
|
+
details = {
|
|
1796
|
+
"conversations_db_page_count": page_count,
|
|
1797
|
+
"conversations_db_freelist_count": freelist_count,
|
|
1798
|
+
"conversations_db_free_ratio": ratio,
|
|
1799
|
+
"warn_ratio": DOCTOR_RECLAIMABLE_WARN_RATIO,
|
|
1800
|
+
}
|
|
1801
|
+
if ratio is not None and ratio >= DOCTOR_RECLAIMABLE_WARN_RATIO:
|
|
1802
|
+
return CheckResult(
|
|
1803
|
+
id="db.conversations_reclaimable",
|
|
1804
|
+
title="Reclaimable transcript space",
|
|
1805
|
+
severity="warn",
|
|
1806
|
+
summary=(
|
|
1807
|
+
f"high — {ratio * 100:.1f}% of conversations.db pages are free"
|
|
1808
|
+
),
|
|
1809
|
+
remediation=(
|
|
1810
|
+
"Run `cctally db vacuum --db conversations` to reclaim disk space."
|
|
1811
|
+
),
|
|
1812
|
+
details=details,
|
|
1813
|
+
)
|
|
1814
|
+
return CheckResult(
|
|
1815
|
+
id="db.conversations_reclaimable",
|
|
1816
|
+
title="Reclaimable transcript space",
|
|
1817
|
+
severity="ok",
|
|
1818
|
+
summary="below threshold",
|
|
1819
|
+
remediation=None,
|
|
1820
|
+
details=details,
|
|
1821
|
+
)
|
|
1822
|
+
|
|
1823
|
+
|
|
1728
1824
|
# Each entry is (category_id, category_title, ((check_id, evaluator_fn_name), ...)).
|
|
1729
1825
|
# The dotted check_id is the stable JSON-contract ID (spec §5.2) AND the
|
|
1730
1826
|
# fingerprint identity-slice key (spec §5.5). When an evaluator raises,
|
|
@@ -1759,6 +1855,8 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
|
|
|
1759
1855
|
("db.migrations.pending", "_check_db_migrations_pending"),
|
|
1760
1856
|
("db.lock_state", "_check_db_lock_state"),
|
|
1761
1857
|
("db.wal_size", "_check_db_wal_size"),
|
|
1858
|
+
("db.reclaimable", "_check_db_reclaimable"),
|
|
1859
|
+
("db.conversations_reclaimable", "_check_db_conversations_reclaimable"),
|
|
1762
1860
|
)),
|
|
1763
1861
|
("data", "Data", (
|
|
1764
1862
|
("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
|
-
|
|
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,
|
package/bin/_lib_statusline.py
CHANGED
|
@@ -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
|
|
package/bin/cctally
CHANGED
|
@@ -1008,6 +1008,9 @@ get_codex_entries = _cctally_cache.get_codex_entries
|
|
|
1008
1008
|
_sum_codex_cost_for_range = _cctally_cache._sum_codex_cost_for_range
|
|
1009
1009
|
get_entries = _cctally_cache.get_entries
|
|
1010
1010
|
open_cache_db = _cctally_cache.open_cache_db
|
|
1011
|
+
open_conversations_db = _cctally_cache.open_conversations_db
|
|
1012
|
+
sync_claude_conversations = _cctally_cache.sync_claude_conversations
|
|
1013
|
+
sync_codex_conversations = _cctally_cache.sync_codex_conversations
|
|
1011
1014
|
_reset_orphan_warning_throttle = _cctally_cache._reset_orphan_warning_throttle
|
|
1012
1015
|
cmd_cache_sync = _cctally_cache.cmd_cache_sync
|
|
1013
1016
|
|