cctally 1.20.2 → 1.20.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.20.4] - 2026-05-28
9
+
10
+ ### Changed
11
+ - **Hardened the v1.20.3 canonical-anchor bypass in `_select_non_overlapping_recorded_windows`: provenance is now passed explicitly from the loader instead of being inferred from merged weight.** v1.20.3 identified canonical anchors as `items[*].weight >= 1000`, treating the loader's `+1000` overlay as a provenance proxy. But the merged weight is `raw_snapshot_count + (1000 if canonical else 0)`, so a raw-only bucket can also reach 1000+ under bulk-imported history or any future polling-frequency change to `record-usage`. Two such raw-only buckets <5h apart would both force-restore through the bypass and render as overlapping recorded blocks. The selector now takes an explicit `canonical_anchors` keyword set, populated by `_load_recorded_five_hour_windows` from `set(canonical_intervals.keys()) - truncated_anchors` — the loader already knows which anchors came from `five_hour_blocks`, so there's no need to re-derive provenance from weight. The DP still runs over the full item set (canonical + raw) so non-canonical phantoms adjacent to a canonical anchor in a different floor bucket continue to lose by weight comparison; the force-restore step only puts back items the caller marked canonical, never any raw-only bucket regardless of its weight. `_CANONICAL_WEIGHT_THRESHOLD = 1000` stays as the loader's overlay value (still names what dominates the DP arithmetic). No user-visible behavior change under current production data — the v1.20.3 fix already worked correctly for every real input shape we've observed — but the contract is no longer fragile to changes in polling cadence or future bulk-import surfaces. Regression: new `test_select_non_overlapping_recorded_windows_high_weight_raw_does_not_bypass` covers two raw-only items at weights 1500/1200; pre-fix returned both, post-fix DP arbitrates to one. (#116 review follow-up)
12
+
13
+ ## [1.20.3] - 2026-05-28
14
+
15
+ ### Fixed
16
+ - **The dashboard's Blocks panel no longer renders the just-started active block with a `~` heuristic marker, and the previous block no longer silently disappears from the panel ~20 minutes after a 5h reset.** Both symptoms came from the same root cause in `_select_non_overlapping_recorded_windows`: when two genuinely-adjacent canonical 5h windows had `resets_at` values whose 10-minute-floored keys landed less than 5h apart (e.g. OLD `R=09:00:01Z` → floor `09:00`, NEW `R=13:59:59Z` → floor `13:50` — a 4h 50m floored-distance for a real block-pair with a 2-second sub-second overlap on the boundary), the weighted-interval-scheduling DP treated them as conflicting and dropped the lighter-weighted one. At `t≈0` after a reset that meant the NEW canonical window vanished from the selector's output, so the dashboard's Blocks panel partitioned NEW-window entries into the heuristic leftover bucket and rendered them with `anchor='heuristic'` and a `_floor_to_hour` `start_at` (the `~` chip in `BlocksPanel.tsx:33-34`); ~20 minutes later, once the NEW window's snapshot count caught up, the DP would flip and drop OLD instead, making the previous block silently disappear. CLI `cctally blocks --active` was unaffected (it queries `five_hour_blocks` directly with `is_closed=0`, bypassing this scheduler). Fix: any item carrying the canonical-source weight overlay (`_CANONICAL_WEIGHT_THRESHOLD = 1000`) is force-restored to the result set if the DP dropped it — `maybe_update_five_hour_block` already deduplicated via `_canonical_5h_window_key` pre-insert, so two canonical rows are by definition non-overlapping physically and don't need re-arbitration on a floor-distance check. The DP still runs over the full item set (canonical + raw), so a non-canonical phantom R adjacent to a canonical anchor in a different floor bucket continues to lose by weight comparison; the bypass only restores canonical items, never adds back a raw-only phantom. (#116)
17
+
8
18
  ## [1.20.2] - 2026-05-28
9
19
 
10
20
  ### Fixed
package/bin/cctally CHANGED
@@ -3951,11 +3951,27 @@ def _resolve_block_selector(
3951
3951
  return dict(row) if row else None
3952
3952
 
3953
3953
 
3954
+ # Weight overlay applied per canonical (``five_hour_blocks``) row by
3955
+ # ``_load_recorded_five_hour_windows``: ``counts[snapped] += _CANONICAL_WEIGHT_THRESHOLD``.
3956
+ # Gives canonical anchors dominant weight inside the
3957
+ # ``_select_non_overlapping_recorded_windows`` DP, so any non-canonical
3958
+ # phantom adjacent to a canonical anchor loses on weight comparison. NOT
3959
+ # used as a provenance check — the selector takes an explicit
3960
+ # ``canonical_anchors`` set from the loader for the force-restore bypass
3961
+ # (issue #116 review follow-up: raw-only buckets with bulk-imported /
3962
+ # high-frequency snapshot histories can also accumulate >= 1000 weight,
3963
+ # so the threshold conflates provenance with support count).
3964
+ _CANONICAL_WEIGHT_THRESHOLD = 1000
3965
+
3966
+
3954
3967
  def _select_non_overlapping_recorded_windows(
3955
3968
  items: list[tuple[dt.datetime, int]],
3969
+ *,
3970
+ canonical_anchors: set[dt.datetime] | None = None,
3956
3971
  ) -> list[dt.datetime]:
3957
3972
  """Pick the max-weight subset of recorded ``R`` values that respect
3958
- the 5h non-overlap constraint.
3973
+ the 5h non-overlap constraint, with canonical anchors guaranteed
3974
+ to survive.
3959
3975
 
3960
3976
  Anthropic 5h windows cannot truly overlap: the next window only
3961
3977
  opens once the previous one resets, so consecutive real ``R``
@@ -3968,8 +3984,31 @@ def _select_non_overlapping_recorded_windows(
3968
3984
  snapshots: the subset that maximizes total support wins. Tie-break
3969
3985
  in the take branch favors including more ``R`` values.
3970
3986
 
3987
+ Canonical bypass (issue #116): any ``R`` passed in ``canonical_anchors``
3988
+ came from the authoritative ``five_hour_blocks`` rollup.
3989
+ ``maybe_update_five_hour_block`` already deduped via
3990
+ ``_canonical_5h_window_key`` pre-insert, so two canonical rows are
3991
+ by definition non-overlapping physically — they only appear "in
3992
+ conflict" here when their 10-min-floored keys land less than
3993
+ ``BLOCK_DURATION`` apart, which happens at every real reset
3994
+ boundary when Anthropic's ``resets_at`` jitters sub-second across
3995
+ the boundary (e.g. OLD ``R=09:00:01Z`` floors to ``09:00``, NEW
3996
+ ``R=13:59:59Z`` floors to ``13:50`` — 4h 50m floored-distance for
3997
+ a genuinely-adjacent block pair). The DP still runs over the full
3998
+ item set so non-canonical phantoms next to a canonical anchor get
3999
+ dropped by weight comparison; the canonical-bypass only force-
4000
+ restores anchors the caller marked canonical, never adds back a
4001
+ raw-only phantom (even one whose raw weight ≥ ``_CANONICAL_WEIGHT_THRESHOLD``
4002
+ — the v1.20.3 fix used weight as a provenance proxy, which the
4003
+ review correctly flagged as conflating support count with provenance).
4004
+
3971
4005
  Args:
3972
4006
  items: ``(R, support_count)`` pairs.
4007
+ canonical_anchors: explicit set of ``R`` values sourced from
4008
+ ``five_hour_blocks``. Any present in ``items`` is guaranteed to
4009
+ appear in the result, even if the DP dropped it on the 5h
4010
+ non-overlap constraint. ``None`` / empty set = pure DP behavior
4011
+ (no bypass).
3973
4012
 
3974
4013
  Returns:
3975
4014
  Sorted ascending list of selected ``R`` values.
@@ -4012,6 +4051,14 @@ def _select_non_overlapping_recorded_windows(
4012
4051
  else:
4013
4052
  i -= 1
4014
4053
  chosen.reverse()
4054
+ # Canonical bypass: force-restore any canonical anchor the DP dropped
4055
+ # (issue #116). Intersect with items' keys so a caller passing anchors
4056
+ # outside the item set can't corrupt the result.
4057
+ if canonical_anchors:
4058
+ items_keys = {R for R, _ in items_sorted}
4059
+ present_canonical = canonical_anchors & items_keys
4060
+ if present_canonical and not present_canonical.issubset(chosen):
4061
+ return sorted(set(chosen) | present_canonical)
4015
4062
  return chosen
4016
4063
 
4017
4064
 
@@ -4297,13 +4344,21 @@ def _load_recorded_five_hour_windows(
4297
4344
  # (only credit-truncated entries land there).
4298
4345
  if snapped in block_start_overrides:
4299
4346
  truncated_anchors.add(snapped)
4300
- counts[snapped] = counts.get(snapped, 0) + 1000
4347
+ counts[snapped] = counts.get(snapped, 0) + _CANONICAL_WEIGHT_THRESHOLD
4301
4348
 
4302
4349
  non_truncated_items = [
4303
4350
  (a, w) for a, w in counts.items() if a not in truncated_anchors
4304
4351
  ]
4352
+ # Pass canonical provenance explicitly: every key currently in
4353
+ # canonical_intervals came from a `five_hour_blocks` row (raw-only
4354
+ # buckets never land in this map). Subtract truncated_anchors because
4355
+ # those bypass the DP via the separate merge below — keeping them
4356
+ # out of canonical_anchors here is a no-op for correctness but
4357
+ # mirrors the same scope as non_truncated_items for clarity.
4358
+ canonical_anchors_for_dp = set(canonical_intervals.keys()) - truncated_anchors
4305
4359
  selected_non_truncated = _select_non_overlapping_recorded_windows(
4306
- non_truncated_items
4360
+ non_truncated_items,
4361
+ canonical_anchors=canonical_anchors_for_dp,
4307
4362
  )
4308
4363
  # Merge truncated anchors back in, sorted ascending. Their non-
4309
4364
  # overlap with the surrounding canonical blocks is guaranteed by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cctally",
3
- "version": "1.20.2",
3
+ "version": "1.20.4",
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": {