cctally 1.118.0 → 1.118.1
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 +7 -0
- package/bin/_cctally_cache.py +90 -18
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.118.1] - 2026-11-06
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- With an account selected, the `dashboard` Conversation Viewer and `transcript` commands now show conversations' AI titles, and title search ranks, prefix-matches and highlights them; a session spanning two accounts keeps its fallback title.
|
|
12
|
+
- After a rebuild of the conversation cache, the `dashboard` Conversation Viewer no longer shows an AI title that a replaced or truncated transcript no longer contains.
|
|
13
|
+
- The `dashboard` Conversation Viewer no longer hides a conversation's earlier messages and AI title until the next rebuild after an unfinished last line is trimmed from its transcript.
|
|
14
|
+
|
|
8
15
|
## [1.118.0] - 2026-11-06
|
|
9
16
|
|
|
10
17
|
### Added
|
package/bin/_cctally_cache.py
CHANGED
|
@@ -289,21 +289,34 @@ _AI_TITLE_UPSERT_SQL = (
|
|
|
289
289
|
"ai_title=excluded.ai_title, source_path=excluded.source_path, byte_offset=excluded.byte_offset"
|
|
290
290
|
)
|
|
291
291
|
|
|
292
|
-
def _ai_title_upsert_sql(table: str = "conversation_ai_titles"
|
|
292
|
+
def _ai_title_upsert_sql(table: str = "conversation_ai_titles", *,
|
|
293
|
+
owner_only: bool = False) -> str:
|
|
293
294
|
"""The AI-title upsert, targeted at the live table or its staging twin.
|
|
294
295
|
|
|
295
296
|
A rebuild builds its replacement generation into staging and leaves the
|
|
296
|
-
live table alone until the publish (#752)
|
|
297
|
-
a
|
|
298
|
-
|
|
297
|
+
live table alone until the publish (#752) — except for a file replayed
|
|
298
|
+
under a NEW incarnation, whose live title rows are replaced,
|
|
299
|
+
owner-restricted, at its batch commit (#1062 §4.2). So the ONE statement
|
|
300
|
+
that writes a title has to be able to name either table. Parameterised
|
|
301
|
+
through this helper rather than by several literals, because literals
|
|
302
|
+
drift.
|
|
303
|
+
|
|
304
|
+
``owner_only`` is that #1062 §4.2 live-table write. The file's own live
|
|
305
|
+
rows were just deleted, so a conflicting live row belongs to ANOTHER file
|
|
306
|
+
of the same session and is kept until the publish installs the replayed
|
|
307
|
+
generation's choice; a later title line of the same file still replaces an
|
|
308
|
+
earlier one, as in the walk.
|
|
299
309
|
"""
|
|
300
|
-
|
|
310
|
+
sql = (
|
|
301
311
|
f"INSERT INTO {table}(session_id,ai_title,source_path,byte_offset) "
|
|
302
312
|
"VALUES(?,?,?,?) "
|
|
303
313
|
"ON CONFLICT(session_id) DO UPDATE SET "
|
|
304
314
|
"ai_title=excluded.ai_title, source_path=excluded.source_path, "
|
|
305
315
|
"byte_offset=excluded.byte_offset"
|
|
306
316
|
)
|
|
317
|
+
if owner_only:
|
|
318
|
+
sql += f" WHERE {table}.source_path = excluded.source_path"
|
|
319
|
+
return sql
|
|
307
320
|
|
|
308
321
|
|
|
309
322
|
# ---------------------------------------------------------------------------
|
|
@@ -21527,12 +21540,36 @@ def scope_conversations_db_to_account(
|
|
|
21527
21540
|
f"""
|
|
21528
21541
|
CREATE TEMP VIEW conversation_messages AS
|
|
21529
21542
|
SELECT * FROM main.conversation_messages WHERE {stored_predicate};
|
|
21543
|
+
-- #1062: a title is in scope iff (a) its own file's message walk has
|
|
21544
|
+
-- committed past the title's line, (b) that file still holds a
|
|
21545
|
+
-- retained in-scope message of the title's session, (c) no retained
|
|
21546
|
+
-- message of the session, in any file, is outside the scope, and
|
|
21547
|
+
-- (d) no Claude conversation rebuild is in progress (its message
|
|
21548
|
+
-- table may be partial). (b) reads the scoped TEMP view; (a), (c)
|
|
21549
|
+
-- and (d) read main. The fragment's unqualified account_key binds to
|
|
21550
|
+
-- the innermost FROM (main.conversation_messages m). The base rowid
|
|
21551
|
+
-- is projected as `rowid` so the title FTS joins resolve on a scoped
|
|
21552
|
+
-- connection and return the base row (C1).
|
|
21530
21553
|
CREATE TEMP VIEW conversation_ai_titles AS
|
|
21531
|
-
SELECT t.* FROM main.conversation_ai_titles t
|
|
21554
|
+
SELECT t.rowid AS rowid, t.* FROM main.conversation_ai_titles t
|
|
21532
21555
|
WHERE EXISTS (
|
|
21556
|
+
SELECT 1 FROM main.conversation_source_files f
|
|
21557
|
+
WHERE f.path = t.source_path
|
|
21558
|
+
AND f.last_byte_offset > t.byte_offset
|
|
21559
|
+
)
|
|
21560
|
+
AND EXISTS (
|
|
21533
21561
|
SELECT 1 FROM conversation_messages m
|
|
21534
|
-
WHERE m.
|
|
21535
|
-
AND m.
|
|
21562
|
+
WHERE m.session_id = t.session_id
|
|
21563
|
+
AND m.source_path = t.source_path
|
|
21564
|
+
)
|
|
21565
|
+
AND NOT EXISTS (
|
|
21566
|
+
SELECT 1 FROM main.conversation_messages m
|
|
21567
|
+
WHERE m.session_id = t.session_id
|
|
21568
|
+
AND NOT ({stored_predicate})
|
|
21569
|
+
)
|
|
21570
|
+
AND NOT EXISTS (
|
|
21571
|
+
SELECT 1 FROM main.cache_meta
|
|
21572
|
+
WHERE key = 'conversation_rebuild_claude_pending'
|
|
21536
21573
|
);
|
|
21537
21574
|
CREATE TEMP VIEW conversation_file_touches AS
|
|
21538
21575
|
SELECT t.* FROM main.conversation_file_touches t
|
|
@@ -23642,6 +23679,19 @@ def _write_claude_prepared_file(
|
|
|
23642
23679
|
and the scoped ``_fill_file_touches`` never commits."""
|
|
23643
23680
|
path_str = prepared.path
|
|
23644
23681
|
sessions: set[str] = set()
|
|
23682
|
+
# #1062 §4.2: a rebuild writes titles to staging and leaves the live table
|
|
23683
|
+
# to the publish (#752) — except for a file replayed under a NEW
|
|
23684
|
+
# incarnation. `prepared.truncated` means exactly that (#1062 F8): a
|
|
23685
|
+
# replacement, a truncation below the cursor, or missing identity
|
|
23686
|
+
# evidence; a continuous file that merely shrank (a trimmed unfinished
|
|
23687
|
+
# tail) is not truncated and keeps its committed rows. A new incarnation's
|
|
23688
|
+
# live rows get what an incremental sync of the file would leave: the
|
|
23689
|
+
# path's rows deleted and its replayed titles re-upserted, owner-restricted,
|
|
23690
|
+
# here in the file's savepoint, so they commit with its cursor. Without it
|
|
23691
|
+
# the fold carries a title the file has lost into the new generation, and
|
|
23692
|
+
# after an interrupted rebuild no later run detects the replacement again.
|
|
23693
|
+
refresh_live = (prepared.truncated
|
|
23694
|
+
and title_table != "conversation_ai_titles")
|
|
23645
23695
|
if prepared.truncated:
|
|
23646
23696
|
sessions.update(
|
|
23647
23697
|
row[0]
|
|
@@ -23664,6 +23714,11 @@ def _write_claude_prepared_file(
|
|
|
23664
23714
|
f"DELETE FROM {title_table} WHERE source_path=?",
|
|
23665
23715
|
(path_str,),
|
|
23666
23716
|
)
|
|
23717
|
+
if refresh_live:
|
|
23718
|
+
conn.execute(
|
|
23719
|
+
"DELETE FROM conversation_ai_titles WHERE source_path=?",
|
|
23720
|
+
(path_str,),
|
|
23721
|
+
)
|
|
23667
23722
|
if prepared.conv_rows:
|
|
23668
23723
|
conn.executemany(_CONV_ACCOUNT_INSERT_SQL, prepared.conv_rows)
|
|
23669
23724
|
_fill_file_touches(
|
|
@@ -23683,6 +23738,9 @@ def _write_claude_prepared_file(
|
|
|
23683
23738
|
)
|
|
23684
23739
|
if prepared.ai_rows:
|
|
23685
23740
|
conn.executemany(_ai_title_upsert_sql(title_table), prepared.ai_rows)
|
|
23741
|
+
if refresh_live:
|
|
23742
|
+
conn.executemany(
|
|
23743
|
+
_ai_title_upsert_sql(owner_only=True), prepared.ai_rows)
|
|
23686
23744
|
size, mtime_ns, final_offset, device_id, inode, incarnation_id, prefix = (
|
|
23687
23745
|
prepared.cursor)
|
|
23688
23746
|
conn.execute(
|
|
@@ -24028,9 +24086,11 @@ def sync_claude_conversations(
|
|
|
24028
24086
|
# nothing, so every one of its records is first observed now and takes
|
|
24029
24087
|
# the active identity rather than being read as historical-and-unknown.
|
|
24030
24088
|
high_water: "dict[tuple[str, str], int]" = {}
|
|
24031
|
-
# #752: a rebuild writes its titles and rollup into staging and
|
|
24032
|
-
#
|
|
24033
|
-
# the previous complete generation for the
|
|
24089
|
+
# #752: a rebuild writes its titles and rollup into staging and
|
|
24090
|
+
# otherwise leaves the live tables to the publish, so readers keep
|
|
24091
|
+
# seeing the previous complete generation for the replay. The one
|
|
24092
|
+
# exception is #1062 §4.2: a file replayed under a NEW incarnation has
|
|
24093
|
+
# its live title rows replaced, owner-restricted, at its batch commit.
|
|
24034
24094
|
title_table = "conversation_ai_titles"
|
|
24035
24095
|
rollup_table = "conversation_sessions"
|
|
24036
24096
|
if rebuild:
|
|
@@ -24132,6 +24192,11 @@ def sync_claude_conversations(
|
|
|
24132
24192
|
stats.files_skipped_unchanged += 1
|
|
24133
24193
|
_report_conversation_progress(progress, "ingest", stats)
|
|
24134
24194
|
continue
|
|
24195
|
+
# The raw shrink decides only the targeted deferral below,
|
|
24196
|
+
# which runs before the incarnation resolver and so cannot yet
|
|
24197
|
+
# tell a truncation below the cursor from a trimmed unfinished
|
|
24198
|
+
# tail; deferring both to a full sync is conservative. Past the
|
|
24199
|
+
# resolver, `truncated` is redefined as "a new incarnation".
|
|
24135
24200
|
truncated = prev is not None and size < prev[0]
|
|
24136
24201
|
if targeted and truncated:
|
|
24137
24202
|
# #939 D1: every exit resolves the open batch.
|
|
@@ -24165,13 +24230,20 @@ def sync_claude_conversations(
|
|
|
24165
24230
|
start_offset = incarnation.start_offset
|
|
24166
24231
|
file_high_water = high_water.get(
|
|
24167
24232
|
(path_str, incarnation.incarnation_id), 0)
|
|
24168
|
-
|
|
24169
|
-
|
|
24170
|
-
|
|
24171
|
-
|
|
24172
|
-
|
|
24173
|
-
|
|
24174
|
-
|
|
24233
|
+
# A new incarnation forces byte-zero treatment: the
|
|
24234
|
+
# bytes before the cursor are not the bytes the cursor
|
|
24235
|
+
# described, so resuming from it would skip real records
|
|
24236
|
+
# and stamp the rest against an identity that never held
|
|
24237
|
+
# them. A CONTINUOUS incarnation is never a truncation,
|
|
24238
|
+
# even when the file shrank (#1062 F8): a pass that ended
|
|
24239
|
+
# on an unfinished last line records a size past its
|
|
24240
|
+
# cursor, and trimming that line shrinks the file without
|
|
24241
|
+
# moving a committed byte. Its committed rows stay; the
|
|
24242
|
+
# walk resumes from the cursor and the cursor row records
|
|
24243
|
+
# the smaller size. Treating that shrink as a truncation
|
|
24244
|
+
# deleted every row of the path and re-inserted only the
|
|
24245
|
+
# tail.
|
|
24246
|
+
truncated = incarnation.is_new and prev is not None
|
|
24175
24247
|
# #939 D1: admission is decided BEFORE the parse. The
|
|
24176
24248
|
# read limit is the size the incarnation decision saw
|
|
24177
24249
|
# (the same fstat recorded as the change detector
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cctally",
|
|
3
|
-
"version": "1.118.
|
|
3
|
+
"version": "1.118.1",
|
|
4
4
|
"description": "Claude Code usage tracker and local dashboard for Pro/Max subscription limits - weekly cost-per-percent trend, quota forecasts, threshold alerts. ccusage-compatible.",
|
|
5
5
|
"homepage": "https://github.com/omrikais/cctally",
|
|
6
6
|
"repository": {
|