cctally 1.77.0 → 1.79.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 +28 -0
- package/README.md +2 -0
- package/bin/_cctally_cache.py +140 -2
- package/bin/_cctally_config.py +50 -0
- package/bin/_cctally_core.py +7 -4
- package/bin/_cctally_dashboard.py +352 -58
- package/bin/_cctally_dashboard_conversation.py +4 -3
- package/bin/_cctally_dashboard_envelope.py +12 -0
- package/bin/_cctally_dashboard_sources.py +25 -3
- package/bin/_cctally_doctor.py +35 -4
- package/bin/_cctally_milestone_history.py +845 -0
- package/bin/_cctally_statusline.py +128 -3
- package/bin/_cctally_tui.py +23 -0
- package/bin/_cctally_update.py +338 -15
- package/bin/_cctally_weekrefs.py +6 -2
- package/bin/_lib_codex_conversation.py +1342 -18
- package/bin/_lib_codex_conversation_export.py +21 -1
- package/bin/_lib_codex_conversation_query.py +530 -67
- package/bin/_lib_conversation_dispatch.py +2 -2
- package/bin/_lib_dashboard_json.py +43 -0
- package/bin/_lib_doctor.py +38 -0
- package/bin/_lib_milestone_history.py +158 -0
- package/bin/_lib_source_analytics.py +20 -6
- package/bin/cctally +36 -0
- package/dashboard/static/assets/index-BwvAcS_k.js +92 -0
- package/dashboard/static/assets/index-CrIlpAlQ.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-CvcCeA4L.css +0 -1
- package/dashboard/static/assets/index-DLNXAevv.js +0 -87
|
@@ -275,13 +275,22 @@ def cmd_statusline(args: argparse.Namespace) -> int:
|
|
|
275
275
|
# side effect that runs AFTER the line is printed and is FULLY guarded:
|
|
276
276
|
# persistence must NEVER break or slow rendering, so the whole call is
|
|
277
277
|
# wrapped here (the feeder itself forks detached + swallows its own
|
|
278
|
-
# errors, but this is belt-and-suspenders). Absence of rate_limits
|
|
279
|
-
# lost persist lock
|
|
280
|
-
#
|
|
278
|
+
# errors, but this is belt-and-suspenders). Absence of rate_limits or a
|
|
279
|
+
# lost persist lock degrades to a clean no-op; the authoritative OAuth
|
|
280
|
+
# confirmation below covers the missing/stale-payload case.
|
|
281
281
|
try:
|
|
282
282
|
_statusline_persist(inp)
|
|
283
283
|
except Exception:
|
|
284
284
|
pass
|
|
285
|
+
# Claude Code may replay an unchanged rate_limits object across timer
|
|
286
|
+
# renders even after the provider counters have advanced. Confirm against
|
|
287
|
+
# the authoritative OAuth surface once per account-wide timer cycle. The
|
|
288
|
+
# helper is fully detached, shares hook-tick's throttle lock/marker, and
|
|
289
|
+
# honors the existing selected-freshness and 429-backoff gates.
|
|
290
|
+
try:
|
|
291
|
+
c._statusline_oauth_tick()
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
285
294
|
return 0
|
|
286
295
|
|
|
287
296
|
|
|
@@ -1229,6 +1238,122 @@ def _statusline_persist(parsed, *, sync_for_test: bool = False) -> None:
|
|
|
1229
1238
|
c._release_persist_lock(lock_fd)
|
|
1230
1239
|
|
|
1231
1240
|
|
|
1241
|
+
def _try_acquire_statusline_oauth_lock() -> "int | None":
|
|
1242
|
+
"""Take hook-tick's account-wide OAuth throttle lock without blocking."""
|
|
1243
|
+
try:
|
|
1244
|
+
_cctally_core.APP_DIR.mkdir(parents=True, exist_ok=True)
|
|
1245
|
+
fd = os.open(
|
|
1246
|
+
_cctally_core.HOOK_TICK_THROTTLE_LOCK_PATH,
|
|
1247
|
+
os.O_WRONLY | os.O_CREAT,
|
|
1248
|
+
0o644,
|
|
1249
|
+
)
|
|
1250
|
+
except OSError:
|
|
1251
|
+
return None
|
|
1252
|
+
try:
|
|
1253
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
1254
|
+
except OSError:
|
|
1255
|
+
try:
|
|
1256
|
+
os.close(fd)
|
|
1257
|
+
except OSError:
|
|
1258
|
+
pass
|
|
1259
|
+
return None
|
|
1260
|
+
return fd
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def _release_statusline_oauth_lock(fd: int) -> None:
|
|
1264
|
+
try:
|
|
1265
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
1266
|
+
except OSError:
|
|
1267
|
+
pass
|
|
1268
|
+
try:
|
|
1269
|
+
os.close(fd)
|
|
1270
|
+
except OSError:
|
|
1271
|
+
pass
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
def _run_statusline_oauth_refresh() -> None:
|
|
1275
|
+
"""Run one already-throttled automatic OAuth confirmation."""
|
|
1276
|
+
c = _cctally()
|
|
1277
|
+
status, _ = c._hook_tick_oauth_refresh(
|
|
1278
|
+
throttle_seconds=float(_cctally_core.STATUSLINE_OAUTH_POLL_SECONDS)
|
|
1279
|
+
)
|
|
1280
|
+
if status.startswith("ok("):
|
|
1281
|
+
c._hook_tick_throttle_touch()
|
|
1282
|
+
|
|
1283
|
+
|
|
1284
|
+
def _fork_statusline_oauth_refresh(lock_fd: int) -> bool:
|
|
1285
|
+
"""Detach one OAuth confirmation while keeping ``lock_fd`` in the child."""
|
|
1286
|
+
try:
|
|
1287
|
+
pid = os.fork()
|
|
1288
|
+
except OSError:
|
|
1289
|
+
return False
|
|
1290
|
+
if pid > 0:
|
|
1291
|
+
# Do not explicitly unlock: the child inherited the same open file
|
|
1292
|
+
# description and owns the account-wide lock until its refresh ends.
|
|
1293
|
+
try:
|
|
1294
|
+
os.close(lock_fd)
|
|
1295
|
+
except OSError:
|
|
1296
|
+
pass
|
|
1297
|
+
return True
|
|
1298
|
+
|
|
1299
|
+
try:
|
|
1300
|
+
try:
|
|
1301
|
+
os.setsid()
|
|
1302
|
+
except OSError:
|
|
1303
|
+
pass
|
|
1304
|
+
try:
|
|
1305
|
+
devnull = os.open(os.devnull, os.O_RDWR)
|
|
1306
|
+
os.dup2(devnull, 0)
|
|
1307
|
+
os.dup2(devnull, 1)
|
|
1308
|
+
os.dup2(devnull, 2)
|
|
1309
|
+
if devnull > 2:
|
|
1310
|
+
os.close(devnull)
|
|
1311
|
+
except OSError:
|
|
1312
|
+
pass
|
|
1313
|
+
_run_statusline_oauth_refresh()
|
|
1314
|
+
except BaseException:
|
|
1315
|
+
pass
|
|
1316
|
+
finally:
|
|
1317
|
+
_release_statusline_oauth_lock(lock_fd)
|
|
1318
|
+
os._exit(0)
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
def _statusline_oauth_tick(*, sync_for_test: bool = False) -> None:
|
|
1322
|
+
"""Schedule one bounded authoritative refresh from a statusline timer.
|
|
1323
|
+
|
|
1324
|
+
Fast prechecks avoid forks while selected data is fresh, a shared 429
|
|
1325
|
+
deadline is active, or another session/hook owns the account-wide poll.
|
|
1326
|
+
Every condition is rechecked under hook-tick's throttle lock before the
|
|
1327
|
+
detached child starts.
|
|
1328
|
+
"""
|
|
1329
|
+
c = _cctally()
|
|
1330
|
+
interval = float(_cctally_core.STATUSLINE_OAUTH_POLL_SECONDS)
|
|
1331
|
+
if c._statusline_observe_age_seconds() < interval:
|
|
1332
|
+
return
|
|
1333
|
+
if c._oauth_backoff_remaining_seconds() > 0:
|
|
1334
|
+
return
|
|
1335
|
+
if c._hook_tick_throttle_age_seconds() < interval:
|
|
1336
|
+
return
|
|
1337
|
+
lock_fd = _try_acquire_statusline_oauth_lock()
|
|
1338
|
+
if lock_fd is None:
|
|
1339
|
+
return
|
|
1340
|
+
try:
|
|
1341
|
+
if c._statusline_observe_age_seconds() < interval:
|
|
1342
|
+
return
|
|
1343
|
+
if c._oauth_backoff_remaining_seconds() > 0:
|
|
1344
|
+
return
|
|
1345
|
+
if c._hook_tick_throttle_age_seconds() < interval:
|
|
1346
|
+
return
|
|
1347
|
+
if sync_for_test:
|
|
1348
|
+
_run_statusline_oauth_refresh()
|
|
1349
|
+
return
|
|
1350
|
+
if _fork_statusline_oauth_refresh(lock_fd):
|
|
1351
|
+
lock_fd = -1
|
|
1352
|
+
finally:
|
|
1353
|
+
if lock_fd >= 0:
|
|
1354
|
+
_release_statusline_oauth_lock(lock_fd)
|
|
1355
|
+
|
|
1356
|
+
|
|
1232
1357
|
def _resolve_context_window(model_id, warn_once) -> "int | None":
|
|
1233
1358
|
"""Look up ``model_id`` in ``CLAUDE_MODEL_CONTEXT_WINDOWS``; fall back
|
|
1234
1359
|
to a family-substring match against
|
package/bin/_cctally_tui.py
CHANGED
|
@@ -1066,6 +1066,13 @@ class DataSnapshot:
|
|
|
1066
1066
|
# at sync-thread time so ``snapshot_to_envelope`` stays a pure
|
|
1067
1067
|
# renderer; empty list when no current 5h block is bound.
|
|
1068
1068
|
five_hour_milestones: list[dict] = field(default_factory=list)
|
|
1069
|
+
# ---- hero-modal historical milestones (spec §1a/§3) ----
|
|
1070
|
+
# Compact per-week navigation index for the CurrentWeekModal's week
|
|
1071
|
+
# chip (build_claude_week_index). Built ONLY on the non-idle rebuild
|
|
1072
|
+
# and stored here so ``snapshot_to_envelope`` stays a pure serializer;
|
|
1073
|
+
# the idle path carries it forward via ``dataclasses.replace``. Empty
|
|
1074
|
+
# list on first paint / when the DB read fails.
|
|
1075
|
+
week_index: list[dict] = field(default_factory=list)
|
|
1069
1076
|
# ---- view-model unification (Bundle 1): pre-computed totals ----
|
|
1070
1077
|
# Populated by the sync thread as sum-over-visible-rows over the
|
|
1071
1078
|
# panel rows ``_dashboard_build_{daily,monthly,weekly}_periods``
|
|
@@ -3180,6 +3187,18 @@ def _tui_build_snapshot(
|
|
|
3180
3187
|
fh_milestones = _tui_build_five_hour_milestones(conn, win_key)
|
|
3181
3188
|
except Exception as exc:
|
|
3182
3189
|
errors.append(f"five-hour-milestones: {exc}")
|
|
3190
|
+
# ---- hero-modal historical milestones week index (spec §1a/§3) ----
|
|
3191
|
+
# Built ONLY here on the non-idle rebuild (the idle short-circuit
|
|
3192
|
+
# returns before this phase and carries the prior index forward via
|
|
3193
|
+
# ``dataclasses.replace``), so an idle dashboard issues NONE of the
|
|
3194
|
+
# index queries. Reached via the ``cctally`` namespace so tests can
|
|
3195
|
+
# monkeypatch it (a call-counter drives the hot-path guard).
|
|
3196
|
+
week_index: list[dict] = []
|
|
3197
|
+
with _perf.phase("build.week_index"):
|
|
3198
|
+
try:
|
|
3199
|
+
week_index = sys.modules["cctally"].build_claude_week_index(conn)
|
|
3200
|
+
except Exception as exc:
|
|
3201
|
+
errors.append(f"week-index: {exc}")
|
|
3183
3202
|
# ---- Projects panel + modal envelope (spec §5.2, plan Task 1) -----
|
|
3184
3203
|
# Per-tick aggregation lives on the sync thread; the dashboard's
|
|
3185
3204
|
# pure ``snapshot_to_envelope`` reads ``snap.projects_envelope``
|
|
@@ -3376,6 +3395,7 @@ def _tui_build_snapshot(
|
|
|
3376
3395
|
daily_panel=daily_panel,
|
|
3377
3396
|
alerts=alerts,
|
|
3378
3397
|
five_hour_milestones=fh_milestones,
|
|
3398
|
+
week_index=week_index,
|
|
3379
3399
|
daily_total_cost_usd=daily_total_cost_usd,
|
|
3380
3400
|
daily_total_tokens=daily_total_tokens,
|
|
3381
3401
|
monthly_total_cost_usd=monthly_total_cost_usd,
|
|
@@ -4165,6 +4185,9 @@ class _TuiSyncThread:
|
|
|
4165
4185
|
last_sync_error=f"sync crashed: {exc}",
|
|
4166
4186
|
generated_at=dt.datetime.now(dt.timezone.utc),
|
|
4167
4187
|
percent_milestones=prev.percent_milestones,
|
|
4188
|
+
# Carry the navigation index forward on a sync crash so the
|
|
4189
|
+
# hero modal's week chip stays usable (spec §3 idle-carry).
|
|
4190
|
+
week_index=prev.week_index,
|
|
4168
4191
|
weekly_history=prev.weekly_history,
|
|
4169
4192
|
weekly_periods=prev.weekly_periods,
|
|
4170
4193
|
monthly_periods=prev.monthly_periods,
|