cctally 1.72.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.
@@ -42,6 +42,54 @@ def _cctally():
42
42
  return sys.modules["cctally"]
43
43
 
44
44
 
45
+ def _gather_statusline_pipeline(c, *, now_utc: dt.datetime) -> dict:
46
+ """Read the #318 statusline pipeline without creating or pruning files."""
47
+ now_epoch = int(now_utc.timestamp())
48
+ result = {
49
+ "transport_age_seconds": None,
50
+ "selected_age_seconds": None,
51
+ "active_candidate_count": 0,
52
+ "control_db_agrees": None,
53
+ "tombstones": {"fiveHour": "absent", "sevenDay": "absent"},
54
+ }
55
+ try:
56
+ result["transport_age_seconds"] = c._statusline_transport_age_seconds()
57
+ except Exception:
58
+ pass
59
+ try:
60
+ result["selected_age_seconds"] = c._statusline_observe_age_seconds()
61
+ except Exception:
62
+ pass
63
+ try:
64
+ result["active_candidate_count"] = len(
65
+ c._scan_active_candidate_spool(now_epoch=now_epoch)
66
+ )
67
+ except Exception:
68
+ pass
69
+ try:
70
+ result["control_db_agrees"] = c._statusline_control_db_agreement(
71
+ now_epoch=now_epoch
72
+ )
73
+ except Exception:
74
+ pass
75
+ for axis, path in (
76
+ ("fiveHour", _cctally_core.STATUSLINE_AUTHORITATIVE_5H_PATH),
77
+ ("sevenDay", _cctally_core.STATUSLINE_AUTHORITATIVE_7D_PATH),
78
+ ):
79
+ try:
80
+ if not path.exists():
81
+ continue
82
+ tombstone = c._read_tombstone(
83
+ axis, now_epoch=now_epoch, fail_closed=False
84
+ )
85
+ result["tombstones"][axis] = (
86
+ tombstone.state if tombstone is not None else "invalid"
87
+ )
88
+ except Exception:
89
+ result["tombstones"][axis] = "invalid"
90
+ return result
91
+
92
+
45
93
  def _codex_lifecycle_activity_24h(
46
94
  *, root_keys: set[str], now_utc: dt.datetime,
47
95
  ) -> dict[str, dict]:
@@ -357,6 +405,16 @@ def doctor_gather_state(
357
405
  except Exception:
358
406
  pass
359
407
 
408
+ # ── Statusline candidate arbitration (#318) ──────────────────────
409
+ # This inspection is deliberately independent of SQLite mutation: marker
410
+ # mtime, candidate/control files, and tombstones are all read fail-soft.
411
+ # In particular it uses the scan-only candidate helper, never the reducer
412
+ # loader that prunes expired or malformed spool files.
413
+ try:
414
+ statusline_pipeline = _gather_statusline_pipeline(c, now_utc=now_utc)
415
+ except Exception:
416
+ statusline_pipeline = None
417
+
360
418
  # Conversation-sessions rollup consistency (#217 S1 / U9). Two cheap COUNTs
361
419
  # (graceful None on a missing table / unreadable DB) + an in-progress signal
362
420
  # so a transient mid-sync mismatch never WARNs. The in-progress signal is a
@@ -787,6 +845,7 @@ def doctor_gather_state(
787
845
  codex_lifecycle_activity_24h=codex_lifecycle_activity_24h,
788
846
  # #311: precomputed statusLine.refreshInterval classification.
789
847
  statusline_refresh_state=statusline_refresh_state,
848
+ statusline_pipeline=statusline_pipeline,
790
849
  )
791
850
 
792
851
 
@@ -2650,15 +2650,21 @@ def _build_transcript_parser(subparsers, name, *, help_text, xref=None):
2650
2650
  t_export = t_sub.add_parser(
2651
2651
  "export", help="Export a whole session as Markdown (anonymized by default)",
2652
2652
  formatter_class=CLIHelpFormatter)
2653
- t_export.add_argument("session_id", metavar="SESSION_ID",
2654
- help="Claude sessionId to export")
2653
+ t_export.add_argument("session_id", metavar="ID",
2654
+ help="A Claude sessionId or a v1. conversation key "
2655
+ "(Codex conversations use the v1. key)")
2655
2656
  t_export.add_argument(
2656
2657
  "--scope", choices=("all", "prompts", "chat", "recipe"), default="all",
2657
- help="Which slice to export (default: all)")
2658
+ help="Which slice to export (default: all; Codex accepts only 'all')")
2658
2659
  t_export.add_argument(
2659
2660
  "--raw", action="store_true",
2660
2661
  help="Disable the whole scrub (identity + secrets); byte-identical to "
2661
2662
  "the dashboard raw export")
2663
+ t_export.add_argument(
2664
+ "--speed", choices=("auto", "standard", "fast"), default=None,
2665
+ help="Codex service tier for per-turn cost (default: auto). Applies only "
2666
+ "to Codex (v1.) conversations; an explicit value on any other ref "
2667
+ "is a usage error")
2662
2668
  t_export.add_argument(
2663
2669
  "-o", "--output", metavar="PATH", default=None,
2664
2670
  help="Write to PATH instead of stdout (same exact bytes)")
@@ -2668,6 +2674,9 @@ def _build_transcript_parser(subparsers, name, *, help_text, xref=None):
2668
2674
  "search", help="Search transcripts across sessions (raw output)",
2669
2675
  formatter_class=CLIHelpFormatter)
2670
2676
  t_search.add_argument("query", metavar="QUERY", help="Search text")
2677
+ t_search.add_argument(
2678
+ "--source", choices=("claude", "codex"), default="claude",
2679
+ help="Which provider's conversations to search (default: claude)")
2671
2680
  t_search.add_argument(
2672
2681
  "--kind",
2673
2682
  choices=("all", "prompts", "assistant", "tools", "thinking",
@@ -2676,7 +2685,11 @@ def _build_transcript_parser(subparsers, name, *, help_text, xref=None):
2676
2685
  t_search.add_argument("--limit", type=int, default=50,
2677
2686
  help="Max results (default: 50)")
2678
2687
  t_search.add_argument("--offset", type=int, default=0,
2679
- help="Result offset for pagination (default: 0)")
2688
+ help="Result offset for pagination (default: 0; "
2689
+ "Claude only — Codex paginates with --cursor)")
2690
+ t_search.add_argument("--cursor", default=None, metavar="TOKEN",
2691
+ help="Codex pagination cursor from a prior nextCursor "
2692
+ "(requires --source codex)")
2680
2693
  t_search.add_argument("--project", action="append", default=None,
2681
2694
  metavar="LABEL",
2682
2695
  help="Filter by project label (repeatable)")
@@ -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
- # 4a. Existing-floor handling (M2; state classified at step 2a). The
2708
- # fully-applied refuse was hoisted above the confirm prompt (#212
2709
- # N2), so only two cases remain here: `--force` clears the prior
2710
- # floor + re-records at a fresh effective; a half-applied credit
2711
- # (floor row, no command-owned snapshot a crash between
2712
- # _apply_credit's floor commit and the synthetic snapshot) falls
2713
- # through to a completion (the plan reuses the existing effective
2714
- # and the apply steps are idempotent). `existing`/`is_completion`
2715
- # were resolved up front keyed on week_start_date /
2716
- # weekly_credit_floors.
2717
- forced = False
2718
- if existing is not None and is_force:
2719
- _force_clear_credit(conn, plan.week_start_date)
2720
- forced = True
2721
-
2722
- five_hour = _resolve_prior_5h(conn, at_dt)
2723
- _apply_credit(conn, plan, five_hour=five_hour)
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 observation marker + OAuth backoff deadline (spec 2026-07-17)
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 (mirrors _hook_tick_throttle_*):
3765
- # it answers "how long since the statusline last fed?", so its age is
3766
- # the signal. It is touched even on a dedup no-op.
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 _statusline_observe_age_seconds() -> float:
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 = _cctally_core.STATUSLINE_OBSERVE_MARKER_PATH.stat().st_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 _statusline_observe_touch() -> None:
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
- _cctally_core.STATUSLINE_OBSERVE_MARKER_PATH.touch(exist_ok=True)
3795
- os.utime(_cctally_core.STATUSLINE_OBSERVE_MARKER_PATH, None)
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.
@@ -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
- try:
685
- rc = c.cmd_record_usage(record_args)
686
- except Exception as exc:
687
- return _RefreshUsageResult(status="record_failed", reason=str(exc))
688
- if rc != 0:
689
- return _RefreshUsageResult(status="record_failed", reason=f"exit {rc}")
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
- # Backfill gate (spec §4). The automatic OAuth poll is now a BACKFILL
881
- # behind the statusline (the primary writer). It fetches only when the
882
- # statusline has NOT fed recently AND no 429 cooldown is pending — so a
883
- # live statusline keeps usage fresh with zero API calls, and a 429 streak
884
- # is suppressed by the shared deadline instead of re-polled every tick.
885
- # Gating on the observation marker (not snapshot age) is what makes the
886
- # demotion correct: cmd_record_usage dedups unchanged percentages without
887
- # refreshing captured_at, so snapshot age keeps growing while the
888
- # statusline is alive. Force-refresh (_refresh_usage_inproc) never routes
889
- # through here, so it is exempt (user-initiated).
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 obs_age < float(_cctally_core.OAUTH_BACKFILL_STALE_SECONDS):
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
- try:
946
- rc = c.cmd_record_usage(record_args)
947
- except Exception:
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
- if rc != 0:
950
- return f"err(record-usage={rc})", None
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))}")