cctally 1.85.1 → 1.87.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 +16 -0
- package/bin/_cctally_cache.py +388 -35
- package/bin/_cctally_dashboard_sources.py +66 -33
- package/bin/_cctally_db.py +117 -1
- package/bin/_cctally_doctor.py +16 -1
- package/bin/_cctally_journal.py +5 -0
- package/bin/_cctally_quota.py +103 -11
- package/bin/_cctally_record.py +32 -6
- package/bin/_lib_aggregators.py +3 -0
- package/bin/_lib_codex_account_adoption.py +173 -0
- package/bin/_lib_doctor.py +28 -0
- package/bin/_lib_quota.py +170 -6
- package/dashboard/static/assets/index-B0ZCsoxI.css +1 -0
- package/dashboard/static/assets/index-Bvp8mxtz.js +92 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/index-DlVVJeS4.js +0 -92
- package/dashboard/static/assets/index-OYBkyglj.css +0 -1
|
@@ -162,6 +162,7 @@ class CodexWeeklyPeriod:
|
|
|
162
162
|
end_at: dt.datetime
|
|
163
163
|
source_root_keys: tuple[str, ...]
|
|
164
164
|
used_percent: float | None = None
|
|
165
|
+
account_keys: tuple[str, ...] = ()
|
|
165
166
|
|
|
166
167
|
|
|
167
168
|
def _codex_history_row_is_model_scoped(row: object) -> bool:
|
|
@@ -506,8 +507,9 @@ def _codex_weekly_periods(
|
|
|
506
507
|
``max(...)`` hands the focused account the OTHER account's percentage —
|
|
507
508
|
the never-combine violation D6 forbids — while ``end_at = min(resets_at,
|
|
508
509
|
next_start)`` clips one account's week at the other's start. ``None`` keeps
|
|
509
|
-
the merged "All accounts" read
|
|
510
|
-
|
|
510
|
+
the merged "All accounts" read used by the parent. Every merged boundary
|
|
511
|
+
retains the account-key set that contributed its percentage; the parent
|
|
512
|
+
publishes that set only under the existing multi-account decoration gate.
|
|
511
513
|
|
|
512
514
|
The predicate is strict equality, deliberately NOT the one-directional
|
|
513
515
|
``(account, unattributed)`` widening ``_codex_five_hour_rows`` uses: that
|
|
@@ -528,8 +530,8 @@ def _codex_weekly_periods(
|
|
|
528
530
|
account_params: tuple = () if account_key is None else (account_key,)
|
|
529
531
|
try:
|
|
530
532
|
rows = stats_conn.execute(
|
|
531
|
-
"SELECT source_root_key, logical_limit_key, limit_name,
|
|
532
|
-
"nominal_start_at_utc, current_percent "
|
|
533
|
+
"SELECT source_root_key, account_key, logical_limit_key, limit_name, "
|
|
534
|
+
"resets_at_utc, nominal_start_at_utc, current_percent "
|
|
533
535
|
"FROM quota_window_blocks "
|
|
534
536
|
"WHERE source='codex' AND window_minutes=10080 "
|
|
535
537
|
f"AND source_root_key IN ({placeholders}) AND orphaned_at IS NULL "
|
|
@@ -547,10 +549,10 @@ def _codex_weekly_periods(
|
|
|
547
549
|
# live boundary together with its own durable row, so the flag is OR-ed on
|
|
548
550
|
# merge rather than taken from either side.
|
|
549
551
|
raw_boundaries: list[
|
|
550
|
-
tuple[dt.datetime, dt.datetime, set[str], list[float], bool]
|
|
552
|
+
tuple[dt.datetime, dt.datetime, set[str], list[float], set[str], bool]
|
|
551
553
|
] = []
|
|
552
554
|
|
|
553
|
-
for (root_key, logical_limit_key, limit_name, resets_at_raw,
|
|
555
|
+
for (root_key, row_account_key, logical_limit_key, limit_name, resets_at_raw,
|
|
554
556
|
start_at_raw, current_percent) in rows:
|
|
555
557
|
if is_model_scoped_codex_quota(logical_limit_key, limit_name):
|
|
556
558
|
continue
|
|
@@ -568,7 +570,14 @@ def _codex_weekly_periods(
|
|
|
568
570
|
used_values = []
|
|
569
571
|
if isinstance(current_percent, (int, float)) and not isinstance(current_percent, bool):
|
|
570
572
|
used_values.append(float(current_percent))
|
|
571
|
-
raw_boundaries.append((
|
|
573
|
+
raw_boundaries.append((
|
|
574
|
+
start_at,
|
|
575
|
+
resets_at,
|
|
576
|
+
{str(root_key)},
|
|
577
|
+
used_values,
|
|
578
|
+
{str(row_account_key or _lib_accounts.UNATTRIBUTED)},
|
|
579
|
+
False,
|
|
580
|
+
))
|
|
572
581
|
|
|
573
582
|
# `active_cycle is None` is the case §7.4 calls out explicitly: no boundary
|
|
574
583
|
# is live, so nothing is exempt and every period clips exactly as before.
|
|
@@ -578,13 +587,18 @@ def _codex_weekly_periods(
|
|
|
578
587
|
active_cycle.resets_at.astimezone(UTC),
|
|
579
588
|
set(active_cycle.source_root_keys),
|
|
580
589
|
[active_cycle.used_percent] if active_cycle.used_percent is not None else [],
|
|
590
|
+
{
|
|
591
|
+
active_cycle.quota_identity.account_key
|
|
592
|
+
if active_cycle.quota_identity is not None
|
|
593
|
+
else _lib_accounts.UNATTRIBUTED
|
|
594
|
+
},
|
|
581
595
|
True,
|
|
582
596
|
))
|
|
583
597
|
|
|
584
598
|
ordered: list[
|
|
585
|
-
tuple[dt.datetime, dt.datetime, set[str], list[float], bool]
|
|
599
|
+
tuple[dt.datetime, dt.datetime, set[str], list[float], set[str], bool]
|
|
586
600
|
] = []
|
|
587
|
-
for start_at, resets_at, period_roots, used_values, is_live in sorted(
|
|
601
|
+
for start_at, resets_at, period_roots, used_values, period_accounts, is_live in sorted(
|
|
588
602
|
raw_boundaries, key=lambda item: (item[0], item[1]),
|
|
589
603
|
):
|
|
590
604
|
if (
|
|
@@ -592,19 +606,26 @@ def _codex_weekly_periods(
|
|
|
592
606
|
and (start_at - ordered[-1][0]).total_seconds()
|
|
593
607
|
< _FIVE_HOUR_JITTER_FLOOR_SECONDS
|
|
594
608
|
):
|
|
595
|
-
|
|
609
|
+
(
|
|
610
|
+
first_start, latest_reset, existing_roots, existing_used,
|
|
611
|
+
existing_accounts, existing_live,
|
|
612
|
+
) = ordered[-1]
|
|
596
613
|
existing_roots.update(period_roots)
|
|
597
614
|
existing_used.extend(used_values)
|
|
615
|
+
existing_accounts.update(period_accounts)
|
|
598
616
|
ordered[-1] = (
|
|
599
617
|
first_start, max(latest_reset, resets_at), existing_roots, existing_used,
|
|
600
|
-
existing_live or is_live,
|
|
618
|
+
existing_accounts, existing_live or is_live,
|
|
601
619
|
)
|
|
602
620
|
else:
|
|
603
621
|
ordered.append((
|
|
604
|
-
start_at, resets_at, set(period_roots), list(used_values),
|
|
622
|
+
start_at, resets_at, set(period_roots), list(used_values),
|
|
623
|
+
set(period_accounts), is_live,
|
|
605
624
|
))
|
|
606
625
|
periods: list[CodexWeeklyPeriod] = []
|
|
607
|
-
for index, (
|
|
626
|
+
for index, (
|
|
627
|
+
start_at, resets_at, period_roots, used_values, period_accounts, is_live,
|
|
628
|
+
) in enumerate(ordered):
|
|
608
629
|
next_start = ordered[index + 1][0] if index + 1 < len(ordered) else None
|
|
609
630
|
# The live cycle always ends at its own reset (#373 §7.4).
|
|
610
631
|
if is_live:
|
|
@@ -617,6 +638,7 @@ def _codex_weekly_periods(
|
|
|
617
638
|
end_at=end_at,
|
|
618
639
|
source_root_keys=tuple(sorted(period_roots)),
|
|
619
640
|
used_percent=max(used_values) if used_values else None,
|
|
641
|
+
account_keys=tuple(sorted(period_accounts)),
|
|
620
642
|
))
|
|
621
643
|
return tuple(periods)
|
|
622
644
|
|
|
@@ -929,6 +951,9 @@ def _bucket_wire(bucket: Any) -> dict[str, object]:
|
|
|
929
951
|
value = getattr(bucket, name, None)
|
|
930
952
|
if value is not None:
|
|
931
953
|
result[name] = value
|
|
954
|
+
account_keys = getattr(bucket, "account_keys", ())
|
|
955
|
+
if account_keys:
|
|
956
|
+
result["account_keys"] = tuple(account_keys)
|
|
932
957
|
return result
|
|
933
958
|
|
|
934
959
|
|
|
@@ -2474,11 +2499,13 @@ def _build_codex_native_weekly_view(
|
|
|
2474
2499
|
display_tz_name: str | None,
|
|
2475
2500
|
speed: str,
|
|
2476
2501
|
account_key: str | None = None,
|
|
2502
|
+
include_account_keys: bool = False,
|
|
2477
2503
|
) -> CodexWeeklyView:
|
|
2478
2504
|
"""Aggregate Codex cost into observed native quota-cycle segments.
|
|
2479
2505
|
|
|
2480
2506
|
``account_key`` scopes the durable boundary read to one account (#416
|
|
2481
|
-
Slice 3A review B1); ``None`` is the merged parent read
|
|
2507
|
+
Slice 3A review B1); ``None`` is the merged parent read. The additive
|
|
2508
|
+
ownership axis is emitted only when ``include_account_keys`` is true.
|
|
2482
2509
|
"""
|
|
2483
2510
|
periods = _codex_weekly_periods(
|
|
2484
2511
|
stats_conn,
|
|
@@ -2532,6 +2559,10 @@ def _build_codex_native_weekly_view(
|
|
|
2532
2559
|
and periods_by_bucket[row.bucket].used_percent > 0
|
|
2533
2560
|
else None
|
|
2534
2561
|
),
|
|
2562
|
+
account_keys=(
|
|
2563
|
+
periods_by_bucket[row.bucket].account_keys
|
|
2564
|
+
if include_account_keys else ()
|
|
2565
|
+
),
|
|
2535
2566
|
)
|
|
2536
2567
|
for row in rows
|
|
2537
2568
|
)
|
|
@@ -2802,14 +2833,11 @@ def _codex_account_scopes_wire(
|
|
|
2802
2833
|
context, account_observations, accounting_entries=rows,
|
|
2803
2834
|
account_key=key,
|
|
2804
2835
|
)
|
|
2805
|
-
#
|
|
2806
|
-
#
|
|
2807
|
-
#
|
|
2808
|
-
#
|
|
2809
|
-
#
|
|
2810
|
-
# has (its own `CodexCycleBoundary` plus `stats_conn`), so it is built
|
|
2811
|
-
# genuinely rather than declared parent-only. No cycle => `()`, an
|
|
2812
|
-
# honest empty state, never another account's ledger.
|
|
2836
|
+
# Each decorated child owns the only honest cycle index for that
|
|
2837
|
+
# account. Reusing a merged parent index here would render account A's
|
|
2838
|
+
# milestone HISTORY on account B's hero. The index is derivable from
|
|
2839
|
+
# the child's own `CodexCycleBoundary` plus `stats_conn`; no cycle =>
|
|
2840
|
+
# `()`, an honest empty state, never another account's ledger.
|
|
2813
2841
|
cycle_index: tuple = ()
|
|
2814
2842
|
if cycle is not None and not hero_failure:
|
|
2815
2843
|
try:
|
|
@@ -3166,6 +3194,15 @@ def build_codex_source_state(
|
|
|
3166
3194
|
monthly = build_codex_monthly_view(
|
|
3167
3195
|
entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
|
|
3168
3196
|
)
|
|
3197
|
+
# R8 gate, resolved once before the parent weekly projection so that only
|
|
3198
|
+
# a decorated merged row gains the additive account axis. Focused children
|
|
3199
|
+
# and <=1-real-account providers remain byte-identical.
|
|
3200
|
+
try:
|
|
3201
|
+
import _cctally_account
|
|
3202
|
+
_codex_decorated = _cctally_account.provider_is_decorated(
|
|
3203
|
+
context.stats_conn, "codex")
|
|
3204
|
+
except Exception:
|
|
3205
|
+
_codex_decorated = False
|
|
3169
3206
|
weekly = _build_codex_native_weekly_view(
|
|
3170
3207
|
context.stats_conn,
|
|
3171
3208
|
visible_accounting_entries,
|
|
@@ -3174,6 +3211,7 @@ def build_codex_source_state(
|
|
|
3174
3211
|
now_utc=context.now_utc,
|
|
3175
3212
|
display_tz_name=context.display_tz_name,
|
|
3176
3213
|
speed=context.speed,
|
|
3214
|
+
include_account_keys=_codex_decorated,
|
|
3177
3215
|
)
|
|
3178
3216
|
sessions = (
|
|
3179
3217
|
build_rooted_codex_session_view(
|
|
@@ -3196,12 +3234,6 @@ def build_codex_source_state(
|
|
|
3196
3234
|
# `accounts[]` cards, `hero.cycles[]`, and the `account_scopes` children —
|
|
3197
3235
|
# hangs off this single boolean, so a <=1-real-account install is provably
|
|
3198
3236
|
# byte-identical by construction rather than by golden observation.
|
|
3199
|
-
try:
|
|
3200
|
-
import _cctally_account
|
|
3201
|
-
_codex_decorated = _cctally_account.provider_is_decorated(
|
|
3202
|
-
context.stats_conn, "codex")
|
|
3203
|
-
except Exception:
|
|
3204
|
-
_codex_decorated = False
|
|
3205
3237
|
quota_blocks = _quota_wire(
|
|
3206
3238
|
context.stats_conn,
|
|
3207
3239
|
accounting_entries=visible_accounting_entries,
|
|
@@ -3210,12 +3242,13 @@ def build_codex_source_state(
|
|
|
3210
3242
|
display_tz_name=context.display_tz_name,
|
|
3211
3243
|
decorated=_codex_decorated,
|
|
3212
3244
|
)
|
|
3213
|
-
# Hero-modal historical-milestone navigation index (spec §1c, §3).
|
|
3214
|
-
#
|
|
3215
|
-
#
|
|
3216
|
-
#
|
|
3245
|
+
# Hero-modal historical-milestone navigation index (spec §1c, §3). A
|
|
3246
|
+
# decorated parent has no single cycle history: its representative
|
|
3247
|
+
# `cycle` belongs to one account, while every child builds its own index
|
|
3248
|
+
# above. Preserve the parent index only for the byte-stable undecorated
|
|
3249
|
+
# shape. Guarded: an index failure must never fail the codex source build.
|
|
3217
3250
|
cycle_index: tuple = ()
|
|
3218
|
-
if cycle is not None and not hero_failure:
|
|
3251
|
+
if not _codex_decorated and cycle is not None and not hero_failure:
|
|
3219
3252
|
try:
|
|
3220
3253
|
cycle_index = tuple(
|
|
3221
3254
|
sys.modules["cctally"].build_codex_cycle_index(
|
package/bin/_cctally_db.py
CHANGED
|
@@ -5997,7 +5997,7 @@ def _032_codex_canonical_reset_anchor(conn: sqlite3.Connection) -> None:
|
|
|
5997
5997
|
updates: list[tuple[str, int]] = []
|
|
5998
5998
|
for row in conn.execute(
|
|
5999
5999
|
"SELECT id, source_root_key, observed_slot, logical_limit_key, "
|
|
6000
|
-
" window_minutes, resets_at_utc "
|
|
6000
|
+
" window_minutes, resets_at_utc, source_path, line_offset "
|
|
6001
6001
|
" FROM quota_window_snapshots "
|
|
6002
6002
|
" WHERE source = 'codex' AND canonical_resets_at_utc IS NULL "
|
|
6003
6003
|
" AND source_root_key IS NOT NULL AND observed_slot IS NOT NULL "
|
|
@@ -6007,6 +6007,7 @@ def _032_codex_canonical_reset_anchor(conn: sqlite3.Connection) -> None:
|
|
|
6007
6007
|
source_root_key=row[1], observed_slot=row[2],
|
|
6008
6008
|
logical_limit_key=row[3], window_minutes=row[4],
|
|
6009
6009
|
resets_at_utc=row[5],
|
|
6010
|
+
source_path=row[6], line_offset=row[7],
|
|
6010
6011
|
)
|
|
6011
6012
|
if anchor is not None:
|
|
6012
6013
|
updates.append((anchor, int(row[0])))
|
|
@@ -6019,6 +6020,121 @@ def _032_codex_canonical_reset_anchor(conn: sqlite3.Connection) -> None:
|
|
|
6019
6020
|
conn.commit()
|
|
6020
6021
|
|
|
6021
6022
|
|
|
6023
|
+
@cache_migration("033_codex_reset_anchor_component_closure")
|
|
6024
|
+
def _033_codex_reset_anchor_component_closure(
|
|
6025
|
+
conn: sqlite3.Connection,
|
|
6026
|
+
) -> None:
|
|
6027
|
+
"""#425: converge migration-032 chain-of-neighbours splits.
|
|
6028
|
+
|
|
6029
|
+
Migration 032 compared each raw reset only with established anchors. Real
|
|
6030
|
+
production history contains tolerance-connected chains whose endpoints are
|
|
6031
|
+
more than 600 seconds apart, leaving one physical window under multiple
|
|
6032
|
+
anchors. Rebuild the component closure over the complete population while
|
|
6033
|
+
retaining the deterministic first observation as the winning anchor.
|
|
6034
|
+
|
|
6035
|
+
Raw ``resets_at_utc`` evidence is never rewritten. Re-running computes the
|
|
6036
|
+
same full-population mapping and therefore changes no rows.
|
|
6037
|
+
"""
|
|
6038
|
+
import _cctally_cache as cache_mod
|
|
6039
|
+
import _lib_quota
|
|
6040
|
+
|
|
6041
|
+
cols = {
|
|
6042
|
+
str(row[1]) for row in conn.execute(
|
|
6043
|
+
"PRAGMA table_info(quota_window_snapshots)")
|
|
6044
|
+
}
|
|
6045
|
+
if "canonical_resets_at_utc" not in cols:
|
|
6046
|
+
return
|
|
6047
|
+
|
|
6048
|
+
groups: dict[tuple, _lib_quota.ResetAnchorComponents] = {}
|
|
6049
|
+
evidence: list[
|
|
6050
|
+
tuple[int, dt.datetime, _lib_quota.ResetAnchorComponents, str | None]
|
|
6051
|
+
] = []
|
|
6052
|
+
for row in conn.execute(
|
|
6053
|
+
"SELECT id, source_root_key, observed_slot, logical_limit_key, "
|
|
6054
|
+
" window_minutes, resets_at_utc, canonical_resets_at_utc, "
|
|
6055
|
+
" source_path, line_offset "
|
|
6056
|
+
" FROM quota_window_snapshots "
|
|
6057
|
+
" WHERE source = 'codex' "
|
|
6058
|
+
" AND source_root_key IS NOT NULL AND observed_slot IS NOT NULL "
|
|
6059
|
+
" ORDER BY source_path, line_offset, id"
|
|
6060
|
+
).fetchall():
|
|
6061
|
+
raw = cache_mod._parse_anchor_iso(row[5])
|
|
6062
|
+
if raw is None:
|
|
6063
|
+
continue
|
|
6064
|
+
group = cache_mod.CodexResetAnchorResolver.group_key(
|
|
6065
|
+
row[1], row[2], row[3], row[4])
|
|
6066
|
+
components = groups.setdefault(
|
|
6067
|
+
group, _lib_quota.ResetAnchorComponents())
|
|
6068
|
+
components.add(
|
|
6069
|
+
raw,
|
|
6070
|
+
order_key=(str(row[7]), int(row[8]), int(row[0])),
|
|
6071
|
+
)
|
|
6072
|
+
evidence.append((int(row[0]), raw, components, row[6]))
|
|
6073
|
+
|
|
6074
|
+
updates: list[tuple[str, int]] = []
|
|
6075
|
+
for row_id, raw, components, stored in evidence:
|
|
6076
|
+
canonical = cache_mod._codex_anchor_iso(components.canonical(raw))
|
|
6077
|
+
if canonical != stored:
|
|
6078
|
+
updates.append((canonical, row_id))
|
|
6079
|
+
if updates:
|
|
6080
|
+
conn.executemany(
|
|
6081
|
+
"UPDATE quota_window_snapshots SET canonical_resets_at_utc = ? "
|
|
6082
|
+
"WHERE id = ?",
|
|
6083
|
+
updates,
|
|
6084
|
+
)
|
|
6085
|
+
conn.commit()
|
|
6086
|
+
|
|
6087
|
+
|
|
6088
|
+
@cache_migration("034_codex_window_spend_adoption")
|
|
6089
|
+
def _034_codex_window_spend_adoption(conn: sqlite3.Connection) -> None:
|
|
6090
|
+
"""One-time window-scoped spend adoption over existing Codex history.
|
|
6091
|
+
|
|
6092
|
+
Spec:
|
|
6093
|
+
``docs/superpowers/specs/2026-07-30-codex-window-scoped-spend-adoption.md``.
|
|
6094
|
+
|
|
6095
|
+
Ingest stamps every window forward from here, but rollout bytes that were
|
|
6096
|
+
already walked keep whatever per-file attribution decided for them — and for
|
|
6097
|
+
history predating the durable attribution map that is nothing at all (#416
|
|
6098
|
+
spec D1). A cycle's milestone ladder then renders ``$0.00`` for a crossing
|
|
6099
|
+
whose spend is real, because the crossing came from the folded observation
|
|
6100
|
+
axis and the dollars from the per-file axis. This handler runs the SAME pass
|
|
6101
|
+
ingest now runs, unbounded, so existing history converges in one pass.
|
|
6102
|
+
|
|
6103
|
+
The whole decision lives in ``_lib_codex_account_adoption`` and the read/write
|
|
6104
|
+
in ``_cctally_cache.apply_codex_window_spend_adoption``; re-implementing the
|
|
6105
|
+
rule here would let the migration and ingest drift.
|
|
6106
|
+
|
|
6107
|
+
Idempotent by construction: only rows whose ``account_key`` is ``NULL`` or
|
|
6108
|
+
empty are candidates, so a re-run over its own output writes nothing. NO
|
|
6109
|
+
self-stamp — the dispatcher central-stamps on a clean return (#140).
|
|
6110
|
+
|
|
6111
|
+
Takes the Codex provider flock like handlers 024-027: this writes a
|
|
6112
|
+
Codex-derived table, so a mid-walk ``sync_codex_cache`` must not interleave.
|
|
6113
|
+
On contention it DEFERS (``MigrationGateNotMet``) before touching any data —
|
|
6114
|
+
the safe side, and free here because that same sync runs the identical pass
|
|
6115
|
+
at its own end anyway.
|
|
6116
|
+
|
|
6117
|
+
``BEGIN IMMEDIATE`` before the read, like handler 024: the pass reads the
|
|
6118
|
+
folded window evidence and then writes back a plan derived from it, so it
|
|
6119
|
+
must hold the write lock for the whole read-then-write rather than upgrade
|
|
6120
|
+
part way through it.
|
|
6121
|
+
"""
|
|
6122
|
+
import _cctally_cache as cache_mod
|
|
6123
|
+
|
|
6124
|
+
held = _acquire_cache_db_codex_provider_flock(
|
|
6125
|
+
conn, migration="034 window spend adoption")
|
|
6126
|
+
try:
|
|
6127
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
6128
|
+
try:
|
|
6129
|
+
cache_mod.apply_codex_window_spend_adoption(conn)
|
|
6130
|
+
conn.commit()
|
|
6131
|
+
except Exception:
|
|
6132
|
+
conn.rollback()
|
|
6133
|
+
raise
|
|
6134
|
+
finally:
|
|
6135
|
+
_release_cache_db_writer_flocks(held)
|
|
6136
|
+
|
|
6137
|
+
|
|
6022
6138
|
# === Region 7d: Stats migration 008_recompute_weekly_cost_snapshots_dedup_fix ===
|
|
6023
6139
|
|
|
6024
6140
|
@stats_migration("008_recompute_weekly_cost_snapshots_dedup_fix")
|
package/bin/_cctally_doctor.py
CHANGED
|
@@ -995,6 +995,7 @@ def _doctor_gather_state_impl(
|
|
|
995
995
|
codex_last_entry_at = None
|
|
996
996
|
codex_project_metadata_health = None
|
|
997
997
|
codex_project_metadata_error = None
|
|
998
|
+
codex_null_reset_anchors = 0
|
|
998
999
|
try:
|
|
999
1000
|
if _cache_probe_allowed and _cctally_core.CACHE_DB_PATH.exists():
|
|
1000
1001
|
conn = sqlite3.connect(str(_cctally_core.CACHE_DB_PATH))
|
|
@@ -1008,6 +1009,18 @@ def _doctor_gather_state_impl(
|
|
|
1008
1009
|
codex_last_entry_at = parse_iso_datetime(
|
|
1009
1010
|
row[1], "codex_session_entries.timestamp_utc",
|
|
1010
1011
|
).astimezone(dt.timezone.utc)
|
|
1012
|
+
try:
|
|
1013
|
+
row = conn.execute(
|
|
1014
|
+
"SELECT COUNT(*) FROM quota_window_snapshots "
|
|
1015
|
+
"WHERE source = 'codex' "
|
|
1016
|
+
"AND canonical_resets_at_utc IS NULL"
|
|
1017
|
+
).fetchone()
|
|
1018
|
+
if row and row[0] is not None:
|
|
1019
|
+
codex_null_reset_anchors = int(row[0])
|
|
1020
|
+
except sqlite3.OperationalError:
|
|
1021
|
+
# Pre-anchor cache shapes have no column to inspect. Their
|
|
1022
|
+
# pending migration is reported by the DB checks instead.
|
|
1023
|
+
pass
|
|
1011
1024
|
# Keep the health probe on the existing read-only cache
|
|
1012
1025
|
# connection. A failed probe is health evidence, not an
|
|
1013
1026
|
# empty corpus: the kernel renders it as a distinct FAIL.
|
|
@@ -1645,6 +1658,8 @@ def _doctor_gather_state_impl(
|
|
|
1645
1658
|
cctally_version = (
|
|
1646
1659
|
cctally_version_tuple[0] if cctally_version_tuple else "unknown"
|
|
1647
1660
|
)
|
|
1661
|
+
accounts_state = _gather_accounts_state(now_utc)
|
|
1662
|
+
accounts_state["codex_null_reset_anchors"] = codex_null_reset_anchors
|
|
1648
1663
|
|
|
1649
1664
|
return _lib_doctor.DoctorState(
|
|
1650
1665
|
symlink_state=symlink_state,
|
|
@@ -1742,7 +1757,7 @@ def _doctor_gather_state_impl(
|
|
|
1742
1757
|
journal_heal_incidents=journal_heal_incidents,
|
|
1743
1758
|
journal_writer_guard=journal_writer_guard,
|
|
1744
1759
|
# Multi-account attribution legs (#341).
|
|
1745
|
-
accounts_state=
|
|
1760
|
+
accounts_state=accounts_state,
|
|
1746
1761
|
cache_repair_marker=cache_repair_marker,
|
|
1747
1762
|
backup_sync_state=backup_sync_state,
|
|
1748
1763
|
)
|
package/bin/_cctally_journal.py
CHANGED
|
@@ -1489,6 +1489,8 @@ def _resolve_obs_anchor(resolver, rec: dict) -> "str | None":
|
|
|
1489
1489
|
source_root_key=root, observed_slot=slot, logical_limit_key=key,
|
|
1490
1490
|
window_minutes=p.get("window_minutes"),
|
|
1491
1491
|
resets_at_utc=p.get("resets_at_utc"),
|
|
1492
|
+
source_path=p.get("source_path"),
|
|
1493
|
+
line_offset=p.get("line_offset"),
|
|
1492
1494
|
)
|
|
1493
1495
|
except Exception: # pragma: no cover — never fail an ingest over a label
|
|
1494
1496
|
return None
|
|
@@ -1523,6 +1525,9 @@ def _apply_quota_records(cache, records) -> None:
|
|
|
1523
1525
|
for rec in records:
|
|
1524
1526
|
covered, decided = oracle.resolve(rec)
|
|
1525
1527
|
anchor = _resolve_obs_anchor(anchors, rec)
|
|
1528
|
+
if anchors is not None:
|
|
1529
|
+
anchors.apply_pending_merges()
|
|
1530
|
+
anchors.mark_file_committed()
|
|
1526
1531
|
row_values = _quota_snapshot_values(rec, anchor)
|
|
1527
1532
|
if not has_anchor:
|
|
1528
1533
|
row_values = row_values[:-1]
|
package/bin/_cctally_quota.py
CHANGED
|
@@ -240,6 +240,7 @@ def load_codex_quota_observations(
|
|
|
240
240
|
active_at: dt.datetime | None = None,
|
|
241
241
|
max_rows: int | None = None,
|
|
242
242
|
physical_signatures: dict[str, str] | None = None,
|
|
243
|
+
canonical_resets_between: "tuple[dt.datetime, dt.datetime] | None" = None,
|
|
243
244
|
) -> tuple[QuotaObservation, ...]:
|
|
244
245
|
"""Load only valid root-qualified S1 physical quota rows.
|
|
245
246
|
|
|
@@ -258,6 +259,14 @@ def load_codex_quota_observations(
|
|
|
258
259
|
supplied, exact S2 signatures are accumulated from the same cursor before
|
|
259
260
|
presentation bounds are applied, so coherence validation does not require
|
|
260
261
|
a second unbounded observation load.
|
|
262
|
+
|
|
263
|
+
``canonical_resets_between`` is an INCLUSIVE ``(low, high)`` bound on the
|
|
264
|
+
window's CANONICAL reset, applied in SQL. Unlike ``captured_at_or_after``
|
|
265
|
+
it is a window-IDENTITY bound, so it never fractures a window group: every
|
|
266
|
+
observation of one physical window shares one canonical anchor, and the
|
|
267
|
+
continuity fold below therefore still sees each retained window whole. That
|
|
268
|
+
is what lets the ingest-side spend-adoption pass bound itself to the windows
|
|
269
|
+
one sync touched instead of materializing all history every hook tick.
|
|
261
270
|
"""
|
|
262
271
|
for name, value in (
|
|
263
272
|
("captured_at_or_after", captured_at_or_after), ("active_at", active_at),
|
|
@@ -269,6 +278,19 @@ def load_codex_quota_observations(
|
|
|
269
278
|
captured_at_or_after = value.astimezone(UTC)
|
|
270
279
|
else:
|
|
271
280
|
active_at = value.astimezone(UTC)
|
|
281
|
+
if canonical_resets_between is not None:
|
|
282
|
+
if len(canonical_resets_between) != 2:
|
|
283
|
+
raise ValueError(
|
|
284
|
+
"canonical_resets_between must be a (low, high) pair")
|
|
285
|
+
bounds = []
|
|
286
|
+
for value in canonical_resets_between:
|
|
287
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
288
|
+
raise ValueError(
|
|
289
|
+
"canonical_resets_between must be timezone-aware")
|
|
290
|
+
bounds.append(value.astimezone(UTC))
|
|
291
|
+
if bounds[0] > bounds[1]:
|
|
292
|
+
raise ValueError("canonical_resets_between must be ordered")
|
|
293
|
+
canonical_resets_between = (bounds[0], bounds[1])
|
|
272
294
|
if max_rows is not None:
|
|
273
295
|
if not isinstance(max_rows, int) or isinstance(max_rows, bool) or max_rows <= 0:
|
|
274
296
|
raise ValueError("max_rows must be a positive integer or None")
|
|
@@ -352,6 +374,23 @@ def load_codex_quota_observations(
|
|
|
352
374
|
return ()
|
|
353
375
|
sql += " AND source_root_key IN (" + ",".join("?" for _ in requested) + ")"
|
|
354
376
|
params.extend(sorted(requested))
|
|
377
|
+
if canonical_resets_between is not None:
|
|
378
|
+
# COALESCE, not the bare column: a pre-032 row (or one the backfill
|
|
379
|
+
# never reached) carries NULL there and the reader falls back to the
|
|
380
|
+
# raw reset, so the bound has to fall back with it or the row would
|
|
381
|
+
# silently drop out of its own window.
|
|
382
|
+
reset_expr = (
|
|
383
|
+
"COALESCE(canonical_resets_at_utc, resets_at_utc)"
|
|
384
|
+
if has_anchor else "resets_at_utc"
|
|
385
|
+
)
|
|
386
|
+
sql += (
|
|
387
|
+
f" AND unixepoch({reset_expr}) >= unixepoch(?)"
|
|
388
|
+
f" AND unixepoch({reset_expr}) <= unixepoch(?)"
|
|
389
|
+
)
|
|
390
|
+
params.extend((
|
|
391
|
+
_utc_iso(canonical_resets_between[0]),
|
|
392
|
+
_utc_iso(canonical_resets_between[1]),
|
|
393
|
+
))
|
|
355
394
|
# When exact signatures are requested this first cursor must cover the
|
|
356
395
|
# complete root history. Otherwise apply dashboard presentation bounds
|
|
357
396
|
# in SQL so only the capped evidence crosses the SQLite/Python boundary.
|
|
@@ -494,6 +533,7 @@ def load_codex_quota_observations(
|
|
|
494
533
|
captured_at_or_after=captured_at_or_after,
|
|
495
534
|
active_at=active_at,
|
|
496
535
|
max_rows=max_rows,
|
|
536
|
+
canonical_resets_between=canonical_resets_between,
|
|
497
537
|
)
|
|
498
538
|
if max_rows is not None and len(result) > max_rows:
|
|
499
539
|
result = sorted(
|
|
@@ -966,7 +1006,9 @@ def _evaluate_quota_alerts(
|
|
|
966
1006
|
return queued
|
|
967
1007
|
|
|
968
1008
|
|
|
969
|
-
def _reanchor_terminal_events_sql(
|
|
1009
|
+
def _reanchor_terminal_events_sql(
|
|
1010
|
+
key_slots: int, minute_slots: int, reset_slots: int,
|
|
1011
|
+
) -> str:
|
|
970
1012
|
# `UPDATE OR IGNORE`, not a plain UPDATE: if this identity already carries an
|
|
971
1013
|
# anchored row at the same threshold, moving the jittered twin onto it would
|
|
972
1014
|
# violate the UNIQUE key. OR IGNORE SKIPS that move (it does not delete the
|
|
@@ -978,6 +1020,11 @@ def _reanchor_terminal_events_sql(key_slots: int, minute_slots: int) -> str:
|
|
|
978
1020
|
# transaction.
|
|
979
1021
|
keys = ",".join(f":key{i}" for i in range(key_slots))
|
|
980
1022
|
minutes = ",".join(f":min{i}" for i in range(minute_slots))
|
|
1023
|
+
member_epochs = ",".join(f":reset{i}" for i in range(reset_slots))
|
|
1024
|
+
member_clause = (
|
|
1025
|
+
f" OR unixepoch(resets_at_utc) IN ({member_epochs})"
|
|
1026
|
+
if member_epochs else ""
|
|
1027
|
+
)
|
|
981
1028
|
return (
|
|
982
1029
|
"UPDATE OR IGNORE quota_threshold_events "
|
|
983
1030
|
" SET resets_at_utc = :anchor, "
|
|
@@ -989,7 +1036,9 @@ def _reanchor_terminal_events_sql(key_slots: int, minute_slots: int) -> str:
|
|
|
989
1036
|
f" AND observed_slot = :slot AND window_minutes IN ({minutes}) "
|
|
990
1037
|
" AND (resets_at_utc <> :anchor OR logical_limit_key <> :limit_key "
|
|
991
1038
|
" OR window_minutes <> :minutes) "
|
|
992
|
-
" AND
|
|
1039
|
+
" AND ("
|
|
1040
|
+
" abs(unixepoch(resets_at_utc) - unixepoch(:anchor)) <= :tolerance"
|
|
1041
|
+
f"{member_clause})"
|
|
993
1042
|
)
|
|
994
1043
|
|
|
995
1044
|
|
|
@@ -1020,15 +1069,24 @@ def _reanchor_terminal_events(conn: sqlite3.Connection, block) -> None:
|
|
|
1020
1069
|
re-materialization (they share this body), and is idempotent: a row already
|
|
1021
1070
|
on the canonical identity is excluded by the three-way `<>` guard.
|
|
1022
1071
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1072
|
+
The reset match accepts either the original 600-second anchor neighbourhood
|
|
1073
|
+
or an exact raw reset retained by this block. The latter is required by
|
|
1074
|
+
#425's transitive component closure: an endpoint can be farther than 600s
|
|
1075
|
+
from the first-sight anchor while still joining it through retained bridge
|
|
1076
|
+
observations. Exact membership keeps the widened reach evidence-bound; a
|
|
1077
|
+
genuinely different cycle is never inferred from distance alone. The length
|
|
1078
|
+
axis remains bounded by its ±1 minute snap.
|
|
1028
1079
|
"""
|
|
1029
1080
|
identity = block.identity
|
|
1030
1081
|
keys = codex_snap_equivalent_limit_keys(identity.logical_limit_key)
|
|
1031
1082
|
minutes = codex_snap_equivalent_window_minutes(identity.window_minutes)
|
|
1083
|
+
membership_evidence = (
|
|
1084
|
+
block.physical_observations or block.observations
|
|
1085
|
+
)
|
|
1086
|
+
reset_epochs = sorted({
|
|
1087
|
+
int(observation.resets_at.timestamp())
|
|
1088
|
+
for observation in membership_evidence
|
|
1089
|
+
})
|
|
1032
1090
|
params: dict[str, object] = {
|
|
1033
1091
|
"anchor": _utc_iso(block.resets_at),
|
|
1034
1092
|
"source": identity.source,
|
|
@@ -1041,8 +1099,13 @@ def _reanchor_terminal_events(conn: sqlite3.Connection, block) -> None:
|
|
|
1041
1099
|
}
|
|
1042
1100
|
params.update({f"key{i}": value for i, value in enumerate(keys)})
|
|
1043
1101
|
params.update({f"min{i}": value for i, value in enumerate(minutes)})
|
|
1102
|
+
params.update({
|
|
1103
|
+
f"reset{i}": value for i, value in enumerate(reset_epochs)})
|
|
1044
1104
|
conn.execute(
|
|
1045
|
-
_reanchor_terminal_events_sql(
|
|
1105
|
+
_reanchor_terminal_events_sql(
|
|
1106
|
+
len(keys), len(minutes), len(reset_epochs)),
|
|
1107
|
+
params,
|
|
1108
|
+
)
|
|
1046
1109
|
|
|
1047
1110
|
|
|
1048
1111
|
def _apply_quota_projection_rows(
|
|
@@ -1441,13 +1504,30 @@ def _codex_cache_account_predicate(
|
|
|
1441
1504
|
dollars are these" — widening it IS attribution, which D1 forbids, and it
|
|
1442
1505
|
puts one row in two scopes.
|
|
1443
1506
|
|
|
1444
|
-
|
|
1507
|
+
FOUR stamping mechanisms exist and must never be conflated: the
|
|
1445
1508
|
quota-observation fold (``adopt_unidentified_observations``, per physical-
|
|
1446
1509
|
window group, landing post-fold in ``quota_window_blocks`` /
|
|
1447
1510
|
``quota_percent_milestones`` and NEVER written back to
|
|
1448
1511
|
``quota_window_snapshots``); per-file-range attribution
|
|
1449
1512
|
(``codex_file_accounts`` -> ``codex_session_entries.account_key``,
|
|
1450
|
-
``stably_absent`` -> NULL);
|
|
1513
|
+
``stably_absent`` -> NULL); WINDOW-SCOPED SPEND ADOPTION (the 2026-07-30
|
|
1514
|
+
spec — ``_lib_codex_account_adoption`` +
|
|
1515
|
+
``_cctally_cache.apply_codex_window_spend_adoption``, which stamps that same
|
|
1516
|
+
``codex_session_entries.account_key`` column at ingest from the window's
|
|
1517
|
+
single identified account); and the stats ``accounts`` registry.
|
|
1518
|
+
|
|
1519
|
+
So ``codex_session_entries.account_key`` now carries window-derived
|
|
1520
|
+
attribution IN ADDITION to per-file decisions, and that is precisely what
|
|
1521
|
+
keeps the cost read strict rather than forcing it to widen. The rule above
|
|
1522
|
+
genuinely pointed both ways for that read: the scope key comes from the
|
|
1523
|
+
observation fold while the rows came from per-file attribution — DIFFERENT
|
|
1524
|
+
mechanisms, which reads as "widen" — yet widening a cost read is attribution
|
|
1525
|
+
D1 forbids and puts one row in two scopes. The resolution is to make the ROW
|
|
1526
|
+
carry the window's answer durably, so scope key and row now agree by
|
|
1527
|
+
construction and the strict flavour is correct rather than merely safe. A
|
|
1528
|
+
row the adoption pass declined to stamp (zero or ambiguous identified
|
|
1529
|
+
accounts, an overlap whose windows disagree) is genuinely nobody's and
|
|
1530
|
+
stays in the ``unattributed`` scope, which is the honest answer.
|
|
1451
1531
|
"""
|
|
1452
1532
|
if account_key is None:
|
|
1453
1533
|
return "", ()
|
|
@@ -1575,7 +1655,19 @@ def codex_quota_breakdown(
|
|
|
1575
1655
|
renders an honest ``$0.00``; the dollars stay visible in the
|
|
1576
1656
|
``unattributed`` scope, which owns them.
|
|
1577
1657
|
|
|
1578
|
-
|
|
1658
|
+
That honest ``$0.00`` used to fire for spend that was NOT nobody's. The
|
|
1659
|
+
crossing carries the window's account (the observation fold put it there)
|
|
1660
|
+
while the rows behind it carried none, because per-file attribution had no
|
|
1661
|
+
decision covering those bytes. ``codex_session_entries.account_key`` now also
|
|
1662
|
+
carries WINDOW-DERIVED attribution — stamped durably at ingest by
|
|
1663
|
+
``apply_codex_window_spend_adoption`` under the same window key and the same
|
|
1664
|
+
single-identified-account guard the fold uses — so the ladder and the
|
|
1665
|
+
dollars agree by construction. The read stays strict on top of it precisely
|
|
1666
|
+
BECAUSE the inference is now durable: doing it here instead would re-file one
|
|
1667
|
+
row under two scopes on every read, while doing it once at ingest moves the
|
|
1668
|
+
row out of ``unattributed`` and into exactly one owner.
|
|
1669
|
+
|
|
1670
|
+
The full rule, and the four stamping mechanisms it turns on, are in
|
|
1579
1671
|
``_codex_cache_account_predicate``.
|
|
1580
1672
|
"""
|
|
1581
1673
|
reset = _parse_utc(resets_at, "resets_at") if isinstance(resets_at, str) else resets_at
|