cctally 1.80.2 → 1.80.4
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,16 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.80.4] - 2026-07-23
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Codex Recent Sessions now keeps MCP-invoked sessions in true last-activity order and derives each Started time and duration from retained accounting events, so a cache rebuild no longer gives historical conversations one shared date or `0m` durations.
|
|
12
|
+
|
|
13
|
+
## [1.80.3] - 2026-07-23
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Cache recovery now detects a damaged `session_entries` B-tree even when `cache.db` still has a readable schema, allowing dashboard startup and `cache-sync --rebuild` to quarantine and recreate the already-corrupt v1.80.1/v1.80.2 cache instead of surfacing a persistent sync error.
|
|
17
|
+
|
|
8
18
|
## [1.80.2] - 2026-07-23
|
|
9
19
|
|
|
10
20
|
### Fixed
|
package/bin/_cctally_cache.py
CHANGED
|
@@ -4812,6 +4812,19 @@ def _cache_open_guarded() -> sqlite3.Connection:
|
|
|
4812
4812
|
# Force a real header/schema read. SELECT 1 is constant-folded and can
|
|
4813
4813
|
# report success without touching a malformed database file.
|
|
4814
4814
|
conn.execute("PRAGMA schema_version").fetchone()
|
|
4815
|
+
# The v1.80.2 production incident left the schema and left edge of
|
|
4816
|
+
# session_entries readable while its interior root page pointed past
|
|
4817
|
+
# EOF on the right. Probe that right-most path explicitly: O(log N), so
|
|
4818
|
+
# every short-lived hook can afford it, unlike PRAGMA quick_check's
|
|
4819
|
+
# whole-database scan.
|
|
4820
|
+
if conn.execute(
|
|
4821
|
+
"SELECT 1 FROM sqlite_schema "
|
|
4822
|
+
"WHERE type='table' AND name='session_entries'"
|
|
4823
|
+
).fetchone() is not None:
|
|
4824
|
+
conn.execute(
|
|
4825
|
+
"SELECT rowid FROM session_entries "
|
|
4826
|
+
"ORDER BY rowid DESC LIMIT 1"
|
|
4827
|
+
).fetchone()
|
|
4815
4828
|
if marker.exists():
|
|
4816
4829
|
conn.close()
|
|
4817
4830
|
conn = None
|
|
@@ -757,12 +757,19 @@ def _codex_conversation_metadata(
|
|
|
757
757
|
metadata: dict[tuple[str, str], dict[str, object]] = {}
|
|
758
758
|
try:
|
|
759
759
|
core_rows = tuple(cache_conn.execute(
|
|
760
|
+
"WITH accounting AS ("
|
|
761
|
+
" SELECT source_root_key, source_path, MIN(id) AS first_id,"
|
|
762
|
+
" MIN(timestamp_utc) AS started_at"
|
|
763
|
+
" FROM codex_session_entries"
|
|
764
|
+
" GROUP BY source_root_key, source_path"
|
|
765
|
+
") "
|
|
760
766
|
"SELECT t.source_root_key, t.source_path, t.native_thread_id, "
|
|
761
|
-
"
|
|
762
|
-
"
|
|
763
|
-
" ORDER BY e.id LIMIT 1) AS accounting_session_id, "
|
|
764
|
-
"t.cwd, t.git_json, t.first_seen_utc, t.last_seen_utc "
|
|
767
|
+
"e.session_id AS accounting_session_id, "
|
|
768
|
+
"t.cwd, t.git_json, a.started_at, t.last_seen_utc "
|
|
765
769
|
"FROM codex_conversation_threads AS t "
|
|
770
|
+
"LEFT JOIN accounting AS a "
|
|
771
|
+
"ON a.source_root_key=t.source_root_key AND a.source_path=t.source_path "
|
|
772
|
+
"LEFT JOIN codex_session_entries AS e ON e.id=a.first_id "
|
|
766
773
|
"ORDER BY t.last_seen_utc DESC, t.conversation_key DESC"
|
|
767
774
|
))
|
|
768
775
|
from _cctally_cache import _codex_conversation_project_attribution
|
|
@@ -778,16 +785,20 @@ def _codex_conversation_metadata(
|
|
|
778
785
|
) in core_rows
|
|
779
786
|
)
|
|
780
787
|
file_aliases = tuple(cache_conn.execute(
|
|
781
|
-
"SELECT source_root_key, path, last_native_thread_id,
|
|
782
|
-
"
|
|
783
|
-
"
|
|
784
|
-
"
|
|
788
|
+
"SELECT f.source_root_key, f.path, f.last_native_thread_id, "
|
|
789
|
+
"f.last_session_id, MIN(e.timestamp_utc) "
|
|
790
|
+
"FROM codex_session_files AS f "
|
|
791
|
+
"LEFT JOIN codex_session_entries AS e "
|
|
792
|
+
"ON e.source_root_key=f.source_root_key AND e.source_path=f.path "
|
|
793
|
+
"WHERE f.last_native_thread_id IS NOT NULL AND f.last_native_thread_id != '' "
|
|
794
|
+
"GROUP BY f.source_root_key, f.path, f.last_native_thread_id, f.last_session_id "
|
|
795
|
+
"ORDER BY f.last_ingested_at DESC, f.path DESC"
|
|
785
796
|
))
|
|
786
797
|
native_ids = tuple(sorted({
|
|
787
798
|
str(native_thread_id) for _, _, native_thread_id, *_ in rows
|
|
788
799
|
if isinstance(native_thread_id, str) and native_thread_id
|
|
789
800
|
} | {
|
|
790
|
-
str(native_thread_id) for _, _, native_thread_id, _ in file_aliases
|
|
801
|
+
str(native_thread_id) for _, _, native_thread_id, *_ in file_aliases
|
|
791
802
|
if isinstance(native_thread_id, str) and native_thread_id
|
|
792
803
|
}))
|
|
793
804
|
provider_roots = {
|
|
@@ -858,7 +869,10 @@ def _codex_conversation_metadata(
|
|
|
858
869
|
# persists the rooted native thread id. Inherit only presentation
|
|
859
870
|
# metadata from that rooted task; the child's accounting path and
|
|
860
871
|
# session id remain its own identity and totals are never merged.
|
|
861
|
-
for
|
|
872
|
+
for (
|
|
873
|
+
root_key, source_path, native_thread_id, accounting_session_id,
|
|
874
|
+
started_at,
|
|
875
|
+
) in file_aliases:
|
|
862
876
|
identity = (str(root_key or ""), str(source_path or ""))
|
|
863
877
|
if not all(identity) or identity in metadata:
|
|
864
878
|
continue
|
|
@@ -871,7 +885,7 @@ def _codex_conversation_metadata(
|
|
|
871
885
|
"root_path": str(provider_roots.get(identity[0]) or ""),
|
|
872
886
|
"project_key": (inherited or {}).get("project_key"),
|
|
873
887
|
"project_label": (inherited or {}).get("project_label"),
|
|
874
|
-
"started_at": (inherited or {}).get("started_at"),
|
|
888
|
+
"started_at": started_at or (inherited or {}).get("started_at"),
|
|
875
889
|
}
|
|
876
890
|
except sqlite3.Error:
|
|
877
891
|
return {}
|