cctally 1.71.0 → 1.73.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 +20 -0
- package/bin/_cctally_cache.py +501 -49
- package/bin/_cctally_core.py +20 -40
- package/bin/_cctally_dashboard_conversation.py +731 -94
- package/bin/_cctally_db.py +286 -1
- package/bin/_cctally_doctor.py +59 -0
- package/bin/_cctally_parser.py +17 -4
- package/bin/_cctally_record.py +162 -33
- package/bin/_cctally_refresh.py +58 -36
- package/bin/_cctally_statusline.py +811 -82
- package/bin/_cctally_transcript.py +223 -7
- package/bin/_lib_codex_conversation.py +509 -0
- package/bin/_lib_codex_conversation_export.py +124 -0
- package/bin/_lib_codex_conversation_query.py +1422 -0
- package/bin/_lib_codex_conversation_watch.py +323 -0
- package/bin/_lib_conversation_dispatch.py +809 -0
- package/bin/_lib_conversation_query.py +88 -0
- package/bin/_lib_conversation_retention.py +344 -0
- package/bin/_lib_doctor.py +62 -0
- package/bin/_lib_statusline_candidates.py +734 -0
- package/bin/cctally +31 -2
- package/package.json +8 -1
package/bin/_cctally_record.py
CHANGED
|
@@ -2548,6 +2548,73 @@ def _credit_json(plan, *, applied, dry_run, forced, stale_replays, hwm_before):
|
|
|
2548
2548
|
}
|
|
2549
2549
|
|
|
2550
2550
|
|
|
2551
|
+
def _revalidate_credit_plan(conn, args, *, now, at_dt, expected_plan):
|
|
2552
|
+
"""Recompute the confirmed credit plan from locked, current DB truth.
|
|
2553
|
+
|
|
2554
|
+
The caller has already completed every preview/refusal/confirmation path.
|
|
2555
|
+
Returning ``None`` is deliberately side-effect free: it means a concurrent
|
|
2556
|
+
writer changed the requested credit's basis and the user must retry rather
|
|
2557
|
+
than authorizing a different mutation than the preview showed.
|
|
2558
|
+
"""
|
|
2559
|
+
try:
|
|
2560
|
+
if getattr(args, "week", None):
|
|
2561
|
+
week_start_date = args.week
|
|
2562
|
+
ws_at, we_at = _get_canonical_boundary_for_date(conn, week_start_date)
|
|
2563
|
+
if not ws_at or not we_at:
|
|
2564
|
+
return None
|
|
2565
|
+
else:
|
|
2566
|
+
fetched = _fetch_current_week_snapshots(conn, at_dt)
|
|
2567
|
+
if fetched is None:
|
|
2568
|
+
return None
|
|
2569
|
+
ws_at, we_at, _samples = fetched
|
|
2570
|
+
ws_at = ws_at if isinstance(ws_at, str) else ws_at.isoformat(timespec="seconds")
|
|
2571
|
+
we_at = we_at if isinstance(we_at, str) else we_at.isoformat(timespec="seconds")
|
|
2572
|
+
week_start_date = parse_iso_datetime(ws_at, "ws_at").date().isoformat()
|
|
2573
|
+
existing = conn.execute(
|
|
2574
|
+
"SELECT id, effective_at_utc, observed_pre_credit_pct "
|
|
2575
|
+
"FROM weekly_credit_floors WHERE week_start_date=? "
|
|
2576
|
+
"ORDER BY unixepoch(effective_at_utc) DESC, id DESC LIMIT 1",
|
|
2577
|
+
(week_start_date,),
|
|
2578
|
+
).fetchone()
|
|
2579
|
+
is_force = bool(getattr(args, "force", False))
|
|
2580
|
+
if getattr(args, "from_pct", None) is not None:
|
|
2581
|
+
from_pct, from_source = float(args.from_pct), "explicit"
|
|
2582
|
+
elif existing is not None and existing[2] is not None:
|
|
2583
|
+
from_pct, from_source = float(existing[2]), "prior_credit"
|
|
2584
|
+
else:
|
|
2585
|
+
from_pct = _resolve_reset_aware_hwm(conn, week_start_date, ws_at, we_at)
|
|
2586
|
+
if from_pct is None:
|
|
2587
|
+
return None
|
|
2588
|
+
from_source = "hwm"
|
|
2589
|
+
is_completion = False
|
|
2590
|
+
if existing is not None and not is_force:
|
|
2591
|
+
owned = conn.execute(
|
|
2592
|
+
"SELECT 1 FROM weekly_usage_snapshots "
|
|
2593
|
+
" WHERE week_start_date=? AND source='record-credit' "
|
|
2594
|
+
" AND unixepoch(captured_at_utc) >= unixepoch(?) LIMIT 1",
|
|
2595
|
+
(week_start_date, existing[1]),
|
|
2596
|
+
).fetchone()
|
|
2597
|
+
is_completion = owned is None
|
|
2598
|
+
if existing is not None and not is_force and not is_completion:
|
|
2599
|
+
return None
|
|
2600
|
+
plan = _build_credit_plan(
|
|
2601
|
+
week_start_date=week_start_date,
|
|
2602
|
+
week_start_at=ws_at,
|
|
2603
|
+
week_end_at=we_at,
|
|
2604
|
+
from_pct=from_pct,
|
|
2605
|
+
from_source=from_source,
|
|
2606
|
+
to_pct=args.to,
|
|
2607
|
+
at_dt=at_dt,
|
|
2608
|
+
now=now,
|
|
2609
|
+
effective_override=existing[1] if is_completion else None,
|
|
2610
|
+
)
|
|
2611
|
+
except (sqlite3.DatabaseError, ValueError, TypeError):
|
|
2612
|
+
return None
|
|
2613
|
+
if plan != expected_plan:
|
|
2614
|
+
return None
|
|
2615
|
+
return plan, existing, is_completion
|
|
2616
|
+
|
|
2617
|
+
|
|
2551
2618
|
def cmd_record_credit(args) -> int:
|
|
2552
2619
|
now = _command_as_of()
|
|
2553
2620
|
try:
|
|
@@ -2704,23 +2771,68 @@ def cmd_record_credit(args) -> int:
|
|
|
2704
2771
|
print("aborted — nothing written")
|
|
2705
2772
|
return 0
|
|
2706
2773
|
|
|
2707
|
-
#
|
|
2708
|
-
#
|
|
2709
|
-
#
|
|
2710
|
-
#
|
|
2711
|
-
#
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2774
|
+
# Every non-mutating exit is above this point. Re-open under the
|
|
2775
|
+
# selected-state writer lock, recompute from current DB truth, and
|
|
2776
|
+
# refuse plan drift before any durable pipeline artifact is visible.
|
|
2777
|
+
# This is intentionally after confirmation: preview/no/TTY refusal
|
|
2778
|
+
# must not create an inflight tombstone merely by inspecting a credit.
|
|
2779
|
+
conn.close()
|
|
2780
|
+
conn = None
|
|
2781
|
+
c = _cctally()
|
|
2782
|
+
with c._selected_state_lock():
|
|
2783
|
+
conn = open_db()
|
|
2784
|
+
revalidated = c._revalidate_credit_plan(
|
|
2785
|
+
conn,
|
|
2786
|
+
args,
|
|
2787
|
+
now=now,
|
|
2788
|
+
at_dt=at_dt,
|
|
2789
|
+
expected_plan=plan,
|
|
2790
|
+
)
|
|
2791
|
+
if revalidated is None:
|
|
2792
|
+
eprint("record-credit: plan changed while awaiting confirmation; retry")
|
|
2793
|
+
return 2
|
|
2794
|
+
plan, existing, is_completion = revalidated
|
|
2795
|
+
stale_replays = _count_stale_replays(conn, plan)
|
|
2796
|
+
hwm_before = _resolve_reset_aware_hwm(
|
|
2797
|
+
conn, plan.week_start_date, plan.week_start_at, plan.week_end_at
|
|
2798
|
+
)
|
|
2799
|
+
if hwm_before is None:
|
|
2800
|
+
hwm_before = plan.from_pct
|
|
2801
|
+
try:
|
|
2802
|
+
handles = c._authoritative_begin(
|
|
2803
|
+
{"sevenDay"}, now_epoch=int(time.time())
|
|
2804
|
+
)
|
|
2805
|
+
except (OSError, ValueError) as exc:
|
|
2806
|
+
eprint(f"record-credit: could not prepare authoritative state: {exc}")
|
|
2807
|
+
return 3
|
|
2808
|
+
|
|
2809
|
+
# Existing-floor handling remains exactly scoped to this week, but
|
|
2810
|
+
# the weekly tombstone is already fail-closed immediately before
|
|
2811
|
+
# either mutation kernel runs.
|
|
2812
|
+
forced = False
|
|
2813
|
+
if existing is not None and is_force:
|
|
2814
|
+
_force_clear_credit(conn, plan.week_start_date)
|
|
2815
|
+
forced = True
|
|
2816
|
+
five_hour = _resolve_prior_5h(conn, at_dt)
|
|
2817
|
+
_apply_credit(conn, plan, five_hour=five_hour)
|
|
2818
|
+
|
|
2819
|
+
# A successful credit is authoritative only after the post-credit
|
|
2820
|
+
# DB state has a stable projection and all selected artifacts are
|
|
2821
|
+
# atomically committed. Leave inflight on any failure so a later
|
|
2822
|
+
# authority repair remains fail-closed.
|
|
2823
|
+
try:
|
|
2824
|
+
projection = c._read_db_projection_stable()
|
|
2825
|
+
completion_epoch = int(time.time())
|
|
2826
|
+
c._authoritative_commit(
|
|
2827
|
+
handles, completion_epoch=completion_epoch
|
|
2828
|
+
)
|
|
2829
|
+
c._reconcile_selected_control(
|
|
2830
|
+
projection, now_epoch=completion_epoch, observed_axes={"sevenDay"}
|
|
2831
|
+
)
|
|
2832
|
+
c._statusline_observe_touch()
|
|
2833
|
+
except Exception as exc:
|
|
2834
|
+
eprint(f"record-credit: authoritative state incomplete: {exc}")
|
|
2835
|
+
return 3
|
|
2724
2836
|
|
|
2725
2837
|
if is_json:
|
|
2726
2838
|
print(json.dumps(_credit_json(
|
|
@@ -3757,13 +3869,14 @@ def _hook_tick_throttle_touch() -> None:
|
|
|
3757
3869
|
|
|
3758
3870
|
|
|
3759
3871
|
# =========================================================================
|
|
3760
|
-
# Statusline
|
|
3872
|
+
# Statusline selected/transport markers + OAuth backoff deadline (#318)
|
|
3761
3873
|
# =========================================================================
|
|
3762
3874
|
#
|
|
3763
3875
|
# Two independent markers with two DIFFERENT time encodings:
|
|
3764
|
-
# - The observation marker is MTIME-based
|
|
3765
|
-
#
|
|
3766
|
-
#
|
|
3876
|
+
# - The selected-observation marker is MTIME-based and represents an actual
|
|
3877
|
+
# selected DB change or authoritative OAuth confirmation.
|
|
3878
|
+
# - The transport marker is also MTIME-based and represents an eligible
|
|
3879
|
+
# regular-pool candidate reaching the spool; it never throttles OAuth.
|
|
3767
3880
|
# - The OAuth backoff marker is CONTENT-based: it stores a FUTURE absolute
|
|
3768
3881
|
# epoch deadline as text. Its mtime is meaningless (~now). Encoding the
|
|
3769
3882
|
# deadline as mtime would future-date the file and, if ever confused
|
|
@@ -3771,11 +3884,9 @@ def _hook_tick_throttle_touch() -> None:
|
|
|
3771
3884
|
# deliberate split (Codex P1-3).
|
|
3772
3885
|
|
|
3773
3886
|
|
|
3774
|
-
def
|
|
3775
|
-
"""Seconds since the statusline last successfully fed usage; +inf if
|
|
3776
|
-
never. Mtime-based, mirroring ``_hook_tick_throttle_age_seconds``."""
|
|
3887
|
+
def _marker_age(path) -> float:
|
|
3777
3888
|
try:
|
|
3778
|
-
mtime =
|
|
3889
|
+
mtime = path.stat().st_mtime
|
|
3779
3890
|
except FileNotFoundError:
|
|
3780
3891
|
return float("inf")
|
|
3781
3892
|
except OSError:
|
|
@@ -3783,20 +3894,38 @@ def _statusline_observe_age_seconds() -> float:
|
|
|
3783
3894
|
return max(0.0, time.time() - mtime)
|
|
3784
3895
|
|
|
3785
3896
|
|
|
3786
|
-
def
|
|
3787
|
-
"""Mark the statusline as alive (mtime := now, creating if missing).
|
|
3788
|
-
|
|
3789
|
-
Called after every persist attempt that fed valid 7d input through
|
|
3790
|
-
``cmd_record_usage`` successfully — INCLUDING a dedup (no-insert)
|
|
3791
|
-
return, so the throttles reflect liveness rather than snapshot age."""
|
|
3897
|
+
def _touch_marker(path) -> None:
|
|
3792
3898
|
try:
|
|
3793
3899
|
_cctally_core.APP_DIR.mkdir(parents=True, exist_ok=True)
|
|
3794
|
-
|
|
3795
|
-
os.utime(
|
|
3900
|
+
path.touch(exist_ok=True)
|
|
3901
|
+
os.utime(path, None)
|
|
3796
3902
|
except OSError:
|
|
3797
3903
|
pass
|
|
3798
3904
|
|
|
3799
3905
|
|
|
3906
|
+
def _statusline_observe_age_seconds() -> float:
|
|
3907
|
+
"""Seconds since selected/authoritative usage changed; +inf if never."""
|
|
3908
|
+
return _marker_age(_cctally_core.STATUSLINE_OBSERVE_MARKER_PATH)
|
|
3909
|
+
|
|
3910
|
+
|
|
3911
|
+
def _statusline_observe_touch() -> None:
|
|
3912
|
+
"""Mark selected usage freshness after a proven selected transition.
|
|
3913
|
+
|
|
3914
|
+
The historical public helper names remain aliases for selected freshness.
|
|
3915
|
+
"""
|
|
3916
|
+
_touch_marker(_cctally_core.STATUSLINE_OBSERVE_MARKER_PATH)
|
|
3917
|
+
|
|
3918
|
+
|
|
3919
|
+
def _statusline_transport_age_seconds() -> float:
|
|
3920
|
+
"""Seconds since an eligible regular-pool candidate reached the spool."""
|
|
3921
|
+
return _marker_age(_cctally_core.STATUSLINE_TRANSPORT_MARKER_PATH)
|
|
3922
|
+
|
|
3923
|
+
|
|
3924
|
+
def _statusline_transport_touch() -> None:
|
|
3925
|
+
"""Mark regular-pool statusline transport after an atomic candidate write."""
|
|
3926
|
+
_touch_marker(_cctally_core.STATUSLINE_TRANSPORT_MARKER_PATH)
|
|
3927
|
+
|
|
3928
|
+
|
|
3800
3929
|
def _oauth_backoff_remaining_seconds() -> float:
|
|
3801
3930
|
"""Seconds until the shared OAuth 429 backoff deadline; ``0.0`` when the
|
|
3802
3931
|
marker is absent, empty, malformed, or already elapsed.
|
package/bin/_cctally_refresh.py
CHANGED
|
@@ -624,10 +624,6 @@ def _refresh_usage_inproc(timeout_seconds: float = 5.0) -> _RefreshUsageResult:
|
|
|
624
624
|
status="fetch_failed",
|
|
625
625
|
reason=f"invalid oauth_usage config: {exc}",
|
|
626
626
|
)
|
|
627
|
-
# Successful API response — clear any pending 429 backoff + counter, so a
|
|
628
|
-
# user-confirmed recovery lets automatic polling resume (spec §4).
|
|
629
|
-
c._oauth_backoff_reset()
|
|
630
|
-
|
|
631
627
|
seven = api.get("seven_day") or {}
|
|
632
628
|
try:
|
|
633
629
|
# Normalize at the OAuth ingress so the payload JSON published
|
|
@@ -681,12 +677,21 @@ def _refresh_usage_inproc(timeout_seconds: float = 5.0) -> _RefreshUsageResult:
|
|
|
681
677
|
# this, cmd_record_usage would fall back to its "statusline" default.
|
|
682
678
|
source="api",
|
|
683
679
|
)
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
680
|
+
authoritative = c._authoritative_record_usage(
|
|
681
|
+
record_args,
|
|
682
|
+
{
|
|
683
|
+
"sevenDay",
|
|
684
|
+
*({"fiveHour"} if five_pct is not None else set()),
|
|
685
|
+
},
|
|
686
|
+
)
|
|
687
|
+
if authoritative.status != "ok":
|
|
688
|
+
return _RefreshUsageResult(
|
|
689
|
+
status="record_failed", reason=authoritative.reason
|
|
690
|
+
)
|
|
691
|
+
# A fetched response becomes evidence of recovery only after its
|
|
692
|
+
# write-ahead tombstones, database reconciliation, selected control, and
|
|
693
|
+
# selected-freshness marker have all committed.
|
|
694
|
+
c._oauth_backoff_reset()
|
|
690
695
|
|
|
691
696
|
# §5.6 Option C: route through cctally's namespace so
|
|
692
697
|
# `monkeypatch.setitem(ns, "_bust_statusline_cache", …)` propagates
|
|
@@ -877,18 +882,38 @@ def _hook_tick_oauth_refresh(
|
|
|
877
882
|
throttle_seconds = float(_get_oauth_usage_config(c.load_config())["throttle_seconds"])
|
|
878
883
|
except OauthUsageConfigError:
|
|
879
884
|
throttle_seconds = float(c.HOOK_TICK_DEFAULT_THROTTLE_SECONDS)
|
|
880
|
-
#
|
|
881
|
-
#
|
|
882
|
-
#
|
|
883
|
-
#
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
885
|
+
# The automatic writer takes the selected-state lock *before* rechecking
|
|
886
|
+
# its suppression conditions. A concurrent publisher can otherwise make
|
|
887
|
+
# this tick issue an unnecessary OAuth request between the initial gate
|
|
888
|
+
# and the request itself.
|
|
889
|
+
try:
|
|
890
|
+
with c._selected_state_lock():
|
|
891
|
+
return _hook_tick_oauth_refresh_locked(
|
|
892
|
+
c,
|
|
893
|
+
token=token,
|
|
894
|
+
timeout_seconds=timeout_seconds,
|
|
895
|
+
throttle_seconds=throttle_seconds,
|
|
896
|
+
)
|
|
897
|
+
except OSError:
|
|
898
|
+
return "err(record-usage=exc)", None
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _hook_tick_oauth_refresh_locked(
|
|
902
|
+
c,
|
|
903
|
+
*,
|
|
904
|
+
token: str,
|
|
905
|
+
timeout_seconds: float,
|
|
906
|
+
throttle_seconds: float,
|
|
907
|
+
) -> tuple[str, dict | None]:
|
|
908
|
+
"""Automatic OAuth path with the selected-state lock already held."""
|
|
909
|
+
# Backfill gate: an inflight/invalid tombstone bypasses only selected-age
|
|
910
|
+
# suppression so a later authoritative result can repair it. The normal
|
|
911
|
+
# throttle and 429 deadline continue to bound OAuth traffic.
|
|
912
|
+
now_epoch = int(time.time())
|
|
913
|
+
needs_repair = c._authoritative_repair_required(now_epoch=now_epoch)
|
|
890
914
|
obs_age = c._statusline_observe_age_seconds()
|
|
891
|
-
if
|
|
915
|
+
if (not needs_repair
|
|
916
|
+
and obs_age < float(_cctally_core.OAUTH_BACKFILL_STALE_SECONDS)):
|
|
892
917
|
return f"skipped(statusline-fresh:{int(obs_age)}s)", None
|
|
893
918
|
backoff_remaining = c._oauth_backoff_remaining_seconds()
|
|
894
919
|
if backoff_remaining > 0:
|
|
@@ -899,9 +924,6 @@ def _hook_tick_oauth_refresh(
|
|
|
899
924
|
try:
|
|
900
925
|
api = c._fetch_oauth_usage(token=token, timeout_seconds=timeout_seconds)
|
|
901
926
|
except RefreshUsageRateLimitError as exc:
|
|
902
|
-
# Arm the shared backoff deadline so the next tick is suppressed
|
|
903
|
-
# rather than immediately re-polling (spec §4). Honors Retry-After
|
|
904
|
-
# when present, else conservative exponential backoff.
|
|
905
927
|
c._oauth_backoff_register_429(
|
|
906
928
|
retry_after_deadline=getattr(exc, "retry_after_deadline", None),
|
|
907
929
|
now=time.time(),
|
|
@@ -911,8 +933,6 @@ def _hook_tick_oauth_refresh(
|
|
|
911
933
|
return "err(network)", None
|
|
912
934
|
except RefreshUsageMalformedError:
|
|
913
935
|
return "err(parse)", None
|
|
914
|
-
# Successful API response — clear any pending 429 backoff + counter.
|
|
915
|
-
c._oauth_backoff_reset()
|
|
916
936
|
seven = api["seven_day"]
|
|
917
937
|
try:
|
|
918
938
|
seven_pct = c._normalize_percent(float(seven["utilization"]))
|
|
@@ -922,10 +942,6 @@ def _hook_tick_oauth_refresh(
|
|
|
922
942
|
five = api.get("five_hour") if isinstance(api.get("five_hour"), dict) else None
|
|
923
943
|
five_pct: float | None = None
|
|
924
944
|
five_resets_epoch: int | None = None
|
|
925
|
-
# An inactive 5h window arrives as `resets_at: null` (key present, value
|
|
926
|
-
# null). Require a string here — mirrors the seven_day guard in
|
|
927
|
-
# _fetch_oauth_usage — so _iso_to_epoch(None) can't raise AttributeError;
|
|
928
|
-
# a malformed (non-null) string still degrades via the except below.
|
|
929
945
|
if (five is not None and "utilization" in five
|
|
930
946
|
and isinstance(five.get("resets_at"), str)):
|
|
931
947
|
try:
|
|
@@ -939,15 +955,21 @@ def _hook_tick_oauth_refresh(
|
|
|
939
955
|
resets_at=str(seven_resets_epoch),
|
|
940
956
|
five_hour_percent=five_pct,
|
|
941
957
|
five_hour_resets_at=str(five_resets_epoch) if five_resets_epoch is not None else None,
|
|
942
|
-
# OAuth feeder — label the row "api" (spec §5).
|
|
943
958
|
source="api",
|
|
944
959
|
)
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
960
|
+
authoritative = c._authoritative_record_usage(
|
|
961
|
+
record_args,
|
|
962
|
+
{"sevenDay", *({"fiveHour"} if five_pct is not None else set())},
|
|
963
|
+
lock_held=True,
|
|
964
|
+
)
|
|
965
|
+
if authoritative.status != "ok":
|
|
966
|
+
reason = authoritative.reason or ""
|
|
967
|
+
if reason.startswith("exit "):
|
|
968
|
+
return f"err(record-usage={reason[5:]})", None
|
|
948
969
|
return "err(record-usage=exc)", None
|
|
949
|
-
|
|
950
|
-
|
|
970
|
+
# Success is acknowledged to the shared OAuth backoff only after the
|
|
971
|
+
# authoritative writer has published tombstones, control, and freshness.
|
|
972
|
+
c._oauth_backoff_reset()
|
|
951
973
|
parts = [f"7d={int(round(seven_pct))}"]
|
|
952
974
|
if five_pct is not None:
|
|
953
975
|
parts.append(f"5h={int(round(five_pct))}")
|