cctally 1.96.1 → 1.97.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 +24 -0
- package/bin/_cctally_dashboard_envelope.py +14 -0
- package/bin/_cctally_dashboard_share.py +25 -5
- package/bin/_cctally_dashboard_sources.py +44 -23
- package/bin/_cctally_db.py +10 -0
- package/bin/_cctally_doctor.py +15 -10
- package/bin/_cctally_journal_repair.py +7 -6
- package/bin/_cctally_parser.py +47 -22
- package/bin/_cctally_tui.py +203 -34
- package/bin/_lib_dashboard_sources.py +376 -85
- package/bin/_lib_journal_router.py +14 -0
- package/bin/_lib_pricing.py +48 -38
- package/bin/_lib_share.py +191 -36
- package/bin/cctally +35 -1
- package/dashboard/static/assets/index-BSESoPIK.css +1 -0
- package/dashboard/static/assets/{index-Bhr5gZ14.js → index-DgsMz5hA.js} +55 -55
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
- package/dashboard/static/assets/index-DfN_fsLZ.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.97.0] - 2026-08-13
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Reveal-mode share exports now allow project directories whose basename is exactly a UUID or source-root-shaped hex token, while the same value outside a typed project field still fails the document-wide privacy guard (#521).
|
|
12
|
+
- Fix UI-QA provenance for subagent-driven UI work by allowing a durable report-only certification commit to name and verify the exact already-committed target, and stop unrelated linked worktrees from perturbing the QA cleanup fingerprint (#558).
|
|
13
|
+
- The dashboard's `All` headline now states a quantity you can check. It is the sum of each provider's own current billing period — Claude's subscription week plus Codex's native seven-day cycle — instead of a thirty-day Claude accounting rollup added to a seven-day Codex cycle under no period label, which on one production install read $18,401.71 beside provider tabs showing about $1,704 and about $4,108. The heading names the cycles it covers and names only the providers actually contributing, each provider block carries its own labelled reset, and the per-provider split is on the hero itself rather than behind a modal. Because the two legs are each provider's own cycle, the figure steps down twice a week at two different reset instants and is not comparable against itself across a reset (#556).
|
|
14
|
+
- The `All` staleness marker no longer sits on the figure permanently. The dashboard's `hero` freshness axis now means "the current cycle resolves and its counters are publishable" rather than "how old the last percent observation is", and percent age moved to the `quota` axis, which is what it always described. The two axes were being combined across clocks forty times apart — ninety seconds against one hour — so the marker was on almost always. A published combined figure is now qualified only by a real qualification, and the self-contradicting pairing of "combined totals are unavailable" with "combined totals use retained actuals" is gone (#556).
|
|
15
|
+
- On an install with more than one account on either provider, the `All` combined total is withheld with a named reason saying which provider has several accounts on separate cycles, and points at the per-account cards that carry each account's own spend. Publishing a single number there would have reproduced the same defect inside the decorated path, because it could not be reconciled against any tab. A count that cannot be read withholds the figure too, rather than assuming a single account (#556).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Deep doctor diagnostics and `db journal-repair` now preserve the same physical journal positions as rebuild when fingerprinting structural correction violations, so irrelevant observations can no longer make one path reject another path's durable resolution audit (#508).
|
|
19
|
+
- Root and setup help now describe both Claude and Codex, expose provider-aware `--source` analytics and native Codex quota commands, and name setup-managed Codex handlers (#555).
|
|
20
|
+
- The concurrent stats-heal writer-storm regression now waits for the detached worker's terminal success event before reading the rebuilt index, instead of racing publication artifacts that can become visible while the worker is still finishing under load (#559).
|
|
21
|
+
- CI no longer reports a reader jump as stranded when the reader is merely still travelling toward it. The transient-drain-failure regression measured whether the linked turn sat inside the reader viewport as soon as the turn mounted, but mounting only means the backward walk has reached it, not that the walk has stopped scrolling; the measurement now waits for the scroll to settle, the way every other jump assertion in that suite already did.
|
|
22
|
+
- The dashboard refreshes far more often on installs with a large Codex history, so newly recorded milestones and every other panel stop lagging minutes behind the database. A per-file lookup in the Codex source panel matched on the provider root alone, and because a machine normally has exactly one Codex root that lookup read every Codex usage row for every rollout file. On one real store of 2,324 files and 151,903 entries it took 57 seconds of an 84-second snapshot rebuild, which the dashboard's work-proportional cooldown then doubled into a staleness window of roughly 170 seconds. A composite index over the two columns the lookup actually matches makes it linear again, measured at 57.20s to 0.208s, which cuts the whole rebuild to about 32 seconds. The index is re-derivable, so an existing cache gains it on the next open with no migration, no data rewrite and no measurable size increase.
|
|
23
|
+
|
|
24
|
+
## [1.96.2] - 2026-08-13
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- CI no longer mistakes loaded-runner latency for a transcript-rebuild phase failure, a hung dashboard budget reconcile, a fake app-server that failed to start, or a writer-storm regression when readers remain healthy relative to both the uncontended baseline and concurrent writers.
|
|
28
|
+
- Settings section links now scroll only the content pane, keep the modal chrome visible, and select the requested section consistently in Safari.
|
|
29
|
+
- Claude Sonnet 5 usage now uses its permanent $2/$10 per-million-token rate, and Claude Mythos 5 plus retained Mythos Preview usage are no longer left unpriced.
|
|
30
|
+
|
|
8
31
|
## [1.96.1] - 2026-08-13
|
|
9
32
|
|
|
10
33
|
### Fixed
|
|
@@ -45,6 +68,7 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
45
68
|
- The sanctioned local test escape hatch now runs the same contract as a remote run. `CCTALLY_TEST_LOCAL=1` previously left the `agentmem`-gated tests skipping while every remote run required them, so a local green and a remote green were different claims and no document said so. The local path now pins the same policy. If `agentmem` is genuinely absent the run still completes rather than dying, but it states which contract it ran and how many tests that skipped, and classifies itself non-authoritative under a named reason (#529).
|
|
46
69
|
|
|
47
70
|
### Fixed
|
|
71
|
+
- Share charts now use an AA-compliant light warning colour and keep stacked segments distinguishable without colour (#524).
|
|
48
72
|
- Remote test routing no longer mistakes runners last provisioned by different branches for a toolchain divergence; parity now compares the caller's declared Python closure while retaining live host-runtime checks (#548).
|
|
49
73
|
- The stats-corruption epic test now waits for the heal worker's terminal outcome instead of racing the earlier request-marker cleanup, and its maintenance-holder handshake uses bounded readiness and release pipes rather than wall-clock guesses. Sanitized pytest failure extracts now retain safe assertion structure and normalized traceback locations while continuing to redact private paths and dynamic values (#552).
|
|
50
74
|
- Remote test runs now refuse a conflicted checkout before contacting a runner, name the unmerged git index and its normal `git add` recovery, and always explain a manifest mismatch even when duplicate path records were the only difference (#550).
|
|
@@ -1188,6 +1188,7 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1188
1188
|
|
|
1189
1189
|
week_lbl: "str | None" = None
|
|
1190
1190
|
reset_at_utc: "dt.datetime | None" = None
|
|
1191
|
+
week_start_at_utc: "dt.datetime | None" = None
|
|
1191
1192
|
if cw is not None:
|
|
1192
1193
|
ws = getattr(cw, "week_start_at", None)
|
|
1193
1194
|
we = getattr(cw, "week_end_at", None)
|
|
@@ -1203,6 +1204,7 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1203
1204
|
elif ws is not None:
|
|
1204
1205
|
week_lbl = format_display_dt(ws, resolved_tz_obj, fmt='%b %d', suffix=False)
|
|
1205
1206
|
reset_at_utc = we
|
|
1207
|
+
week_start_at_utc = ws
|
|
1206
1208
|
|
|
1207
1209
|
# Header forecast_pct should match the projection that drove the
|
|
1208
1210
|
# verdict pill next to it. The View (issue #57) carries the
|
|
@@ -1616,7 +1618,19 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1616
1618
|
None if cw.five_hour_resets_at is None
|
|
1617
1619
|
else max(0, int((cw.five_hour_resets_at - now_utc).total_seconds())),
|
|
1618
1620
|
"spent_usd": cw.spent_usd,
|
|
1621
|
+
# #556 S1 §3.3 — the token half of the same accumulation pass
|
|
1622
|
+
# that produced `spent_usd`. `getattr` keeps legacy fixture
|
|
1623
|
+
# modules that construct `TuiCurrentWeek` without the field
|
|
1624
|
+
# serializing, the same way `five_hour_block` does.
|
|
1625
|
+
"total_tokens": getattr(cw, "total_tokens", 0),
|
|
1619
1626
|
"dollar_per_pct": cw.dollars_per_percent,
|
|
1627
|
+
# #556 S1 §3.5 — the effective cycle start, the companion of
|
|
1628
|
+
# the already-published `reset_at_utc` end. Composition needs
|
|
1629
|
+
# BOTH bounds to label the Claude leg's period, and the source
|
|
1630
|
+
# version needs them to detect a nominal rollover (§3.6).
|
|
1631
|
+
# Effective, not nominal: `_tui_build_current_week` stores this
|
|
1632
|
+
# AFTER `_apply_midweek_reset_override`.
|
|
1633
|
+
"week_start_at": _iso_z(week_start_at_utc),
|
|
1620
1634
|
"reset_at_utc": _iso_z(reset_at_utc),
|
|
1621
1635
|
"reset_in_sec":
|
|
1622
1636
|
None if reset_at_utc is None
|
|
@@ -50,7 +50,6 @@ from _cctally_config import save_config, _load_config_unlocked
|
|
|
50
50
|
from _lib_fmt import stable_sum
|
|
51
51
|
from _lib_pricing import _calculate_entry_cost, claude_usage_dict
|
|
52
52
|
from _lib_five_hour import _canonical_5h_window_key
|
|
53
|
-
from _lib_dashboard_sources import source_domain_freshness
|
|
54
53
|
from _lib_display_tz import _resolve_tz, resolve_display_tz_name
|
|
55
54
|
|
|
56
55
|
|
|
@@ -1783,12 +1782,33 @@ def _build_codex_source_share_snapshot(ls, *, state, panel: str,
|
|
|
1783
1782
|
)
|
|
1784
1783
|
|
|
1785
1784
|
|
|
1785
|
+
def _share_current_week_evidence_is_stale(state) -> bool:
|
|
1786
|
+
"""Whether ONE provider's own current-cycle evidence is stale (#556 §4.7).
|
|
1787
|
+
|
|
1788
|
+
This used to read the shared ``domain_freshness.hero`` axis, which #556 S1
|
|
1789
|
+
repointed to accounting resolvability. The aggregate ``quota`` axis is not
|
|
1790
|
+
the substitute: a stale five-hour row stales it independently of the weekly
|
|
1791
|
+
cycle this note describes. So each provider is read on its own field —
|
|
1792
|
+
Claude's percent-observation label under ``hero.current_week.freshness``,
|
|
1793
|
+
Codex's additive ``hero.cycle_freshness``, which is omitted while fresh.
|
|
1794
|
+
"""
|
|
1795
|
+
data = getattr(state, "data", None)
|
|
1796
|
+
hero = data.get("hero") if isinstance(data, Mapping) else None
|
|
1797
|
+
if not isinstance(hero, Mapping):
|
|
1798
|
+
return False
|
|
1799
|
+
if getattr(state, "source", None) == "claude":
|
|
1800
|
+
current_week = hero.get("current_week")
|
|
1801
|
+
freshness = (
|
|
1802
|
+
current_week.get("freshness")
|
|
1803
|
+
if isinstance(current_week, Mapping) else None
|
|
1804
|
+
)
|
|
1805
|
+
return isinstance(freshness, Mapping) and freshness.get("label") == "stale"
|
|
1806
|
+
return hero.get("cycle_freshness") == "stale"
|
|
1807
|
+
|
|
1808
|
+
|
|
1786
1809
|
def _share_apply_current_week_freshness(snapshot, state, panel: str):
|
|
1787
1810
|
"""Qualify retained current-week actuals with provider-local evidence age."""
|
|
1788
|
-
if (
|
|
1789
|
-
panel != "current-week"
|
|
1790
|
-
or source_domain_freshness(state, "hero") != "stale"
|
|
1791
|
-
):
|
|
1811
|
+
if panel != "current-week" or not _share_current_week_evidence_is_stale(state):
|
|
1792
1812
|
return snapshot
|
|
1793
1813
|
provider = "Claude" if state.source == "claude" else "Codex"
|
|
1794
1814
|
note = (
|
|
@@ -1261,6 +1261,24 @@ def _codex_cache_report_wire(
|
|
|
1261
1261
|
)
|
|
1262
1262
|
|
|
1263
1263
|
|
|
1264
|
+
#: Per-file terminal thread aliases, joined to their first accounting entry.
|
|
1265
|
+
#: The ``(source_root_key, source_path)`` join predicate needs the composite
|
|
1266
|
+
#: ``idx_codex_entries_root_path`` to stay linear: a single-column root index
|
|
1267
|
+
#: cannot discriminate when every rollout resolves to one provider root, so the
|
|
1268
|
+
#: join degenerates to files x entries. Module-level so the query-plan
|
|
1269
|
+
#: regression asserts THIS text rather than a copy that can drift from it.
|
|
1270
|
+
_CODEX_FILE_ALIAS_SQL = (
|
|
1271
|
+
"SELECT f.source_root_key, f.path, f.last_native_thread_id, "
|
|
1272
|
+
"f.last_session_id, MIN(e.timestamp_utc) "
|
|
1273
|
+
"FROM codex_session_files AS f "
|
|
1274
|
+
"LEFT JOIN codex_session_entries AS e "
|
|
1275
|
+
"ON e.source_root_key=f.source_root_key AND e.source_path=f.path "
|
|
1276
|
+
"WHERE f.last_native_thread_id IS NOT NULL AND f.last_native_thread_id != '' "
|
|
1277
|
+
"GROUP BY f.source_root_key, f.path, f.last_native_thread_id, f.last_session_id "
|
|
1278
|
+
"ORDER BY f.last_ingested_at DESC, f.path DESC"
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
|
|
1264
1282
|
def _codex_conversation_metadata(
|
|
1265
1283
|
cache_conn: sqlite3.Connection,
|
|
1266
1284
|
) -> dict[tuple[str, str], dict[str, object]]:
|
|
@@ -1302,16 +1320,7 @@ def _codex_conversation_metadata(
|
|
|
1302
1320
|
cwd, git_json, first_seen_at, _last_seen_at,
|
|
1303
1321
|
) in core_rows
|
|
1304
1322
|
)
|
|
1305
|
-
file_aliases = tuple(cache_conn.execute(
|
|
1306
|
-
"SELECT f.source_root_key, f.path, f.last_native_thread_id, "
|
|
1307
|
-
"f.last_session_id, MIN(e.timestamp_utc) "
|
|
1308
|
-
"FROM codex_session_files AS f "
|
|
1309
|
-
"LEFT JOIN codex_session_entries AS e "
|
|
1310
|
-
"ON e.source_root_key=f.source_root_key AND e.source_path=f.path "
|
|
1311
|
-
"WHERE f.last_native_thread_id IS NOT NULL AND f.last_native_thread_id != '' "
|
|
1312
|
-
"GROUP BY f.source_root_key, f.path, f.last_native_thread_id, f.last_session_id "
|
|
1313
|
-
"ORDER BY f.last_ingested_at DESC, f.path DESC"
|
|
1314
|
-
))
|
|
1323
|
+
file_aliases = tuple(cache_conn.execute(_CODEX_FILE_ALIAS_SQL))
|
|
1315
1324
|
native_ids = tuple(sorted({
|
|
1316
1325
|
str(native_thread_id) for _, _, native_thread_id, *_ in rows
|
|
1317
1326
|
if isinstance(native_thread_id, str) and native_thread_id
|
|
@@ -2393,6 +2402,11 @@ def refresh_codex_source_clock(
|
|
|
2393
2402
|
"hero",
|
|
2394
2403
|
),)
|
|
2395
2404
|
availability = "partial"
|
|
2405
|
+
# #556 S1 §4.1: an EXPIRED boundary is exactly the state the
|
|
2406
|
+
# accounting axis reports as stale. Build time can never see it
|
|
2407
|
+
# (`_resolve_codex_weekly_cycle` retains only `resets_at > now`),
|
|
2408
|
+
# so this clock is the only writer of that value.
|
|
2409
|
+
domain_freshness["hero"] = "stale"
|
|
2396
2410
|
cycle_changed = True
|
|
2397
2411
|
# 3. budget last
|
|
2398
2412
|
if refreshed_budget is not None:
|
|
@@ -2411,6 +2425,10 @@ def refresh_codex_source_clock(
|
|
|
2411
2425
|
data=data,
|
|
2412
2426
|
domain_freshness=domain_freshness,
|
|
2413
2427
|
clock_data=state.clock_data,
|
|
2428
|
+
# #556 S1 §3.8: this constructor lists every field explicitly, so an
|
|
2429
|
+
# omission silently drops the authoritative account count and makes the
|
|
2430
|
+
# combined figure fail closed on an idle tick that changed nothing else.
|
|
2431
|
+
account_scope=state.account_scope,
|
|
2414
2432
|
private_session_labels=state.private_session_labels,
|
|
2415
2433
|
)
|
|
2416
2434
|
return state if refreshed_state == state else refreshed_state
|
|
@@ -3408,7 +3426,6 @@ def build_codex_source_state(
|
|
|
3408
3426
|
cache_conn=context.cache_conn,
|
|
3409
3427
|
)
|
|
3410
3428
|
budget_entries = _codex_entries_from_accounting(accounting_entries)
|
|
3411
|
-
cycle_reason: str | None = None
|
|
3412
3429
|
cycles_all: list[CodexCycleBoundary] = []
|
|
3413
3430
|
try:
|
|
3414
3431
|
# Per-account list (#341 Task 2). ``cycles_all`` drives the per-account
|
|
@@ -3419,9 +3436,12 @@ def build_codex_source_state(
|
|
|
3419
3436
|
# `conflicting`.
|
|
3420
3437
|
cycles_all = _resolve_codex_weekly_cycle(quota_observations, context.now_utc)
|
|
3421
3438
|
cycle = cycles_all[0] if cycles_all else None
|
|
3422
|
-
except CodexCycleUnavailable
|
|
3439
|
+
except CodexCycleUnavailable:
|
|
3440
|
+
# #556 S1 §4.1: the reason no longer moves a freshness axis. A
|
|
3441
|
+
# `stale` reason is observation AGE, which `quota` owns; every reason
|
|
3442
|
+
# here leaves `cycle` unresolved, which `cycle_failure` below turns
|
|
3443
|
+
# into the hero failure the accounting axis actually reports.
|
|
3423
3444
|
cycle = None
|
|
3424
|
-
cycle_reason = exc.reason
|
|
3425
3445
|
cycle_failure = cycle is None and has_cached_codex_accounting_entries(
|
|
3426
3446
|
cache_conn=context.cache_conn,
|
|
3427
3447
|
)
|
|
@@ -3826,16 +3846,17 @@ def build_codex_source_state(
|
|
|
3826
3846
|
if ingest_backlog is not None else {}),
|
|
3827
3847
|
},
|
|
3828
3848
|
domain_freshness={
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3849
|
+
# #556 S1 §4.1: `hero` means current-cycle ACCOUNTING
|
|
3850
|
+
# resolvability, not observation age. A stale-but-still-future
|
|
3851
|
+
# boundary stays RESOLVED — the spend it bounds is correct, and
|
|
3852
|
+
# Codex has no background quota poll, so `stale_after_seconds`
|
|
3853
|
+
# (3600) makes an idle weekly observation stale within the hour
|
|
3854
|
+
# while nothing about the accounting changed. The percent age is
|
|
3855
|
+
# already carried by `quota` below and by the additive hero-local
|
|
3856
|
+
# `cycle_freshness` field. `_resolve_codex_weekly_cycle` retains
|
|
3857
|
+
# only boundaries with `resets_at > now`, so a resolved cycle is
|
|
3858
|
+
# never expired at build time; the idle clock owns expiry.
|
|
3859
|
+
"hero": "stale" if hero_failure else "fresh",
|
|
3839
3860
|
"quota": (
|
|
3840
3861
|
"stale"
|
|
3841
3862
|
if quota["summary"]["freshness"] == "stale"
|
package/bin/_cctally_db.py
CHANGED
|
@@ -4310,6 +4310,16 @@ def _apply_cache_schema(conn: sqlite3.Connection) -> None:
|
|
|
4310
4310
|
"CREATE INDEX IF NOT EXISTS idx_codex_entries_ts_root_conversation "
|
|
4311
4311
|
"ON codex_session_entries(timestamp_utc, source_root_key, conversation_key)"
|
|
4312
4312
|
)
|
|
4313
|
+
# The per-file alias join in `_codex_conversation_metadata` matches on
|
|
4314
|
+
# (source_root_key, source_path). `idx_codex_entries_source_root` cannot
|
|
4315
|
+
# serve it: a machine normally has ONE provider root, so a root-only search
|
|
4316
|
+
# visits every entry row for every file and the join costs files x entries
|
|
4317
|
+
# on every dashboard snapshot build. Re-derivable, so it belongs on the
|
|
4318
|
+
# unconditional path with the S3 index above rather than in a migration.
|
|
4319
|
+
conn.execute(
|
|
4320
|
+
"CREATE INDEX IF NOT EXISTS idx_codex_entries_root_path "
|
|
4321
|
+
"ON codex_session_entries(source_root_key, source_path)"
|
|
4322
|
+
)
|
|
4313
4323
|
# The per-file terminal thread facts seed a later append without rereading
|
|
4314
4324
|
# the prefix. They are nullable for old cache rows; migration 024 never
|
|
4315
4325
|
# fabricates these source facts and instead clears/rederives them.
|
package/bin/_cctally_doctor.py
CHANGED
|
@@ -38,6 +38,7 @@ import sys
|
|
|
38
38
|
|
|
39
39
|
import _cctally_core
|
|
40
40
|
import _lib_changelog
|
|
41
|
+
import _lib_journal_router
|
|
41
42
|
from _cctally_core import _now_utc, eprint, now_utc_iso, parse_iso_datetime
|
|
42
43
|
from _lib_dashboard_json import encode_dashboard_json
|
|
43
44
|
|
|
@@ -46,10 +47,10 @@ from _lib_dashboard_json import encode_dashboard_json
|
|
|
46
47
|
#: (`_lib_journal.resolve_effective_events`) reads only `evt`, `correction` and
|
|
47
48
|
#: `correction_batch`; `op` is kept because the rebuild-equivalent account
|
|
48
49
|
#: normalization is defined over evt/op records. Everything else — above all the
|
|
49
|
-
#: `obs` lines, ~97% of a real journal —
|
|
50
|
-
#:
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
#: `obs` lines, ~97% of a real journal — becomes a `None` positional slot as it
|
|
51
|
+
#: is decoded, so peak RSS tracks decision dictionaries plus one pointer per
|
|
52
|
+
#: decoded line rather than the whole decoded journal.
|
|
53
|
+
_CONFLICT_SCAN_RECORD_TYPES = _lib_journal_router.RETAINED_RECORD_TYPES
|
|
53
54
|
|
|
54
55
|
#: Doctor needs only recent evidence for this diagnostic. Bound both line count
|
|
55
56
|
#: and bytes so a corrupt single-line file cannot defeat the tail limit.
|
|
@@ -1798,10 +1799,13 @@ def _doctor_gather_state_impl(
|
|
|
1798
1799
|
# RETAIN ONLY what the selector consumes. `obs` lines are
|
|
1799
1800
|
# ~97% of a real journal (984k of 1.02M) and
|
|
1800
1801
|
# `resolve_effective_events` ignores them entirely —
|
|
1801
|
-
# keeping
|
|
1802
|
-
# result (#374 review).
|
|
1803
|
-
|
|
1804
|
-
|
|
1802
|
+
# keeping their dictionaries cost 4.3 GB of peak RSS for
|
|
1803
|
+
# an identical result (#374 review). They still consume a
|
|
1804
|
+
# lightweight slot because their physical sequence is
|
|
1805
|
+
# part of three durable violation fingerprints (#508).
|
|
1806
|
+
decoded_records.append(
|
|
1807
|
+
_lib_journal_router.selector_slot(record)
|
|
1808
|
+
)
|
|
1805
1809
|
prior_high_water = (
|
|
1806
1810
|
seg,
|
|
1807
1811
|
offset + len(raw) + 1,
|
|
@@ -1821,8 +1825,9 @@ def _doctor_gather_state_impl(
|
|
|
1821
1825
|
else _jr.resolve_cutover_claude_account()
|
|
1822
1826
|
)
|
|
1823
1827
|
for record in decoded_records:
|
|
1824
|
-
|
|
1825
|
-
|
|
1828
|
+
if record is not None:
|
|
1829
|
+
_jr._normalize_legacy_account_stamp(
|
|
1830
|
+
record, cutover_claude)
|
|
1826
1831
|
selection = _jl.resolve_effective_events(
|
|
1827
1832
|
decoded_records,
|
|
1828
1833
|
protocol_prefix_evidence=protocol_evidence,
|
|
@@ -54,10 +54,10 @@ def _read_prefix(high_water):
|
|
|
54
54
|
inline exactly as `rebuild_stats_index` captures it. Before this, those were
|
|
55
55
|
four separate whole-prefix traversals on top of this one (#496 S5 §4).
|
|
56
56
|
|
|
57
|
-
Every
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
Every decoded line contributes one selector slot. Decision records stay
|
|
58
|
+
decoded; observations and other irrelevant records become ``None``
|
|
59
|
+
placeholders, preserving the `enumerate` numbering that durable violation
|
|
60
|
+
fingerprints hash without retaining whole-prefix decoded history.
|
|
61
61
|
"""
|
|
62
62
|
if high_water is None:
|
|
63
63
|
return [], (), None, {}
|
|
@@ -106,7 +106,7 @@ def _read_prefix(high_water):
|
|
|
106
106
|
and record.get("id") == _journal.CUTOVER_OP_ID
|
|
107
107
|
):
|
|
108
108
|
cutover_captured = _journal._cutover_value_of(record)
|
|
109
|
-
records.append(record)
|
|
109
|
+
records.append(_lib_journal_router.selector_slot(record))
|
|
110
110
|
prior_high_water = record_end
|
|
111
111
|
prefix_hash = hasher.digest_at(high_water)
|
|
112
112
|
# The accumulator buffers the segment it is reading — 410 MB on the
|
|
@@ -136,7 +136,8 @@ def _read_prefix(high_water):
|
|
|
136
136
|
else:
|
|
137
137
|
cutover_claude = cutover_captured
|
|
138
138
|
for record in records:
|
|
139
|
-
|
|
139
|
+
if record is not None:
|
|
140
|
+
_journal._normalize_legacy_account_stamp(record, cutover_claude)
|
|
140
141
|
return records, tuple(evidence), prefix_hash, audit_ends
|
|
141
142
|
|
|
142
143
|
|
package/bin/_cctally_parser.py
CHANGED
|
@@ -58,6 +58,24 @@ class CLIHelpFormatter(
|
|
|
58
58
|
super().__init__(prog, **kwargs) # type: ignore[arg-type]
|
|
59
59
|
|
|
60
60
|
|
|
61
|
+
_PHYSICAL_PROVIDER_SOURCES = ("claude", "codex")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _analytics_source_choices() -> tuple[str, ...]:
|
|
65
|
+
"""Return physical providers plus the provider-separated combined view."""
|
|
66
|
+
return (*_PHYSICAL_PROVIDER_SOURCES, "all")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _provider_display_names() -> str:
|
|
70
|
+
"""Render the canonical physical-provider tuple for user-facing help."""
|
|
71
|
+
labels = tuple(source.title() for source in _PHYSICAL_PROVIDER_SOURCES)
|
|
72
|
+
if len(labels) == 1:
|
|
73
|
+
return labels[0]
|
|
74
|
+
if len(labels) == 2:
|
|
75
|
+
return " and ".join(labels)
|
|
76
|
+
return f"{', '.join(labels[:-1])}, and {labels[-1]}"
|
|
77
|
+
|
|
78
|
+
|
|
61
79
|
def _argparse_has_arg(parser, option_string: str) -> bool:
|
|
62
80
|
"""Return True if ``parser`` already registered ``option_string``."""
|
|
63
81
|
for action in parser._actions:
|
|
@@ -288,11 +306,12 @@ def _add_source_args(
|
|
|
288
306
|
"""Attach the source selector or pin a nested provider alias."""
|
|
289
307
|
if fixed_source is None:
|
|
290
308
|
parser.add_argument(
|
|
291
|
-
"--source", choices=(
|
|
292
|
-
|
|
309
|
+
"--source", choices=_analytics_source_choices(),
|
|
310
|
+
default=_PHYSICAL_PROVIDER_SOURCES[0],
|
|
311
|
+
help="Analytics provider.",
|
|
293
312
|
)
|
|
294
313
|
else:
|
|
295
|
-
if fixed_source not in
|
|
314
|
+
if fixed_source not in _PHYSICAL_PROVIDER_SOURCES:
|
|
296
315
|
raise ValueError(f"unsupported fixed source {fixed_source!r}")
|
|
297
316
|
parser.set_defaults(source=fixed_source)
|
|
298
317
|
if speed:
|
|
@@ -2717,8 +2736,9 @@ def _build_setup_parser(subparsers, name, *, help_text, xref=None):
|
|
|
2717
2736
|
formatter_class=CLIHelpFormatter,
|
|
2718
2737
|
description=textwrap.dedent(
|
|
2719
2738
|
"""\
|
|
2720
|
-
Install cctally
|
|
2721
|
-
~/.claude/settings.json
|
|
2739
|
+
Install cctally for local providers by adding Claude hook entries
|
|
2740
|
+
to ~/.claude/settings.json and native Codex handlers to every
|
|
2741
|
+
configured Codex home (all additive and idempotent). Also create
|
|
2722
2742
|
user-facing symlinks under ~/.local/bin/.
|
|
2723
2743
|
|
|
2724
2744
|
Modes (mutually exclusive):
|
|
@@ -2823,7 +2843,7 @@ def _build_transcript_parser(subparsers, name, *, help_text, xref=None):
|
|
|
2823
2843
|
formatter_class=CLIHelpFormatter)
|
|
2824
2844
|
t_search.add_argument("query", metavar="QUERY", help="Search text")
|
|
2825
2845
|
t_search.add_argument(
|
|
2826
|
-
"--source", choices=
|
|
2846
|
+
"--source", choices=_PHYSICAL_PROVIDER_SOURCES, default="claude",
|
|
2827
2847
|
help="Which provider's conversations to search (default: claude)")
|
|
2828
2848
|
t_search.add_argument(
|
|
2829
2849
|
"--account", metavar="REF", default=None,
|
|
@@ -3270,7 +3290,7 @@ def _build_hook_tick_parser(subparsers, name, *, help_text, xref=None):
|
|
|
3270
3290
|
help=argparse.SUPPRESS) # JSON string fed to mock fetch (tests only)
|
|
3271
3291
|
ht.add_argument("--foreground", action="store_true",
|
|
3272
3292
|
help=argparse.SUPPRESS) # Codex native hook wrapper
|
|
3273
|
-
ht.add_argument("--source", choices=
|
|
3293
|
+
ht.add_argument("--source", choices=_PHYSICAL_PROVIDER_SOURCES, default="claude",
|
|
3274
3294
|
help=argparse.SUPPRESS) # setup-managed native Codex hook
|
|
3275
3295
|
ht.set_defaults(func=c.cmd_hook_tick)
|
|
3276
3296
|
|
|
@@ -3572,7 +3592,7 @@ _REGISTRATION = (
|
|
|
3572
3592
|
_Reg('account', _build_account_parser, "Inspect per-provider account registry (list / show / label)", None, None),
|
|
3573
3593
|
_Reg('telemetry', _build_telemetry_parser, "Show or change anonymous install-count telemetry", None, None),
|
|
3574
3594
|
_Reg('alerts', _build_alerts_parser, "Manage threshold alerts", None, None),
|
|
3575
|
-
_Reg('setup', _build_setup_parser, "Install
|
|
3595
|
+
_Reg('setup', _build_setup_parser, "Install provider hooks/handlers + symlinks", None, None),
|
|
3576
3596
|
_Reg('db', _build_db_parser, "Migration / DB management (status, skip, unskip)", None, None),
|
|
3577
3597
|
_Reg('doctor', _build_doctor_parser, "Diagnose data freshness and install state", None, None),
|
|
3578
3598
|
_Reg('pricing-check', _build_pricing_check_parser, "Detect stale or missing embedded model pricing", None, None),
|
|
@@ -3592,28 +3612,33 @@ _REGISTRATION = (
|
|
|
3592
3612
|
|
|
3593
3613
|
def build_parser() -> argparse.ArgumentParser:
|
|
3594
3614
|
c = _cctally()
|
|
3615
|
+
providers = _provider_display_names()
|
|
3616
|
+
source_choices = ",".join(_analytics_source_choices())
|
|
3617
|
+
quick_start_reports = "\n".join(
|
|
3618
|
+
f" cctally report --source {source}"
|
|
3619
|
+
for source in _PHYSICAL_PROVIDER_SOURCES
|
|
3620
|
+
)
|
|
3595
3621
|
p = argparse.ArgumentParser(
|
|
3596
3622
|
prog="cctally",
|
|
3597
3623
|
formatter_class=CLIHelpFormatter,
|
|
3598
3624
|
description=textwrap.dedent(
|
|
3599
|
-
"""\
|
|
3600
|
-
Track
|
|
3601
|
-
in a local SQLite database.
|
|
3625
|
+
f"""\
|
|
3626
|
+
Track {providers} subscription usage and local USD cost in SQLite.
|
|
3602
3627
|
|
|
3603
3628
|
Data flow:
|
|
3604
|
-
1) Claude Code
|
|
3605
|
-
2)
|
|
3606
|
-
3)
|
|
3607
|
-
|
|
3629
|
+
1) Claude Code hooks and status lines retain Claude usage and sessions.
|
|
3630
|
+
2) Native Codex handlers retain Codex sessions and quota windows.
|
|
3631
|
+
3) Provider-aware project, diff, range-cost, cache-report, and report
|
|
3632
|
+
commands accept --source {{{source_choices}}}.
|
|
3633
|
+
4) Provider-native reports keep {providers} quota percentages and
|
|
3634
|
+
reset windows separate; `cctally codex quota` exposes Codex windows.
|
|
3608
3635
|
"""
|
|
3609
3636
|
),
|
|
3610
|
-
epilog=
|
|
3611
|
-
""
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
cctally report
|
|
3616
|
-
"""
|
|
3637
|
+
epilog=(
|
|
3638
|
+
"Quick start:\n"
|
|
3639
|
+
" cctally setup\n"
|
|
3640
|
+
f"{quick_start_reports}\n"
|
|
3641
|
+
" cctally codex quota statusline"
|
|
3617
3642
|
),
|
|
3618
3643
|
)
|
|
3619
3644
|
p.add_argument(
|