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.
@@ -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, a
279
- # lost persist lock, or the throttle window all degrade to a clean
280
- # no-op; the OAuth backfill covers the "no statusline feeding" case.
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
@@ -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,