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
package/bin/_cctally_core.py
CHANGED
|
@@ -60,8 +60,10 @@ def _init_paths_from_env() -> None:
|
|
|
60
60
|
`import _cctally_core`).
|
|
61
61
|
"""
|
|
62
62
|
global APP_DIR, LEGACY_APP_DIR, LOG_DIR, DEV_MODE
|
|
63
|
-
global DB_PATH, CACHE_DB_PATH
|
|
63
|
+
global DB_PATH, CACHE_DB_PATH, CONVERSATIONS_DB_PATH
|
|
64
64
|
global CACHE_LOCK_PATH, CACHE_LOCK_CODEX_PATH, CACHE_LOCK_MAINTENANCE_PATH
|
|
65
|
+
global CONVERSATIONS_LOCK_PATH, CONVERSATIONS_LOCK_CODEX_PATH
|
|
66
|
+
global CONVERSATIONS_LOCK_MAINTENANCE_PATH
|
|
65
67
|
global CONFIG_LOCK_PATH
|
|
66
68
|
global CONFIG_PATH, MIGRATION_ERROR_LOG_PATH, CHANGELOG_PATH
|
|
67
69
|
global HOOK_TICK_LOG_DIR, HOOK_TICK_LOG_PATH, HOOK_TICK_LOG_ROTATED_PATH
|
|
@@ -101,6 +103,7 @@ def _init_paths_from_env() -> None:
|
|
|
101
103
|
|
|
102
104
|
DB_PATH = APP_DIR / "stats.db"
|
|
103
105
|
CACHE_DB_PATH = APP_DIR / "cache.db"
|
|
106
|
+
CONVERSATIONS_DB_PATH = APP_DIR / "conversations.db"
|
|
104
107
|
|
|
105
108
|
CACHE_LOCK_PATH = APP_DIR / "cache.db.lock"
|
|
106
109
|
CACHE_LOCK_CODEX_PATH = APP_DIR / "cache.db.codex.lock"
|
|
@@ -108,6 +111,14 @@ def _init_paths_from_env() -> None:
|
|
|
108
111
|
# retention prune across processes, held ABOVE the two provider flocks so a
|
|
109
112
|
# rebuild/reingest cannot land between candidate selection and deletion.
|
|
110
113
|
CACHE_LOCK_MAINTENANCE_PATH = APP_DIR / "cache.db.maintenance.lock"
|
|
114
|
+
# #320: transcript ingest and maintenance are physically independent from
|
|
115
|
+
# the latency-sensitive token/quota cache. Never reuse the cache.db flock
|
|
116
|
+
# namespace for conversations.db writes.
|
|
117
|
+
CONVERSATIONS_LOCK_PATH = APP_DIR / "conversations.db.lock"
|
|
118
|
+
CONVERSATIONS_LOCK_CODEX_PATH = APP_DIR / "conversations.db.codex.lock"
|
|
119
|
+
CONVERSATIONS_LOCK_MAINTENANCE_PATH = (
|
|
120
|
+
APP_DIR / "conversations.db.maintenance.lock"
|
|
121
|
+
)
|
|
111
122
|
CONFIG_LOCK_PATH = APP_DIR / "config.json.lock"
|
|
112
123
|
|
|
113
124
|
CONFIG_PATH = APP_DIR / "config.json"
|
|
@@ -359,7 +359,9 @@ import _lib_log
|
|
|
359
359
|
from _cctally_config import save_config, _load_config_unlocked
|
|
360
360
|
from _cctally_db import _render_migration_error_banner
|
|
361
361
|
from _cctally_cache import (
|
|
362
|
-
get_entries, iter_entries, iter_entries_with_id, open_cache_db,
|
|
362
|
+
get_entries, iter_entries, iter_entries_with_id, open_cache_db,
|
|
363
|
+
open_conversations_db, sync_cache, sync_claude_conversations,
|
|
364
|
+
sync_codex_conversations,
|
|
363
365
|
_prune_orphaned_cache_entries,
|
|
364
366
|
)
|
|
365
367
|
from _lib_snapshot_cache import (
|
|
@@ -1175,8 +1177,7 @@ def _dashboard_sync_loop(
|
|
|
1175
1177
|
|
|
1176
1178
|
def _dashboard_maybe_prune_retention() -> None:
|
|
1177
1179
|
"""#313 P3 (F7): throttled transcript retention prune driven from the
|
|
1178
|
-
|
|
1179
|
-
work (Task 6), so its cost paces the deadline. Opens a dedicated cache
|
|
1180
|
+
independent conversation sync thread. Opens a dedicated transcript
|
|
1180
1181
|
connection (no provider flock, no open transaction) for the orchestrator.
|
|
1181
1182
|
No-op when retention is disabled; never raises (a prune failure must not
|
|
1182
1183
|
crash the sync thread)."""
|
|
@@ -1187,7 +1188,7 @@ def _dashboard_maybe_prune_retention() -> None:
|
|
|
1187
1188
|
retention_days = resolve_retention_days(c.load_config())
|
|
1188
1189
|
if retention_days <= 0:
|
|
1189
1190
|
return
|
|
1190
|
-
conn = c.
|
|
1191
|
+
conn = c.open_conversations_db(attach_cache=False)
|
|
1191
1192
|
try:
|
|
1192
1193
|
retention._maybe_prune_conversation_retention(
|
|
1193
1194
|
conn,
|
|
@@ -4100,10 +4101,6 @@ def _qs_str(q: dict, key: str, default: str | None) -> str | None:
|
|
|
4100
4101
|
_DEBUG_CACHE_TABLES = (
|
|
4101
4102
|
"session_entries",
|
|
4102
4103
|
"session_files",
|
|
4103
|
-
"conversation_messages",
|
|
4104
|
-
"conversation_sessions",
|
|
4105
|
-
"conversation_ai_titles",
|
|
4106
|
-
"conversation_file_touches",
|
|
4107
4104
|
"codex_session_entries",
|
|
4108
4105
|
"codex_session_files",
|
|
4109
4106
|
)
|
|
@@ -6769,11 +6766,6 @@ def cmd_dashboard(args: argparse.Namespace) -> int:
|
|
|
6769
6766
|
and _time.monotonic() - last_heal[0] >= 60.0):
|
|
6770
6767
|
last_heal[0] = _time.monotonic()
|
|
6771
6768
|
_dashboard_self_heal_orphans(skip_sync=self._skip_sync)
|
|
6772
|
-
# #313 P3 (F7): throttled transcript retention prune, inside the
|
|
6773
|
-
# measured iteration so its cost is in the cooldown work. Gated
|
|
6774
|
-
# off under --no-sync (frozen mode makes no cache writes).
|
|
6775
|
-
if not self._skip_sync:
|
|
6776
|
-
_dashboard_maybe_prune_retention()
|
|
6777
6769
|
|
|
6778
6770
|
# Work-proportional cooldown (F10): sleep to t0 + max(interval, work)
|
|
6779
6771
|
# so a slow rebuild cannot peg a full core. The manual POST /api/sync
|
|
@@ -6797,6 +6789,47 @@ def cmd_dashboard(args: argparse.Namespace) -> int:
|
|
|
6797
6789
|
if sync_thread is not None:
|
|
6798
6790
|
sync_thread.start()
|
|
6799
6791
|
|
|
6792
|
+
# #320: transcript/search ingestion runs on its own thread and SQLite file.
|
|
6793
|
+
# A multi-GB first rebuild or a contended conversations.db therefore cannot
|
|
6794
|
+
# delay `_run_sync_now`, its `last_sync_at` stamp, or core SSE publication.
|
|
6795
|
+
conversation_sync_stop = threading.Event()
|
|
6796
|
+
|
|
6797
|
+
def _conversation_sync_loop() -> None:
|
|
6798
|
+
interval = max(5.0, float(args.sync_interval))
|
|
6799
|
+
while not conversation_sync_stop.is_set():
|
|
6800
|
+
conn = None
|
|
6801
|
+
try:
|
|
6802
|
+
conn = open_conversations_db()
|
|
6803
|
+
sync_claude_conversations(conn)
|
|
6804
|
+
sync_codex_conversations(conn)
|
|
6805
|
+
_dashboard_maybe_prune_retention()
|
|
6806
|
+
except (OSError, sqlite3.DatabaseError) as exc:
|
|
6807
|
+
eprint(f"[conversations] background sync unavailable: {exc}")
|
|
6808
|
+
except Exception as exc: # noqa: BLE001
|
|
6809
|
+
# Transcript parsing/normalization is deliberately outside the
|
|
6810
|
+
# core freshness loop. Keep this worker alive so a later clean
|
|
6811
|
+
# tick can self-heal instead of permanently stopping after one
|
|
6812
|
+
# malformed provider record.
|
|
6813
|
+
eprint(
|
|
6814
|
+
"[conversations] background sync failed: "
|
|
6815
|
+
f"{type(exc).__name__}: {exc}"
|
|
6816
|
+
)
|
|
6817
|
+
finally:
|
|
6818
|
+
if conn is not None:
|
|
6819
|
+
conn.close()
|
|
6820
|
+
conversation_sync_stop.wait(interval)
|
|
6821
|
+
|
|
6822
|
+
conversation_sync_thread = (
|
|
6823
|
+
None if args.no_sync
|
|
6824
|
+
else threading.Thread(
|
|
6825
|
+
target=_conversation_sync_loop,
|
|
6826
|
+
daemon=True,
|
|
6827
|
+
name="dashboard-conversations-sync",
|
|
6828
|
+
)
|
|
6829
|
+
)
|
|
6830
|
+
if conversation_sync_thread is not None:
|
|
6831
|
+
conversation_sync_thread.start()
|
|
6832
|
+
|
|
6800
6833
|
# Spec §3.5 (codex review fix #5): update-check thread runs even
|
|
6801
6834
|
# under --no-sync (frozen-data sessions still surface new versions).
|
|
6802
6835
|
# Dedicated stop event so we can join cleanly on shutdown without
|
|
@@ -6879,6 +6912,9 @@ def cmd_dashboard(args: argparse.Namespace) -> int:
|
|
|
6879
6912
|
finally:
|
|
6880
6913
|
if sync_thread is not None:
|
|
6881
6914
|
sync_thread.stop()
|
|
6915
|
+
conversation_sync_stop.set()
|
|
6916
|
+
if conversation_sync_thread is not None:
|
|
6917
|
+
conversation_sync_thread.join(timeout=2)
|
|
6882
6918
|
update_check_stop.set()
|
|
6883
6919
|
srv.shutdown()
|
|
6884
6920
|
http_thread.join(timeout=2)
|
|
@@ -20,9 +20,9 @@ What lives here (spec §6):
|
|
|
20
20
|
(``_require_transcripts_allowed`` / ``_transcript_gate`` / …) STAY on the
|
|
21
21
|
class and are reached via the ``handler`` parameter (spec §6).
|
|
22
22
|
|
|
23
|
-
Late-binding (spec §6): the
|
|
23
|
+
Late-binding (spec §6): the conversation DB opener calls (in
|
|
24
24
|
``_run_conversation_query_impl`` + the events handler) reach
|
|
25
|
-
``sys.modules["_cctally_dashboard"].
|
|
25
|
+
``sys.modules["_cctally_dashboard"].open_conversations_db(...)`` — patched on the
|
|
26
26
|
dashboard module object at ``tests/test_conversation_endpoints.py:674``.
|
|
27
27
|
|
|
28
28
|
Cross-module reaches (spec §2.1 "fully-qualify cross-module refs"): the
|
|
@@ -44,7 +44,13 @@ import socket
|
|
|
44
44
|
import sqlite3
|
|
45
45
|
import sys
|
|
46
46
|
|
|
47
|
-
from _cctally_cache import
|
|
47
|
+
from _cctally_cache import (
|
|
48
|
+
open_cache_db,
|
|
49
|
+
sync_codex_cache,
|
|
50
|
+
sync_claude_conversations,
|
|
51
|
+
sync_codex_conversations,
|
|
52
|
+
_codex_provider_roots,
|
|
53
|
+
)
|
|
48
54
|
|
|
49
55
|
# Live-tail watch-loop tuning — used ONLY by _handle_get_conversation_events_impl
|
|
50
56
|
# below, so moved here with the events handler (spec §4.1 / §6).
|
|
@@ -91,7 +97,7 @@ class _BadConversationFilter(Exception):
|
|
|
91
97
|
|
|
92
98
|
|
|
93
99
|
def _cached_file_sigs(conn, paths):
|
|
94
|
-
"""{path: size_bytes} from
|
|
100
|
+
"""{path: size_bytes} from conversation_source_files for the given paths — the transcript cache's
|
|
95
101
|
own view of how far each file is ingested. Size-only by design, matching the
|
|
96
102
|
watch kernel's size-only signature (`file_sig`) and sync_cache's size-only
|
|
97
103
|
delta signal: mtime is NOT consulted, because a size-unchanged ingest does
|
|
@@ -105,7 +111,7 @@ def _cached_file_sigs(conn, paths):
|
|
|
105
111
|
placeholders = ",".join("?" for _ in paths)
|
|
106
112
|
try:
|
|
107
113
|
rows = conn.execute(
|
|
108
|
-
f"SELECT path, size_bytes FROM
|
|
114
|
+
f"SELECT path, size_bytes FROM conversation_source_files "
|
|
109
115
|
f"WHERE path IN ({placeholders})", list(paths)).fetchall()
|
|
110
116
|
except sqlite3.OperationalError:
|
|
111
117
|
return out
|
|
@@ -115,7 +121,7 @@ def _cached_file_sigs(conn, paths):
|
|
|
115
121
|
|
|
116
122
|
|
|
117
123
|
def _codex_cached_file_sigs(conn, paths):
|
|
118
|
-
"""{path: size_bytes} from ``
|
|
124
|
+
"""{path: size_bytes} from ``codex_conversation_source_files`` for the given paths — the
|
|
119
125
|
Codex analogue of ``_cached_file_sigs`` (spec §5.2). Size-only, matching the
|
|
120
126
|
watch kernel's size-only signature and ``sync_codex_cache``'s size-only
|
|
121
127
|
delta. Paths with no row are absent → treated as changed. Baselines the
|
|
@@ -129,7 +135,7 @@ def _codex_cached_file_sigs(conn, paths):
|
|
|
129
135
|
placeholders = ",".join("?" for _ in paths)
|
|
130
136
|
try:
|
|
131
137
|
rows = conn.execute(
|
|
132
|
-
f"SELECT path, size_bytes FROM
|
|
138
|
+
f"SELECT path, size_bytes FROM codex_conversation_source_files "
|
|
133
139
|
f"WHERE path IN ({placeholders})", list(paths)).fetchall()
|
|
134
140
|
except sqlite3.OperationalError:
|
|
135
141
|
return out
|
|
@@ -144,7 +150,7 @@ def _codex_all_committed_sizes(conn):
|
|
|
144
150
|
baseline. A plain SELECT (no lock), so it never widens the SSE lock scope."""
|
|
145
151
|
try:
|
|
146
152
|
rows = conn.execute(
|
|
147
|
-
"SELECT path, size_bytes FROM
|
|
153
|
+
"SELECT path, size_bytes FROM codex_conversation_source_files").fetchall()
|
|
148
154
|
except sqlite3.OperationalError:
|
|
149
155
|
return {}
|
|
150
156
|
return {p: size for (p, size) in rows}
|
|
@@ -162,7 +168,7 @@ def _codex_classified_paths(conn, paths):
|
|
|
162
168
|
placeholders = ",".join("?" for _ in paths)
|
|
163
169
|
try:
|
|
164
170
|
rows = conn.execute(
|
|
165
|
-
f"SELECT path FROM
|
|
171
|
+
f"SELECT path FROM codex_conversation_source_files "
|
|
166
172
|
f"WHERE path IN ({placeholders}) AND last_conversation_key IS NOT NULL",
|
|
167
173
|
list(paths)).fetchall()
|
|
168
174
|
except sqlite3.OperationalError:
|
|
@@ -444,12 +450,12 @@ def _run_conversation_query_impl(handler, kernel_call, log_label):
|
|
|
444
450
|
has ALREADY been sent and the caller must just ``return``; ``ok=True``
|
|
445
451
|
carries the kernel result (which may itself be ``None`` — the reader's
|
|
446
452
|
404 sentinel — so the explicit flag, not ``body is None``, signals
|
|
447
|
-
failure). An ``
|
|
453
|
+
failure). An ``open_conversations_db`` failure is a ``cache unavailable:`` 500;
|
|
448
454
|
a kernel exception is logged as ``<log_label> failed: %r`` and returned
|
|
449
455
|
as a ``{type}: {msg}`` 500 — byte-identical to the inlined handlers.
|
|
450
456
|
"""
|
|
451
457
|
try:
|
|
452
|
-
conn = sys.modules["_cctally_dashboard"].
|
|
458
|
+
conn = sys.modules["_cctally_dashboard"].open_conversations_db()
|
|
453
459
|
except (sqlite3.DatabaseError, OSError) as exc:
|
|
454
460
|
handler._respond_json(500, {"error": f"cache unavailable: {exc}"})
|
|
455
461
|
return False, None
|
|
@@ -785,7 +791,7 @@ def _bare_conversation_events(handler, session_id: str) -> None:
|
|
|
785
791
|
_send_sse_headers(handler)
|
|
786
792
|
passive = bool(type(handler).no_sync)
|
|
787
793
|
try:
|
|
788
|
-
conn = sys.modules["_cctally_dashboard"].
|
|
794
|
+
conn = sys.modules["_cctally_dashboard"].open_conversations_db()
|
|
789
795
|
except (sqlite3.DatabaseError, OSError):
|
|
790
796
|
# Cache unavailable — degrade to keep-alive only; client backstop
|
|
791
797
|
# tick still surfaces turns. (Headers already sent; can't 500.)
|
|
@@ -796,7 +802,7 @@ def _bare_conversation_events(handler, session_id: str) -> None:
|
|
|
796
802
|
return cq.session_source_paths(conn, session_id) if conn else []
|
|
797
803
|
|
|
798
804
|
def _ingest(changed):
|
|
799
|
-
return
|
|
805
|
+
return sync_claude_conversations(conn, only_paths=set(changed))
|
|
800
806
|
|
|
801
807
|
try:
|
|
802
808
|
_run_conversation_events_stream(
|
|
@@ -840,7 +846,24 @@ def _make_codex_discovery_step(handler, conn, conversation_key, cq_codex):
|
|
|
840
846
|
if not to_ingest:
|
|
841
847
|
return files, False
|
|
842
848
|
try:
|
|
843
|
-
|
|
849
|
+
# A newly discovered child has no compact thread row yet. Advance
|
|
850
|
+
# the independent core cursor first so transcript normalization can
|
|
851
|
+
# link the child through the read-only attached cache, then commit
|
|
852
|
+
# the transcript cursor. Neither lock is held across the other.
|
|
853
|
+
core = open_cache_db()
|
|
854
|
+
try:
|
|
855
|
+
core_stats = sync_codex_cache(
|
|
856
|
+
core, only_paths=set(to_ingest)
|
|
857
|
+
)
|
|
858
|
+
finally:
|
|
859
|
+
core.close()
|
|
860
|
+
if not core_stats.targeted_clean:
|
|
861
|
+
return files, False
|
|
862
|
+
transcript_stats = sync_codex_conversations(
|
|
863
|
+
conn, only_paths=set(to_ingest)
|
|
864
|
+
)
|
|
865
|
+
if not transcript_stats.targeted_clean:
|
|
866
|
+
return files, False
|
|
844
867
|
except sqlite3.DatabaseError:
|
|
845
868
|
return files, False
|
|
846
869
|
# Reap the now-classified candidates (child → already widened; non-child
|
|
@@ -866,7 +889,7 @@ def _qualified_conversation_events(handler, key: str) -> None:
|
|
|
866
889
|
targeted ingest + the directory-frontier child discovery."""
|
|
867
890
|
disp = _conversation_dispatch_impl()
|
|
868
891
|
try:
|
|
869
|
-
conn = sys.modules["_cctally_dashboard"].
|
|
892
|
+
conn = sys.modules["_cctally_dashboard"].open_conversations_db()
|
|
870
893
|
except (sqlite3.DatabaseError, OSError):
|
|
871
894
|
conn = None
|
|
872
895
|
|
|
@@ -921,7 +944,7 @@ def _qualified_conversation_events(handler, key: str) -> None:
|
|
|
921
944
|
return cq_codex.codex_conversation_source_paths(conn, key)
|
|
922
945
|
|
|
923
946
|
def _ingest(changed):
|
|
924
|
-
return
|
|
947
|
+
return sync_codex_conversations(conn, only_paths=set(changed))
|
|
925
948
|
|
|
926
949
|
cached = lambda paths: _codex_cached_file_sigs(conn, paths)
|
|
927
950
|
discovery = _make_codex_discovery_step(handler, conn, key, cq_codex)
|
|
@@ -932,7 +955,7 @@ def _qualified_conversation_events(handler, key: str) -> None:
|
|
|
932
955
|
return cq.session_source_paths(conn, native)
|
|
933
956
|
|
|
934
957
|
def _ingest(changed):
|
|
935
|
-
return
|
|
958
|
+
return sync_claude_conversations(conn, only_paths=set(changed))
|
|
936
959
|
|
|
937
960
|
cached = lambda paths: _cached_file_sigs(conn, paths)
|
|
938
961
|
discovery = None
|
|
@@ -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,
|
|
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"))
|
|
@@ -713,22 +733,33 @@ def _codex_conversation_metadata(
|
|
|
713
733
|
|
|
714
734
|
``state_5.sqlite.threads.title`` is Codex's persisted user-facing task name.
|
|
715
735
|
Conversation rollup titles are derived from prompt text and therefore must
|
|
716
|
-
never be substituted for that name on the dashboard.
|
|
717
|
-
|
|
736
|
+
never be substituted for that name on the dashboard. Project attribution
|
|
737
|
+
is derived from the compact thread ``cwd``/``git_json`` retained in
|
|
738
|
+
``cache.db`` so non-conversation panels never open ``conversations.db``.
|
|
718
739
|
"""
|
|
719
740
|
metadata: dict[tuple[str, str], dict[str, object]] = {}
|
|
720
741
|
try:
|
|
721
|
-
|
|
742
|
+
core_rows = tuple(cache_conn.execute(
|
|
722
743
|
"SELECT t.source_root_key, t.source_path, t.native_thread_id, "
|
|
723
744
|
"(SELECT e.session_id FROM codex_session_entries AS e "
|
|
724
745
|
" WHERE e.source_root_key=t.source_root_key AND e.source_path=t.source_path "
|
|
725
746
|
" ORDER BY e.id LIMIT 1) AS accounting_session_id, "
|
|
726
|
-
"
|
|
747
|
+
"t.cwd, t.git_json, t.first_seen_utc, t.last_seen_utc "
|
|
727
748
|
"FROM codex_conversation_threads AS t "
|
|
728
|
-
"
|
|
729
|
-
"ON r.conversation_key=t.conversation_key "
|
|
730
|
-
"ORDER BY r.last_activity_utc DESC, t.conversation_key DESC"
|
|
749
|
+
"ORDER BY t.last_seen_utc DESC, t.conversation_key DESC"
|
|
731
750
|
))
|
|
751
|
+
from _cctally_cache import _codex_conversation_project_attribution
|
|
752
|
+
rows = tuple(
|
|
753
|
+
(
|
|
754
|
+
root_key, source_path, native_thread_id, accounting_session_id,
|
|
755
|
+
*_codex_conversation_project_attribution(root_key, cwd, git_json),
|
|
756
|
+
first_seen_at,
|
|
757
|
+
)
|
|
758
|
+
for (
|
|
759
|
+
root_key, source_path, native_thread_id, accounting_session_id,
|
|
760
|
+
cwd, git_json, first_seen_at, _last_seen_at,
|
|
761
|
+
) in core_rows
|
|
762
|
+
)
|
|
732
763
|
file_aliases = tuple(cache_conn.execute(
|
|
733
764
|
"SELECT source_root_key, path, last_native_thread_id, last_session_id "
|
|
734
765
|
"FROM codex_session_files "
|
|
@@ -1833,6 +1864,8 @@ def _build_codex_native_weekly_view(
|
|
|
1833
1864
|
labels: dict[str, str] = {}
|
|
1834
1865
|
periods_by_bucket: dict[str, CodexWeeklyPeriod] = {}
|
|
1835
1866
|
for entry in entries:
|
|
1867
|
+
if codex_model_scoped_quota_pool(getattr(entry, "model", None)) is not None:
|
|
1868
|
+
continue
|
|
1836
1869
|
timestamp = getattr(entry, "timestamp").astimezone(UTC)
|
|
1837
1870
|
root_key = str(getattr(entry, "source_root_key", "") or "")
|
|
1838
1871
|
period = next((
|