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.
@@ -27,16 +27,24 @@ Spec: docs/superpowers/specs/2026-05-30-extract-five-hour-statusline-cmd-design.
27
27
  from __future__ import annotations
28
28
 
29
29
  import argparse
30
+ import dataclasses
30
31
  import datetime as dt
31
32
  import fcntl
33
+ import hashlib
32
34
  import json
35
+ import math
33
36
  import os
34
37
  import pathlib
38
+ import re
39
+ import secrets
40
+ import sqlite3
35
41
  import sys
42
+ import time
36
43
  from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
37
44
 
38
45
  import _cctally_core
39
46
  import _lib_statusline
47
+ import _lib_statusline_candidates as _candidates
40
48
  from _cctally_core import _command_as_of, eprint, open_db
41
49
 
42
50
 
@@ -334,6 +342,56 @@ def _release_persist_lock(fd: "int | None") -> None:
334
342
  pass
335
343
 
336
344
 
345
+ class _AuthoritativeRecordResult:
346
+ """Outcome of the selected-state authoritative writer protocol.
347
+
348
+ This deliberately carries a small status surface rather than leaking a
349
+ SQLite result. A non-``ok`` result means the durable tombstone remains
350
+ inflight, so callers must not touch selected freshness or clear OAuth
351
+ backoff state.
352
+ """
353
+
354
+ def __init__(self, status: str, reason: str | None = None):
355
+ self.status = status
356
+ self.reason = reason
357
+
358
+
359
+ class _SelectedStateLock:
360
+ """Blocking owner of the one selected-state writer critical section."""
361
+
362
+ def __init__(self):
363
+ self._fd = -1
364
+
365
+ def __enter__(self):
366
+ _cctally_core.APP_DIR.mkdir(parents=True, exist_ok=True)
367
+ self._fd = os.open(
368
+ _cctally_core.STATUSLINE_PERSIST_LOCK_PATH,
369
+ os.O_WRONLY | os.O_CREAT,
370
+ 0o644,
371
+ )
372
+ fcntl.flock(self._fd, fcntl.LOCK_EX)
373
+ return self
374
+
375
+ def __exit__(self, exc_type, exc, traceback):
376
+ if self._fd < 0:
377
+ return False
378
+ try:
379
+ fcntl.flock(self._fd, fcntl.LOCK_UN)
380
+ except OSError:
381
+ pass
382
+ try:
383
+ os.close(self._fd)
384
+ except OSError:
385
+ pass
386
+ self._fd = -1
387
+ return False
388
+
389
+
390
+ def _selected_state_lock() -> _SelectedStateLock:
391
+ """Return the blocking lock shared by every selected-state writer."""
392
+ return _SelectedStateLock()
393
+
394
+
337
395
  def _record_args(*, percent, resets_at, five_hour_percent, five_hour_resets_at,
338
396
  source):
339
397
  """Build the cmd_record_usage Namespace the feeder passes to the kernel.
@@ -353,40 +411,752 @@ def _record_args(*, percent, resets_at, five_hour_percent, five_hour_resets_at,
353
411
  )
354
412
 
355
413
 
356
- def _fork_persist(args, parent_lock_fd: int) -> None:
357
- """Run cmd_record_usage in a DETACHED child so the render stays fast.
358
-
359
- Lifecycle (spec §2, Codex P2-3): single ``fork()``; **on fork() failure
360
- SKIP persistence entirely** (unlike hook-tick we must NOT run inline —
361
- cmd_record_usage may sync weekly cost / scan 5h totals / evaluate budget
362
- axes, any of which would stall the render). In the child: ``setsid()``,
363
- immediately redirect fd 0/1/2 to /dev/null (an inherited stdout pipe
364
- would delay the statusline's EOF), then record and ``os._exit(0)``.
365
-
366
- Lock contract: the PARENT releases only its OWN fd (in the caller's
367
- ``finally``); the child CLOSES its inherited copy of the parent fd and
368
- RE-ACQUIRES the persist lock on a fresh, INDEPENDENT fd (blocking) — so
369
- the parent's release cannot race a second render into a duplicate insert.
370
- The child then re-checks the observation marker UNDER that held lock and
371
- records only if still stale, which is the authoritative herd guard: a
372
- concurrent render's non-blocking acquire fails until this child has
373
- recorded AND touched the marker."""
414
+ _CANDIDATE_FINAL_RE = re.compile(r"[0-9a-f]{64}\.json\Z")
415
+
416
+
417
+ class ProjectionUnstable(RuntimeError):
418
+ """stats.db changed while its selected projection was being read."""
419
+
420
+
421
+ def _candidate_identity_token(parsed) -> str:
422
+ if isinstance(parsed.session_id, str) and parsed.session_id:
423
+ kind, value = "session_id", parsed.session_id
424
+ elif isinstance(parsed.transcript_path, str) and parsed.transcript_path:
425
+ kind, value = "transcript_path", parsed.transcript_path
426
+ else:
427
+ kind, value = "anonymous", ""
428
+ return hashlib.sha256(f"{kind}\0{value}".encode("utf-8")).hexdigest()
429
+
430
+
431
+ def _statusline_reset_is_plausible(axis: str, epoch: int, now_epoch: int) -> bool:
432
+ if not isinstance(epoch, int) or isinstance(epoch, bool):
433
+ return False
434
+ if axis == "fiveHour":
435
+ return now_epoch - 600 <= epoch <= now_epoch + 6 * 3600
436
+ return now_epoch - 30 * 86400 <= epoch <= now_epoch + 8 * 86400
437
+
438
+
439
+ def _candidate_from_input(parsed, *, received_at: int) -> "_candidates.Candidate | None":
440
+ def axis(percent, resets_at, name):
441
+ if isinstance(percent, bool) or not isinstance(percent, (int, float)):
442
+ return None
443
+ if not math.isfinite(float(percent)) or not 0 <= float(percent) <= 100:
444
+ return None
445
+ if not _statusline_reset_is_plausible(name, resets_at, received_at):
446
+ return None
447
+ return _candidates.AxisValue(float(percent), int(resets_at))
448
+
449
+ five = axis(parsed.rate_limits_5h_pct, parsed.rate_limits_5h_resets_at, "fiveHour")
450
+ seven = axis(parsed.rate_limits_7d_pct, parsed.rate_limits_7d_resets_at, "sevenDay")
451
+ if five is None and seven is None:
452
+ return None
453
+ return _candidates.Candidate(
454
+ token=_candidate_identity_token(parsed),
455
+ received_at=received_at,
456
+ five_hour=five,
457
+ seven_day=seven,
458
+ )
459
+
460
+
461
+ def _candidate_path(token: str) -> pathlib.Path:
462
+ return _cctally_core.STATUSLINE_CANDIDATE_DIR / f"{token}.json"
463
+
464
+
465
+ def _atomic_write_json(path: pathlib.Path, document: dict) -> None:
466
+ """Publish compact JSON with a unique same-directory exclusive temp file."""
467
+ path.parent.mkdir(parents=True, exist_ok=True)
468
+ if path.parent == _cctally_core.STATUSLINE_CANDIDATE_DIR:
469
+ os.chmod(path.parent, 0o700)
470
+ token = secrets.token_hex(16)
471
+ temp = path.parent / f".{path.name}.tmp.{os.getpid()}.{token}"
472
+ fd = -1
473
+ try:
474
+ fd = os.open(temp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
475
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
476
+ fd = -1
477
+ json.dump(document, handle, allow_nan=False, separators=(",", ":"))
478
+ os.replace(temp, path)
479
+ try:
480
+ os.chmod(path, 0o600)
481
+ except OSError:
482
+ pass
483
+ finally:
484
+ if fd >= 0:
485
+ try:
486
+ os.close(fd)
487
+ except OSError:
488
+ pass
489
+ try:
490
+ temp.unlink()
491
+ except FileNotFoundError:
492
+ pass
493
+ except OSError:
494
+ pass
495
+
496
+
497
+ def _candidate_document(candidate: "_candidates.Candidate") -> dict:
498
+ document = {"schemaVersion": 1, "receivedAt": candidate.received_at}
499
+ if candidate.five_hour is not None:
500
+ document["fiveHour"] = {
501
+ "percent": candidate.five_hour.percent,
502
+ "resetsAt": candidate.five_hour.raw_resets_at,
503
+ }
504
+ if candidate.seven_day is not None:
505
+ document["sevenDay"] = {
506
+ "percent": candidate.seven_day.percent,
507
+ "resetsAt": candidate.seven_day.raw_resets_at,
508
+ }
509
+ return document
510
+
511
+
512
+ def _write_candidate(candidate: "_candidates.Candidate") -> None:
513
+ _cctally_core.STATUSLINE_CANDIDATE_DIR.mkdir(parents=True, exist_ok=True, mode=0o700)
514
+ os.chmod(_cctally_core.STATUSLINE_CANDIDATE_DIR, 0o700)
515
+ _atomic_write_json(_candidate_path(candidate.token), _candidate_document(candidate))
516
+
517
+
518
+ def _load_candidate_spool(*, now_epoch: int) -> tuple["_candidates.Candidate", ...]:
519
+ directory = _cctally_core.STATUSLINE_CANDIDATE_DIR
520
+ try:
521
+ entries = tuple(directory.iterdir())
522
+ except OSError:
523
+ return ()
524
+ result = []
525
+ for path in entries:
526
+ # Match the complete final grammar before touching an entry. In-flight
527
+ # peer temps are deliberately neither read nor pruned.
528
+ if not _CANDIDATE_FINAL_RE.fullmatch(path.name):
529
+ continue
530
+ try:
531
+ raw = path.read_bytes()
532
+ candidate = _candidates.load_candidate_document(
533
+ raw,
534
+ now_epoch=now_epoch,
535
+ reset_is_plausible=lambda axis, epoch: _statusline_reset_is_plausible(axis, epoch, now_epoch),
536
+ token=path.stem,
537
+ )
538
+ except (OSError, _candidates.StateValidationError):
539
+ try:
540
+ path.unlink()
541
+ except OSError:
542
+ pass
543
+ continue
544
+ if not (-5 <= now_epoch - candidate.received_at < _cctally_core.STATUSLINE_CANDIDATE_TTL_SECONDS):
545
+ try:
546
+ path.unlink()
547
+ except OSError:
548
+ pass
549
+ continue
550
+ result.append(candidate)
551
+ return tuple(result)
552
+
553
+
554
+ def _scan_active_candidate_spool(*, now_epoch: int) -> tuple["_candidates.Candidate", ...]:
555
+ """Read valid active candidates without pruning or otherwise mutating.
556
+
557
+ `doctor` needs a truthful active-count signal but retains its global
558
+ read-only contract. In contrast, the reducer loader above owns best-effort
559
+ cleanup of malformed/expired final entries.
560
+ """
561
+ try:
562
+ entries = tuple(_cctally_core.STATUSLINE_CANDIDATE_DIR.iterdir())
563
+ except OSError:
564
+ return ()
565
+ result = []
566
+ for path in entries:
567
+ if not _CANDIDATE_FINAL_RE.fullmatch(path.name):
568
+ continue
569
+ try:
570
+ candidate = _candidates.load_candidate_document(
571
+ path.read_bytes(),
572
+ now_epoch=now_epoch,
573
+ reset_is_plausible=lambda axis, epoch: _statusline_reset_is_plausible(
574
+ axis, epoch, now_epoch
575
+ ),
576
+ token=path.stem,
577
+ )
578
+ except (OSError, _candidates.StateValidationError):
579
+ continue
580
+ if -5 <= now_epoch - candidate.received_at < _cctally_core.STATUSLINE_CANDIDATE_TTL_SECONDS:
581
+ result.append(candidate)
582
+ return tuple(result)
583
+
584
+
585
+ def _fingerprint_file(path: pathlib.Path) -> "_candidates.FileFingerprint | None":
586
+ try:
587
+ st = path.stat()
588
+ except FileNotFoundError:
589
+ return None
590
+ except OSError:
591
+ return None
592
+ return _candidates.FileFingerprint(st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns)
593
+
594
+
595
+ def _db_file_fingerprint() -> dict[str, "_candidates.FileFingerprint | None"]:
596
+ db = _cctally_core.DB_PATH
597
+ return {
598
+ "main": _fingerprint_file(db),
599
+ "wal": _fingerprint_file(db.with_name(db.name + "-wal")),
600
+ }
601
+
602
+
603
+ def _epoch_from_iso(value: object) -> int:
604
+ return int(_cctally_core.parse_iso_datetime(str(value), "statusline projection").timestamp())
605
+
606
+
607
+ def _read_db_projection_once() -> "_candidates.DbProjection":
608
+ conn = open_db()
609
+ try:
610
+ weekly_rows = conn.execute(
611
+ "SELECT id, weekly_percent, week_start_date, week_start_at, week_end_at, "
612
+ " captured_at_utc, source "
613
+ "FROM weekly_usage_snapshots "
614
+ "WHERE weekly_percent IS NOT NULL AND week_end_at IS NOT NULL "
615
+ "ORDER BY unixepoch(captured_at_utc) DESC, id DESC"
616
+ ).fetchall()
617
+ five_rows = conn.execute(
618
+ "SELECT id, five_hour_percent, five_hour_resets_at, five_hour_window_key, "
619
+ " captured_at_utc, source "
620
+ "FROM weekly_usage_snapshots "
621
+ "WHERE five_hour_percent IS NOT NULL AND five_hour_resets_at IS NOT NULL "
622
+ "ORDER BY unixepoch(captured_at_utc) DESC, id DESC"
623
+ ).fetchall()
624
+
625
+ weekly_groups: dict[int, list] = {}
626
+ for row in weekly_rows:
627
+ try:
628
+ raw = _epoch_from_iso(row["week_end_at"])
629
+ except (TypeError, ValueError):
630
+ continue
631
+ canonical = int(_cctally_core._normalize_week_boundary_dt(
632
+ dt.datetime.fromtimestamp(raw, tz=dt.timezone.utc)
633
+ ).timestamp())
634
+ weekly_groups.setdefault(canonical, []).append(row)
635
+
636
+ weekly_projection = None
637
+ if weekly_groups:
638
+ canonical = max(weekly_groups)
639
+ rows = weekly_groups[canonical]
640
+ reference = max(
641
+ rows, key=lambda row: (_epoch_from_iso(row["captured_at_utc"]), int(row["id"]))
642
+ )
643
+ week_start_at = reference["week_start_at"]
644
+ if not week_start_at:
645
+ week_start_at = dt.datetime.fromtimestamp(
646
+ canonical - 7 * 86400, tz=dt.timezone.utc
647
+ ).isoformat().replace("+00:00", "Z")
648
+ week_end_at = reference["week_end_at"]
649
+ floor = _cctally_core._reset_aware_floor(
650
+ conn,
651
+ str(reference["week_start_date"]),
652
+ str(week_start_at),
653
+ str(week_end_at),
654
+ )
655
+ floor_epoch = _epoch_from_iso(floor) if floor else 0
656
+ eligible = [
657
+ row for row in rows
658
+ if floor_epoch == 0 or _epoch_from_iso(row["captured_at_utc"]) >= floor_epoch
659
+ ]
660
+ if eligible:
661
+ weekly = max(
662
+ eligible,
663
+ key=lambda row: (_epoch_from_iso(row["captured_at_utc"]), int(row["id"])),
664
+ )
665
+ raw = _epoch_from_iso(weekly["week_end_at"])
666
+ weekly_projection = _candidates.AxisProjection(
667
+ float(weekly["weekly_percent"]), raw, canonical,
668
+ _epoch_from_iso(weekly["captured_at_utc"]),
669
+ str(weekly["source"] or "statusline"), floor_epoch,
670
+ )
671
+
672
+ now_epoch = int(time.time())
673
+ five_groups: dict[int, list] = {}
674
+ for row in five_rows:
675
+ try:
676
+ raw = _epoch_from_iso(row["five_hour_resets_at"])
677
+ except (TypeError, ValueError):
678
+ continue
679
+ if not _statusline_reset_is_plausible("fiveHour", raw, now_epoch):
680
+ continue
681
+ stored_key = row["five_hour_window_key"]
682
+ canonical = (
683
+ int(stored_key)
684
+ if stored_key is not None
685
+ else _cctally()._canonical_5h_window_key(raw)
686
+ )
687
+ five_groups.setdefault(canonical, []).append(row)
688
+
689
+ five_projection = None
690
+ if five_groups:
691
+ canonical = max(five_groups)
692
+ reset_event_id = _cctally()._resolve_active_five_hour_reset_event_id(conn, canonical)
693
+ floor_epoch = 0
694
+ if reset_event_id:
695
+ event = conn.execute(
696
+ "SELECT effective_reset_at_utc FROM five_hour_reset_events WHERE id = ?",
697
+ (reset_event_id,),
698
+ ).fetchone()
699
+ if event is not None:
700
+ floor_epoch = _epoch_from_iso(event["effective_reset_at_utc"])
701
+ eligible = [
702
+ row for row in five_groups[canonical]
703
+ if floor_epoch == 0 or _epoch_from_iso(row["captured_at_utc"]) >= floor_epoch
704
+ ]
705
+ if eligible:
706
+ five = max(
707
+ eligible,
708
+ key=lambda row: (_epoch_from_iso(row["captured_at_utc"]), int(row["id"])),
709
+ )
710
+ raw = _epoch_from_iso(five["five_hour_resets_at"])
711
+ five_projection = _candidates.AxisProjection(
712
+ float(five["five_hour_percent"]), raw, canonical,
713
+ _epoch_from_iso(five["captured_at_utc"]),
714
+ str(five["source"] or "statusline"), int(reset_event_id),
715
+ )
716
+ finally:
717
+ conn.close()
718
+ return _candidates.DbProjection(five_projection, weekly_projection)
719
+
720
+
721
+ def _read_db_projection_stable(*, attempts: int = 3) -> "_candidates.DbProjection":
722
+ for _ in range(attempts):
723
+ before = _db_file_fingerprint()
724
+ projection = _read_db_projection_once()
725
+ after = _db_file_fingerprint()
726
+ if before == after and after["main"] is not None:
727
+ return dataclasses.replace(projection, db_files=after)
728
+ raise ProjectionUnstable("stats.db changed during projection")
729
+
730
+
731
+ def _fingerprint_document(value: "_candidates.FileFingerprint | None") -> dict | None:
732
+ if value is None:
733
+ return None
734
+ return {"device": value.device, "inode": value.inode, "size": value.size, "mtimeNs": value.mtime_ns}
735
+
736
+
737
+ def _axis_projection_document(value: "_candidates.AxisProjection | None") -> dict | None:
738
+ if value is None:
739
+ return None
740
+ return {
741
+ "percent": value.percent,
742
+ "rawResetsAt": value.raw_resets_at,
743
+ "canonicalKey": value.canonical_key,
744
+ "capturedAt": value.captured_at,
745
+ "source": value.source,
746
+ "resetGeneration": value.reset_generation,
747
+ }
748
+
749
+
750
+ def _pending_document(value: "_candidates.PendingDrop | None") -> dict | None:
751
+ if value is None:
752
+ return None
753
+ signature = value.retry_signature
754
+ return {
755
+ "canonicalKey": value.canonical_key,
756
+ "reducedPercent": value.reduced_percent,
757
+ "firstSeenAt": value.first_seen_at,
758
+ "kernelStage": value.kernel_stage,
759
+ "attempts": value.attempts,
760
+ "contributors": {
761
+ token: {"baselineReceivedAt": item.baseline_received_at, "satisfied": item.satisfied}
762
+ for token, item in value.contributors.items()
763
+ },
764
+ "retrySignature": (
765
+ None if signature is None else {
766
+ "candidateKey": signature.candidate_key,
767
+ "candidatePercent": signature.candidate_percent,
768
+ "dbKey": signature.db_key,
769
+ "dbPercent": signature.db_percent,
770
+ "dbResetGeneration": signature.db_reset_generation,
771
+ }
772
+ ),
773
+ }
774
+
775
+
776
+ def _control_document(control: "_candidates.ControlState") -> dict:
777
+ files = control.db_projection.db_files or {"main": None, "wal": None}
778
+ return {
779
+ "schemaVersion": 1,
780
+ "dbProjection": {
781
+ "fiveHour": _axis_projection_document(control.db_projection.five_hour),
782
+ "sevenDay": _axis_projection_document(control.db_projection.seven_day),
783
+ },
784
+ "dbFiles": {
785
+ "main": _fingerprint_document(files.get("main")),
786
+ "wal": _fingerprint_document(files.get("wal")),
787
+ },
788
+ "pendingDrops": {
789
+ "fiveHour": _pending_document(control.pending_drops.get("fiveHour")),
790
+ "sevenDay": _pending_document(control.pending_drops.get("sevenDay")),
791
+ },
792
+ }
793
+
794
+
795
+ def _read_control_state(*, now_epoch: int) -> "_candidates.ControlState | None":
796
+ try:
797
+ return _candidates.load_control_document(
798
+ _cctally_core.STATUSLINE_SELECTED_PATH.read_bytes(), now_epoch=now_epoch
799
+ )
800
+ except (OSError, _candidates.StateValidationError):
801
+ return None
802
+
803
+
804
+ def _statusline_control_db_agreement(*, now_epoch: int) -> bool | None:
805
+ """Return control/DB-fingerprint agreement without opening SQLite.
806
+
807
+ ``None`` means the derived control document is absent; ``False`` means an
808
+ existing document was invalid or its stable fingerprint is stale. This is
809
+ intentionally stat-only so doctor remains a read-only diagnostic.
810
+ """
811
+ control = _read_control_state(now_epoch=now_epoch)
812
+ if control is None:
813
+ try:
814
+ return False if _cctally_core.STATUSLINE_SELECTED_PATH.exists() else None
815
+ except OSError:
816
+ return None
817
+ return control.db_projection.db_files == _db_file_fingerprint()
818
+
819
+
820
+ def _write_control_state(control: "_candidates.ControlState") -> None:
821
+ _atomic_write_json(_cctally_core.STATUSLINE_SELECTED_PATH, _control_document(control))
822
+
823
+
824
+ def _reconcile_selected_control(
825
+ projection: "_candidates.DbProjection", *, now_epoch: int, observed_axes=()
826
+ ) -> None:
827
+ """Rewrite DB-derived control while retaining only reducer pending state."""
828
+ existing = _read_control_state(now_epoch=now_epoch)
829
+ pending = dict(
830
+ existing.pending_drops
831
+ if existing is not None
832
+ else {"fiveHour": None, "sevenDay": None}
833
+ )
834
+ for axis in observed_axes:
835
+ if axis in _candidates.AXES:
836
+ pending[axis] = None
837
+ _write_control_state(_candidates.ControlState(projection, pending))
838
+
839
+
840
+ def _tombstone_path(axis: str) -> pathlib.Path:
841
+ return (
842
+ _cctally_core.STATUSLINE_AUTHORITATIVE_5H_PATH
843
+ if axis == "fiveHour" else _cctally_core.STATUSLINE_AUTHORITATIVE_7D_PATH
844
+ )
845
+
846
+
847
+ def _tombstone_document(value: "_candidates.Tombstone") -> dict:
848
+ document = {"schemaVersion": 1, "axis": value.axis, "state": value.state}
849
+ if value.state == "inflight":
850
+ document["startedAt"] = value.started_at
851
+ document["priorBlockReceivedAtThrough"] = value.prior_block_received_at_through
852
+ else:
853
+ document["blockReceivedAtThrough"] = value.block_received_at_through
854
+ return document
855
+
856
+
857
+ def _read_tombstone(axis: str, *, now_epoch: int, fail_closed: bool = True) -> "_candidates.Tombstone | None":
858
+ try:
859
+ return _candidates.load_tombstone_document(
860
+ _tombstone_path(axis).read_bytes(), expected_axis=axis, now_epoch=now_epoch
861
+ )
862
+ except FileNotFoundError:
863
+ return None
864
+ except (OSError, _candidates.StateValidationError):
865
+ return _candidates.Tombstone(axis, "inflight") if fail_closed else None
866
+
867
+
868
+ def _write_tombstone(axis: str, value: "_candidates.Tombstone") -> None:
869
+ _atomic_write_json(_tombstone_path(axis), _tombstone_document(value))
870
+
871
+
872
+ def _authoritative_begin(axes, *, now_epoch: int) -> dict[str, "_candidates.Tombstone"]:
873
+ """Write fail-closed inflight tombstones before an authority mutation.
874
+
875
+ Each axis retains a prior committed cutoff while it is inflight. This
876
+ means that a crash after an equality-deduplicated database call has the
877
+ same safety posture as a crash before a write: no stale spool candidate
878
+ can become selected until a later authoritative repair commits it.
879
+ """
880
+ requested = frozenset(axes)
881
+ if not requested or not requested <= set(_candidates.AXES):
882
+ raise ValueError("authoritative writer requires known observation axes")
883
+ handles = {}
884
+ for axis in sorted(requested):
885
+ previous = _read_tombstone(axis, now_epoch=now_epoch, fail_closed=True)
886
+ if previous is None:
887
+ carried = None
888
+ elif previous.state == "committed":
889
+ carried = previous.block_received_at_through
890
+ else:
891
+ carried = previous.prior_block_received_at_through
892
+ inflight = _candidates.Tombstone(
893
+ axis=axis,
894
+ state="inflight",
895
+ started_at=now_epoch,
896
+ prior_block_received_at_through=carried,
897
+ )
898
+ _write_tombstone(axis, inflight)
899
+ handles[axis] = inflight
900
+ return handles
901
+
902
+
903
+ def _authoritative_commit(
904
+ handles: dict[str, "_candidates.Tombstone"], *, completion_epoch: int
905
+ ) -> None:
906
+ """Finalize every write-ahead tombstone with a monotonic cutoff."""
907
+ for axis, inflight in handles.items():
908
+ cutoff = max(
909
+ inflight.prior_block_received_at_through or 0,
910
+ completion_epoch + _cctally_core.STATUSLINE_CANDIDATE_FUTURE_SKEW_SECONDS,
911
+ )
912
+ _write_tombstone(
913
+ axis,
914
+ _candidates.Tombstone(
915
+ axis=axis,
916
+ state="committed",
917
+ block_received_at_through=cutoff,
918
+ ),
919
+ )
920
+
921
+
922
+ def _after_authoritative_record() -> None:
923
+ """Test seam immediately after the authority kernel returns success."""
924
+
925
+
926
+ def _authoritative_repair_required(*, now_epoch: int) -> bool:
927
+ """Whether an inflight/invalid tombstone needs an OAuth repair attempt."""
928
+ for axis in _candidates.AXES:
929
+ value = _read_tombstone(axis, now_epoch=now_epoch, fail_closed=True)
930
+ if value is not None and value.state == "inflight":
931
+ return True
932
+ return False
933
+
934
+
935
+ def _authoritative_record_usage(
936
+ args: argparse.Namespace,
937
+ observed_axes,
938
+ *,
939
+ lock_held: bool = False,
940
+ ) -> _AuthoritativeRecordResult:
941
+ """Record OAuth authority under write-ahead tombstones and reconcile it.
942
+
943
+ ``lock_held`` is reserved for automatic polling, which holds the selected
944
+ lock across its post-lock freshness/backoff recheck and network request.
945
+ All other callers acquire the same blocking lock here.
946
+ """
947
+ if not lock_held:
948
+ try:
949
+ with _selected_state_lock():
950
+ return _authoritative_record_usage(
951
+ args, observed_axes, lock_held=True
952
+ )
953
+ except OSError as exc:
954
+ return _AuthoritativeRecordResult("record_failed", str(exc))
955
+
956
+ now_epoch = int(time.time())
957
+ try:
958
+ handles = _authoritative_begin(observed_axes, now_epoch=now_epoch)
959
+ except (OSError, ValueError) as exc:
960
+ return _AuthoritativeRecordResult("record_failed", str(exc))
961
+
962
+ try:
963
+ rc = _cctally().cmd_record_usage(args)
964
+ except Exception as exc:
965
+ return _AuthoritativeRecordResult("record_failed", str(exc))
966
+ if rc != 0:
967
+ return _AuthoritativeRecordResult("record_failed", f"exit {rc}")
968
+
969
+ try:
970
+ # Intentionally after an equality-deduplicated success: tests use this
971
+ # seam to prove the write-ahead tombstone still fails closed on crash.
972
+ _cctally()._after_authoritative_record()
973
+ projection = _read_db_projection_stable()
974
+ completion_epoch = int(time.time())
975
+ _authoritative_commit(handles, completion_epoch=completion_epoch)
976
+ _reconcile_selected_control(
977
+ projection, now_epoch=completion_epoch, observed_axes=observed_axes
978
+ )
979
+ _cctally()._statusline_observe_touch()
980
+ except Exception as exc:
981
+ # Do not clean up the inflight tombstone on a post-record failure.
982
+ # It is the durable proof that a later authoritative writer must repair
983
+ # this axis before any spool candidate may be selected again.
984
+ return _AuthoritativeRecordResult("record_failed", str(exc))
985
+ return _AuthoritativeRecordResult("ok")
986
+
987
+
988
+ def _empty_control(projection: "_candidates.DbProjection") -> "_candidates.ControlState":
989
+ return _candidates.ControlState(projection, {"fiveHour": None, "sevenDay": None})
990
+
991
+
992
+ def _canonicalize_candidates(
993
+ candidates: tuple["_candidates.Candidate", ...],
994
+ projection: "_candidates.DbProjection",
995
+ ) -> tuple["_candidates.Candidate", ...]:
374
996
  c = _cctally()
997
+ five_values = [candidate.five_hour for candidate in candidates if candidate.five_hour is not None]
998
+ anchor = None
999
+ if projection.five_hour is not None:
1000
+ anchor = (projection.five_hour.raw_resets_at, projection.five_hour.canonical_key)
1001
+ resolved = _candidates.canonicalize_five_hour_axes(
1002
+ five_values,
1003
+ db_anchor=anchor,
1004
+ canonicalize=lambda raw, prior: c._canonical_5h_window_key(
1005
+ raw,
1006
+ prior_epoch=None if prior is None else prior[0],
1007
+ prior_key=None if prior is None else prior[1],
1008
+ ),
1009
+ )
1010
+ # Canonicalization establishes a physical-window key, not a selected
1011
+ # candidate value. Several sessions can share the exact raw reset while
1012
+ # reporting different percentages; mapping raw -> AxisValue would let the
1013
+ # final filesystem enumeration overwrite every session's percentage.
1014
+ by_raw_key = {value.raw_resets_at: value.canonical_key for value in resolved}
1015
+ result = []
1016
+ for candidate in candidates:
1017
+ seven = candidate.seven_day
1018
+ if seven is not None:
1019
+ canonical = int(_cctally_core._normalize_week_boundary_dt(
1020
+ dt.datetime.fromtimestamp(seven.raw_resets_at, tz=dt.timezone.utc)
1021
+ ).timestamp())
1022
+ seven = dataclasses.replace(seven, canonical_key=canonical)
1023
+ five = candidate.five_hour
1024
+ if five is not None:
1025
+ five = dataclasses.replace(five, canonical_key=by_raw_key[five.raw_resets_at])
1026
+ result.append(dataclasses.replace(candidate, five_hour=five, seven_day=seven))
1027
+ return tuple(result)
1028
+
1029
+
1030
+ def _build_publication_plan(
1031
+ decision: "_candidates.ReductionDecision",
1032
+ projection: "_candidates.DbProjection",
1033
+ *,
1034
+ now_epoch: int,
1035
+ ) -> "_candidates.PublicationPlan | None":
1036
+ if decision.plan is None:
1037
+ return None
1038
+ seven = decision.plan.seven_day
1039
+ if seven is None and projection.seven_day is not None:
1040
+ source = projection.seven_day
1041
+ if now_epoch < source.raw_resets_at <= now_epoch + 8 * 86400:
1042
+ seven = _candidates.AxisValue(source.percent, source.raw_resets_at, source.canonical_key)
1043
+ if seven is None:
1044
+ return None
1045
+ five = decision.plan.five_hour
1046
+ if five is None and decision.plan.seven_day is not None and projection.five_hour is not None:
1047
+ source = projection.five_hour
1048
+ if now_epoch < source.raw_resets_at <= now_epoch + 6 * 3600:
1049
+ five = _candidates.AxisValue(source.percent, source.raw_resets_at, source.canonical_key)
1050
+ if five is not None and not (now_epoch < five.raw_resets_at <= now_epoch + 6 * 3600):
1051
+ five = None
1052
+ return _candidates.PublicationPlan(seven_day=seven, five_hour=five)
1053
+
1054
+
1055
+ def _projection_changed(before: "_candidates.DbProjection", after: "_candidates.DbProjection") -> bool:
1056
+ return before.five_hour != after.five_hour or before.seven_day != after.seven_day
1057
+
1058
+
1059
+ def _statusline_reduce_and_publish() -> "_candidates.ReductionDecision | None":
1060
+ now_epoch = int(time.time())
1061
+ candidates = _load_candidate_spool(now_epoch=now_epoch)
1062
+ if not candidates:
1063
+ existing = _read_control_state(now_epoch=now_epoch)
1064
+ if existing is not None and any(existing.pending_drops.values()):
1065
+ projection = _read_db_projection_stable()
1066
+ control = _empty_control(projection)
1067
+ _write_control_state(control)
1068
+ return _candidates.ReductionDecision("WRITE_CONTROL", control)
1069
+ return None
1070
+ projection = _read_db_projection_stable()
1071
+ existing = _read_control_state(now_epoch=now_epoch)
1072
+ control_repair_required = (
1073
+ existing is None or existing.db_projection.db_files != projection.db_files
1074
+ )
1075
+ control = (
1076
+ _candidates.ControlState(projection, existing.pending_drops)
1077
+ if existing is not None else _empty_control(projection)
1078
+ )
1079
+ tombstones = {
1080
+ axis: _read_tombstone(axis, now_epoch=now_epoch)
1081
+ for axis in _candidates.AXES
1082
+ }
1083
+ candidates = _canonicalize_candidates(candidates, projection)
1084
+ decision = _candidates.reduce_candidates(
1085
+ candidates, db=projection, control=control, tombstones=tombstones, now_epoch=now_epoch
1086
+ )
1087
+ if decision.action == "NOOP":
1088
+ if control_repair_required:
1089
+ decision = dataclasses.replace(decision, action="WRITE_CONTROL")
1090
+ _write_control_state(decision.control)
1091
+ return decision
1092
+ if decision.action == "WRITE_CONTROL":
1093
+ _write_control_state(decision.control)
1094
+ return decision
1095
+ plan = _build_publication_plan(decision, projection, now_epoch=now_epoch)
1096
+ if plan is None or plan.seven_day is None:
1097
+ _write_control_state(decision.control)
1098
+ return decision
1099
+ args = _record_args(
1100
+ percent=plan.seven_day.percent,
1101
+ resets_at=plan.seven_day.raw_resets_at,
1102
+ five_hour_percent=None if plan.five_hour is None else plan.five_hour.percent,
1103
+ five_hour_resets_at=None if plan.five_hour is None else plan.five_hour.raw_resets_at,
1104
+ source="statusline",
1105
+ )
1106
+ if _cctally().cmd_record_usage(args) != 0:
1107
+ return decision
1108
+ after = _read_db_projection_stable()
1109
+ if _projection_changed(projection, after):
1110
+ # A real DB change is authoritative selected truth, so re-reduce to
1111
+ # clear any satisfied pending axis whose projection now matches it.
1112
+ refreshed = _candidates.reduce_candidates(
1113
+ candidates, db=after, control=decision.control,
1114
+ tombstones=tombstones, now_epoch=int(time.time()),
1115
+ )
1116
+ else:
1117
+ # The record kernel can deliberately leave the DB unchanged: the first
1118
+ # zero arms its own debounce and unsupported small drops are HWM
1119
+ # rejected. Preserve the precomputed bounded attempt state so the next
1120
+ # tick—not this post-record reconciliation—performs the required fresh
1121
+ # contributor-consensus pass before spending another kernel call.
1122
+ refreshed = _candidates.ReductionDecision("WRITE_CONTROL", decision.control)
1123
+ _write_control_state(refreshed.control)
1124
+ if _projection_changed(projection, after):
1125
+ _cctally()._statusline_observe_touch()
1126
+ return refreshed
1127
+
1128
+
1129
+ def _preliminary_decision() -> "_candidates.ReductionDecision | None":
1130
+ now_epoch = int(time.time())
1131
+ candidates = _load_candidate_spool(now_epoch=now_epoch)
1132
+ if not candidates:
1133
+ return None
1134
+ control = _read_control_state(now_epoch=now_epoch)
1135
+ if control is None or control.db_projection.db_files != _db_file_fingerprint():
1136
+ return _candidates.ReductionDecision("WRITE_CONTROL", control or _empty_control(
1137
+ _candidates.DbProjection(None, None, _db_file_fingerprint())
1138
+ ))
1139
+ candidates = _canonicalize_candidates(candidates, control.db_projection)
1140
+ return _candidates.reduce_candidates(
1141
+ candidates,
1142
+ db=control.db_projection,
1143
+ control=control,
1144
+ tombstones={axis: _read_tombstone(axis, now_epoch=now_epoch) for axis in _candidates.AXES},
1145
+ now_epoch=now_epoch,
1146
+ )
1147
+
1148
+
1149
+ def _fork_persist(parent_lock_fd: int) -> None:
1150
+ """Run the revalidated spool reducer in a detached serialized child."""
375
1151
  try:
376
1152
  pid = os.fork()
377
1153
  except OSError:
378
- # Out of pids/memory — skip persistence, render fast (spec §2).
379
1154
  return
380
1155
  if pid > 0:
381
- # Parent: return at once; the caller's finally releases parent_lock_fd.
382
1156
  return
383
1157
 
384
1158
  # --- child ---
385
1159
  try:
386
- # Drop the inherited copy of the parent's lock fd so only the parent's
387
- # own fd holds the shared lock; the child owns the lock ONLY via the
388
- # independent fd re-acquired below (avoids a self-block on our own
389
- # inherited exclusive lock).
390
1160
  try:
391
1161
  os.close(parent_lock_fd)
392
1162
  except OSError:
@@ -395,7 +1165,6 @@ def _fork_persist(args, parent_lock_fd: int) -> None:
395
1165
  os.setsid()
396
1166
  except OSError:
397
1167
  pass
398
- # Detach stdio immediately so nothing keeps the render's pipe open.
399
1168
  try:
400
1169
  devnull = os.open(os.devnull, os.O_RDWR)
401
1170
  os.dup2(devnull, 0)
@@ -412,18 +1181,12 @@ def _fork_persist(args, parent_lock_fd: int) -> None:
412
1181
  _cctally_core.STATUSLINE_PERSIST_LOCK_PATH,
413
1182
  os.O_WRONLY | os.O_CREAT, 0o644,
414
1183
  )
415
- # BLOCKING: wait for the parent to release its copy, then hold the
416
- # lock across record + marker-touch so a concurrent render can't
417
- # observe a stale marker and double-insert.
418
1184
  fcntl.flock(child_fd, fcntl.LOCK_EX)
419
1185
  except OSError:
420
1186
  child_fd = -1
421
1187
  try:
422
- throttle = float(_cctally_core.STATUSLINE_PERSIST_THROTTLE_SECONDS)
423
- if c._statusline_observe_age_seconds() >= throttle:
424
- c.cmd_record_usage(args)
425
- # Touch the liveness marker even on a dedup no-op return.
426
- c._statusline_observe_touch()
1188
+ if child_fd >= 0:
1189
+ _statusline_reduce_and_publish()
427
1190
  finally:
428
1191
  if child_fd >= 0:
429
1192
  try:
@@ -435,70 +1198,37 @@ def _fork_persist(args, parent_lock_fd: int) -> None:
435
1198
  except OSError:
436
1199
  pass
437
1200
  except BaseException:
438
- # A detached child must never surface a traceback onto the render.
439
1201
  pass
440
1202
  finally:
441
1203
  os._exit(0)
442
1204
 
443
1205
 
444
1206
  def _statusline_persist(parsed, *, sync_for_test: bool = False) -> None:
445
- """Persist the parsed CC `rate_limits` (the primary automatic writer).
446
-
447
- Fully guarded — cmd_statusline also wraps this in try/except, and every
448
- step degrades to a clean no-op. ``sync_for_test=True`` runs the kernel
449
- INLINE (no fork) so persistence tests are deterministic and no detached
450
- child outlives fixture cleanup."""
451
- c = _cctally()
452
- # 0. Pool-identity guard (spec 2026-07-17 #311 D1). A bracket-variant
453
- # model id (e.g. `claude-opus-4-8[1m]`) reports a SEPARATE rate-limit
454
- # pool; persisting it poisons the default-pool DB (HWM latch + dedup
455
- # freeze). Skip BEFORE the lock/fork AND before touching the
456
- # observation marker — a foreign-pool session is not evidence the
457
- # regular-pool pipeline is alive, so the OAuth backfill must keep
458
- # aging. Render is unchanged (this is persist-only). `.model_id` is a
459
- # dataclass attr, not a dict key, so this never AttributeErrors into
460
- # cmd_statusline's silent `except`.
1207
+ """Spool an eligible session candidate then reduce it opportunistically."""
461
1208
  if _lib_statusline.is_alternate_pool_model_id(parsed.model_id):
462
1209
  return
463
- # 1. Require a usable 7d reading. Absence is a clean no-op (older CC / CC
464
- # not supplying rate_limits — the OAuth backfill covers that case).
465
- if parsed.rate_limits_7d_pct is None or parsed.rate_limits_7d_resets_at is None:
1210
+ candidate = _candidate_from_input(parsed, received_at=int(time.time()))
1211
+ if candidate is None:
466
1212
  return
467
- # 2. 5h pair-gate: pass both or neither. An inactive 5h window arrives as
468
- # {used_percentage: 0, resets_at: null}; the parser surfaces the two
469
- # fields independently, so drop the whole pair when either is missing
470
- # (mirrors both OAuth paths).
471
- five_pct = parsed.rate_limits_5h_pct
472
- five_reset = parsed.rate_limits_5h_resets_at
473
- if five_pct is None or five_reset is None:
474
- five_pct = None
475
- five_reset = None
476
- # 3. Non-blocking cross-process lock — losers render without forking.
477
- lock_fd = _try_acquire_persist_lock()
1213
+ try:
1214
+ _write_candidate(candidate)
1215
+ _cctally()._statusline_transport_touch()
1216
+ except OSError:
1217
+ return
1218
+ c = _cctally()
1219
+ lock_fd = c._try_acquire_persist_lock()
478
1220
  if lock_fd is None:
479
1221
  return
480
1222
  try:
481
- # 4. Liveness throttle UNDER the lock (observation marker, NOT
482
- # snapshot age).
483
- throttle = float(_cctally_core.STATUSLINE_PERSIST_THROTTLE_SECONDS)
484
- if c._statusline_observe_age_seconds() < throttle:
1223
+ preliminary = _preliminary_decision()
1224
+ if preliminary is None or preliminary.action == "NOOP":
485
1225
  return
486
- args = _record_args(
487
- percent=parsed.rate_limits_7d_pct,
488
- resets_at=parsed.rate_limits_7d_resets_at,
489
- five_hour_percent=five_pct,
490
- five_hour_resets_at=five_reset,
491
- source="statusline",
492
- )
493
- # 5. Run the kernel — detached unless test-sync.
494
1226
  if sync_for_test:
495
- c.cmd_record_usage(args)
496
- # Touch the liveness marker even on a dedup no-op return.
497
- c._statusline_observe_touch()
1227
+ _statusline_reduce_and_publish()
498
1228
  return
499
- _fork_persist(args, lock_fd)
1229
+ _fork_persist(lock_fd)
500
1230
  finally:
501
- _release_persist_lock(lock_fd)
1231
+ c._release_persist_lock(lock_fd)
502
1232
 
503
1233
 
504
1234
  def _resolve_context_window(model_id, warn_once) -> "int | None":
@@ -881,4 +1611,3 @@ def _build_statusline_injections(warn_once):
881
1611
  context_pct=_context_pct,
882
1612
  warn_once=warn_once,
883
1613
  )
884
-