cctally 1.116.0 → 1.117.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 +36 -0
- package/README.md +4 -6
- package/bin/_cctally_account.py +57 -3
- package/bin/_cctally_cache.py +5720 -885
- package/bin/_cctally_claude_attribution.py +997 -0
- package/bin/_cctally_completion.py +101 -28
- package/bin/_cctally_core.py +235 -12
- package/bin/_cctally_dashboard.py +224 -7
- package/bin/_cctally_dashboard_conversation.py +107 -21
- package/bin/_cctally_dashboard_envelope.py +34 -0
- package/bin/_cctally_db.py +928 -80
- package/bin/_cctally_diagnosis_sources.py +78 -13
- package/bin/_cctally_doctor.py +211 -86
- package/bin/_cctally_five_hour.py +389 -18
- package/bin/_cctally_forecast.py +30 -6
- package/bin/_cctally_journal.py +2418 -158
- package/bin/_cctally_oauth_samples.py +185 -7
- package/bin/_cctally_parser.py +38 -0
- package/bin/_cctally_project.py +34 -8
- package/bin/_cctally_quarantine.py +821 -343
- package/bin/_cctally_quota.py +69 -4
- package/bin/_cctally_quota_calibration.py +677 -3
- package/bin/_cctally_quota_model.py +927 -127
- package/bin/_cctally_record.py +922 -445
- package/bin/_cctally_rederive.py +119 -15
- package/bin/_cctally_statusline.py +1048 -176
- package/bin/_cctally_store.py +544 -110
- package/bin/_cctally_tui.py +93 -9
- package/bin/_cctally_update.py +77 -14
- package/bin/_lib_account_protocol.py +227 -19
- package/bin/_lib_claude_attribution.py +421 -0
- package/bin/_lib_clone_copy.py +30 -0
- package/bin/_lib_codex_conversation_query.py +45 -9
- package/bin/_lib_codex_conversation_watch.py +72 -13
- package/bin/_lib_codex_rollout_container.py +234 -19
- package/bin/_lib_codex_worker_lifecycle.py +374 -0
- package/bin/_lib_credit.py +22 -13
- package/bin/_lib_dashboard_sources.py +163 -10
- package/bin/_lib_detector_access_registry.py +1335 -166
- package/bin/_lib_doctor.py +276 -92
- package/bin/_lib_ingest_frontier.py +269 -36
- package/bin/_lib_journal.py +273 -22
- package/bin/_lib_jsonl.py +12 -0
- package/bin/_lib_meter_rate_change.py +72 -0
- package/bin/_lib_outcome_quarantine.py +917 -554
- package/bin/_lib_provider_activity.py +24 -6
- package/bin/_lib_quota_calibration.py +57 -3
- package/bin/_lib_quota_copy.py +7 -2
- package/bin/_lib_quota_lineage.py +613 -0
- package/bin/_lib_quota_model.py +435 -22
- package/bin/_lib_rate_change_delivery.py +16 -0
- package/bin/_lib_reclaim_planner.py +6 -4
- package/bin/_lib_rederive.py +2046 -21
- package/bin/_lib_selector_state.py +131 -3
- package/bin/_lib_stale_proof.py +113 -4
- package/bin/_lib_statusline.py +72 -3
- package/bin/_lib_statusline_candidates.py +246 -42
- package/bin/_lib_write_io.py +418 -5
- package/bin/cctally +24 -11
- package/dashboard/static/assets/{ConversationsView-Dflb_myA.js → ConversationsView-Dcfn8T6f.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-DFX5CfIs.js → DoctorModal-BqmXbnhn.js} +1 -1
- package/dashboard/static/assets/{ModalRoot-HlxoagIa.js → ModalRoot-Ck3WJK2V.js} +1 -1
- package/dashboard/static/assets/{ProjectsDrillPanel-C3PJy1xx.js → ProjectsDrillPanel-B-HZkDnT.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BczsWKhD.js → SourceDetailModal-QhJkHB7i.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-Os-Lo5cs.js → UpdateModal-CkXEIQcK.js} +1 -1
- package/dashboard/static/assets/index-D34eYhAc.css +1 -0
- package/dashboard/static/assets/{index-CYZ3NLqF.js → index-HKS1s56o.js} +3 -3
- package/dashboard/static/assets/{outlineNavigation-k-zIif6W.js → outlineNavigation-BNGaNPCp.js} +1 -1
- package/dashboard/static/assets/{useKeymap-CW7Up0Ay.js → useKeymap-CUgEnnua.js} +1 -1
- package/dashboard/static/dashboard.html +3 -3
- package/package.json +5 -1
- package/dashboard/static/assets/index-BKITk1CT.css +0 -1
|
@@ -26,9 +26,16 @@ nothing, and one that conflicts is detected from the retained whole-record
|
|
|
26
26
|
digest, which taints the batch and forces a rebuild that re-derives from the
|
|
27
27
|
journal.
|
|
28
28
|
|
|
29
|
-
This module imports nothing outside the stdlib
|
|
30
|
-
|
|
31
|
-
journal on disk — the same
|
|
29
|
+
This module imports nothing outside the stdlib, `_lib_journal` and the pure
|
|
30
|
+
`_lib_quota_lineage` kernel, and in particular never imports
|
|
31
|
+
`_cctally_journal`, so it is unit-testable without a journal on disk — the same
|
|
32
|
+
rule `bin/_lib_journal_router.py` follows.
|
|
33
|
+
|
|
34
|
+
#1015 (spec 5.1): the batch-completion step also gives each newly completed
|
|
35
|
+
batch's lineage footprint (:func:`completed_batch_footprints`) while it still
|
|
36
|
+
holds every action core, before compaction drops them. The footprint is kept
|
|
37
|
+
off :class:`SelectorRows` — those rows are compared for validation and owned by
|
|
38
|
+
the selector — and handed to the caller through ``lineage`` out-dicts.
|
|
32
39
|
"""
|
|
33
40
|
from __future__ import annotations
|
|
34
41
|
|
|
@@ -36,6 +43,7 @@ import json
|
|
|
36
43
|
from dataclasses import dataclass, replace
|
|
37
44
|
|
|
38
45
|
import _lib_journal as _jl
|
|
46
|
+
import _lib_quota_lineage
|
|
39
47
|
|
|
40
48
|
|
|
41
49
|
#: Bumped whenever the durable row shapes or the merge semantics change. A
|
|
@@ -800,6 +808,107 @@ def advance_counter(
|
|
|
800
808
|
)
|
|
801
809
|
|
|
802
810
|
|
|
811
|
+
def _revision_of(winner, owner_of):
|
|
812
|
+
"""The `_lib_quota_lineage.Revision` of one winning candidate."""
|
|
813
|
+
if winner.status != "active" or winner.record is None:
|
|
814
|
+
return _lib_quota_lineage.Revision("tombstone")
|
|
815
|
+
return _lib_quota_lineage.Revision(
|
|
816
|
+
"active", owner_of((winner.record or {}).get("payload")))
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def _takes_winner(prior, candidate) -> bool:
|
|
820
|
+
"""The selector's winner rule: the highest revision, and at the same
|
|
821
|
+
revision the lowest sequence (a durable winner with no sequence keeps its
|
|
822
|
+
place)."""
|
|
823
|
+
if prior is None or candidate.rev > prior.rev:
|
|
824
|
+
return True
|
|
825
|
+
if candidate.rev < prior.rev:
|
|
826
|
+
return False
|
|
827
|
+
return (prior.sequence is not None and candidate.sequence is not None
|
|
828
|
+
and candidate.sequence < prior.sequence)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def completed_batch_footprints(fold, batch_ids, *, owner_of, seed=None):
|
|
832
|
+
"""#1015 spec 5.1: the footprint of each completed batch in ``batch_ids``.
|
|
833
|
+
|
|
834
|
+
Returns ``{batch_id: frozenset((provider, account_key), ...)}`` through
|
|
835
|
+
`_lib_quota_lineage.batch_footprint`. Batches are walked in commit
|
|
836
|
+
order, and each action's predecessor is the revision effective
|
|
837
|
+
immediately before its batch commits: the winner over ``seed`` (the
|
|
838
|
+
durable winner of an event id before ``fold``'s stream, None on a full
|
|
839
|
+
pass) advanced by every base evt folded before that commit and by every
|
|
840
|
+
earlier completed batch — dominated and tombstoned revisions included —
|
|
841
|
+
then the revision the batch's own earlier action produced.
|
|
842
|
+
|
|
843
|
+
``owner_of(payload)`` is the `(provider, account_key)` the evt fold stores
|
|
844
|
+
for a payload, or None when it cannot be resolved. ``batch_ids`` names
|
|
845
|
+
batches whose cores ``fold`` still holds (newly completed on this pass);
|
|
846
|
+
any other id is ignored.
|
|
847
|
+
"""
|
|
848
|
+
targets = []
|
|
849
|
+
for batch_id in batch_ids:
|
|
850
|
+
if batch_id not in fold.completed:
|
|
851
|
+
continue
|
|
852
|
+
commit = fold.markers.get(batch_id, {}).get("commit")
|
|
853
|
+
actions = fold.actions.get(batch_id, {})
|
|
854
|
+
if commit is None or any(
|
|
855
|
+
actions[seq][0] is None for seq in actions):
|
|
856
|
+
continue
|
|
857
|
+
targets.append((commit[3], batch_id))
|
|
858
|
+
if not targets:
|
|
859
|
+
return {}
|
|
860
|
+
targets.sort()
|
|
861
|
+
base = sorted(
|
|
862
|
+
(candidate for candidate in fold.candidates
|
|
863
|
+
if candidate.batch_id is None),
|
|
864
|
+
key=lambda item: item.sequence)
|
|
865
|
+
winners: dict = {}
|
|
866
|
+
seeded: set = set()
|
|
867
|
+
|
|
868
|
+
def current(event_id):
|
|
869
|
+
if event_id not in seeded:
|
|
870
|
+
seeded.add(event_id)
|
|
871
|
+
if event_id not in winners and seed is not None:
|
|
872
|
+
prior = seed(event_id)
|
|
873
|
+
if prior is not None:
|
|
874
|
+
winners[event_id] = prior
|
|
875
|
+
return winners.get(event_id)
|
|
876
|
+
|
|
877
|
+
def offer(candidate):
|
|
878
|
+
prior = current(candidate.event_id)
|
|
879
|
+
if _takes_winner(prior, candidate):
|
|
880
|
+
winners[candidate.event_id] = candidate
|
|
881
|
+
|
|
882
|
+
footprints: dict = {}
|
|
883
|
+
position = 0
|
|
884
|
+
for commit_sequence, batch_id in targets:
|
|
885
|
+
while position < len(base) and base[position].sequence < commit_sequence:
|
|
886
|
+
offer(base[position])
|
|
887
|
+
position += 1
|
|
888
|
+
batch_actions = fold.actions.get(batch_id, {})
|
|
889
|
+
ordered = [batch_actions[seq] for seq in sorted(batch_actions)]
|
|
890
|
+
actions = []
|
|
891
|
+
for normalized, _digest, _sequence in ordered:
|
|
892
|
+
tombstone = normalized.get("action") == "tombstone"
|
|
893
|
+
actions.append(_lib_quota_lineage.Action(
|
|
894
|
+
event_id=normalized["id"],
|
|
895
|
+
rev=int(normalized["rev"]),
|
|
896
|
+
kind="tombstone" if tombstone else "replace",
|
|
897
|
+
payload_owner=(None if tombstone
|
|
898
|
+
else owner_of(normalized.get("payload"))),
|
|
899
|
+
))
|
|
900
|
+
|
|
901
|
+
def predecessor_of(event_id, _rev):
|
|
902
|
+
winner = current(event_id)
|
|
903
|
+
return None if winner is None else _revision_of(winner, owner_of)
|
|
904
|
+
|
|
905
|
+
footprints[batch_id] = _lib_quota_lineage.batch_footprint(
|
|
906
|
+
actions, predecessor_of)
|
|
907
|
+
for normalized, _digest, sequence in ordered:
|
|
908
|
+
offer(_jl._candidate_from_correction(normalized, sequence))
|
|
909
|
+
return footprints
|
|
910
|
+
|
|
911
|
+
|
|
803
912
|
def merge_delta(
|
|
804
913
|
rows: SelectorRows,
|
|
805
914
|
new_records,
|
|
@@ -807,6 +916,7 @@ def merge_delta(
|
|
|
807
916
|
next_sequence: int,
|
|
808
917
|
coordinates=None,
|
|
809
918
|
covered=None,
|
|
919
|
+
lineage=None,
|
|
810
920
|
):
|
|
811
921
|
"""Continue the fold over ``new_records`` from durable state.
|
|
812
922
|
|
|
@@ -826,6 +936,13 @@ def merge_delta(
|
|
|
826
936
|
`journal_protocol_resolution` op: acknowledging a violation authenticates an
|
|
827
937
|
exact length-framed raw-prefix SHA-256, a claimed hash is never accepted
|
|
828
938
|
without recomputation, and no durable summary can reconstruct it.
|
|
939
|
+
|
|
940
|
+
``lineage`` (#1015) is an optional out-dict. When it carries an
|
|
941
|
+
``"owner_of"`` callable, it receives under ``"footprints"`` the
|
|
942
|
+
footprint of every batch this delta completed, computed here, in the
|
|
943
|
+
batch-completion step, before the cores are compacted away (see
|
|
944
|
+
:func:`completed_batch_footprints`); the predecessor of an action is
|
|
945
|
+
seeded from the durable winner of its event id.
|
|
829
946
|
"""
|
|
830
947
|
fold = _seed_fold(rows)
|
|
831
948
|
seeded_completed = {
|
|
@@ -903,6 +1020,17 @@ def merge_delta(
|
|
|
903
1020
|
# this one converges through the narrowest prefix.
|
|
904
1021
|
transitions.sort(key=lambda item: (item.causal_sequence, item.batch_id))
|
|
905
1022
|
|
|
1023
|
+
if lineage is not None and lineage.get("owner_of") is not None:
|
|
1024
|
+
prior_rows = {row.event_id: row for row in rows.effective}
|
|
1025
|
+
|
|
1026
|
+
def seed(event_id):
|
|
1027
|
+
row = prior_rows.get(event_id)
|
|
1028
|
+
return None if row is None else _event_from_row(row)
|
|
1029
|
+
|
|
1030
|
+
lineage["footprints"] = completed_batch_footprints(
|
|
1031
|
+
fold, sorted(fold.completed - seeded_completed),
|
|
1032
|
+
owner_of=lineage["owner_of"], seed=seed)
|
|
1033
|
+
|
|
906
1034
|
winners, conflicts = _merge_candidates(rows, fold.candidates)
|
|
907
1035
|
batches, batch_records = _batch_rows(fold, coordinates)
|
|
908
1036
|
merged_batches = _carry_coordinates(rows, batches)
|
package/bin/_lib_stale_proof.py
CHANGED
|
@@ -20,6 +20,14 @@ Both axes are decided here. The weekly axis earns by (i) or (ii); the
|
|
|
20
20
|
five-hour axis earns by (i) only, against the `statusline` source's detector
|
|
21
21
|
baseline under the current five-hour credit generation (D11), and its
|
|
22
22
|
validity follows the five-hour rows of the section 4.4 table.
|
|
23
|
+
|
|
24
|
+
#1019/#1014 (spec 4.1) adds the receipt-free DROP proof beside it: a
|
|
25
|
+
member's LOW is proven stale by an admitted provider sample in its exact
|
|
26
|
+
account and window, requested after the member's bound (its frozen supported
|
|
27
|
+
``t_m``, else its ``received_at`` second), reading more than `TOLERANCE_PP`
|
|
28
|
+
ABOVE it and captured before the member's advertised reset
|
|
29
|
+
(`proven_stale_low`). Its facts are independent of the rise facts: a member
|
|
30
|
+
may be eligible for one direction and refused for the other.
|
|
23
31
|
"""
|
|
24
32
|
from __future__ import annotations
|
|
25
33
|
|
|
@@ -124,14 +132,36 @@ class Member:
|
|
|
124
132
|
|
|
125
133
|
@dataclasses.dataclass(frozen=True)
|
|
126
134
|
class MemberFacts:
|
|
135
|
+
"""One member's refusal facts. ``refused``, ``proof_observation_id`` and
|
|
136
|
+
``receipt_valid`` decide its RISE (#797 spec 4.4); ``drop_refused`` and
|
|
137
|
+
``drop_proof_observation_id`` decide its DROP (#1019 spec 4.1),
|
|
138
|
+
independently and without a receipt."""
|
|
139
|
+
|
|
127
140
|
refused: bool
|
|
128
141
|
proof_observation_id: str | None
|
|
129
142
|
receipt_valid: bool
|
|
143
|
+
drop_refused: bool = False
|
|
144
|
+
drop_proof_observation_id: str | None = None
|
|
130
145
|
|
|
131
146
|
|
|
147
|
+
#: Eligible in both directions.
|
|
132
148
|
ALL_ELIGIBLE = MemberFacts(False, None, False)
|
|
133
149
|
|
|
134
150
|
|
|
151
|
+
@dataclasses.dataclass(frozen=True)
|
|
152
|
+
class DropMember:
|
|
153
|
+
"""A member's reading as the drop proof sees it (#1019 spec 4.1): its
|
|
154
|
+
contributor token, its bound (`drop_bound`), its axis value, its window
|
|
155
|
+
and the raw reset it advertised."""
|
|
156
|
+
|
|
157
|
+
token: str
|
|
158
|
+
bound_utc: str
|
|
159
|
+
axis: str
|
|
160
|
+
percent: float
|
|
161
|
+
window: int
|
|
162
|
+
advertised_reset_epoch: int | None = None
|
|
163
|
+
|
|
164
|
+
|
|
135
165
|
def sample_identity(token: str, witness, *, axis: str, percent: float,
|
|
136
166
|
window: int) -> SampleIdentity | None:
|
|
137
167
|
"""σ for a member, or None when its evidence is unknown: no witness, an
|
|
@@ -147,6 +177,45 @@ def sample_identity(token: str, witness, *, axis: str, percent: float,
|
|
|
147
177
|
witness.evidence_revision, witness.parser_revision)
|
|
148
178
|
|
|
149
179
|
|
|
180
|
+
def drop_bound(token: str, witness, received_at_epoch: int | None, *, axis: str,
|
|
181
|
+
percent: float, window: int) -> str | None:
|
|
182
|
+
"""The bound a member's LOW is proven stale after (#1019 spec 4.1): the
|
|
183
|
+
frozen, supported ``t_m`` when `sample_identity` accepts its witness;
|
|
184
|
+
otherwise its ``received_at`` as a whole-second UTC instant, an
|
|
185
|
+
assumption-free upper bound on when its already-composed reading
|
|
186
|
+
existed; otherwise None (always eligible). The fallback is used for this
|
|
187
|
+
proof alone: it is never a σ, a receipt or a provenance field."""
|
|
188
|
+
sigma = sample_identity(token, witness, axis=axis, percent=percent, window=window)
|
|
189
|
+
if sigma is not None:
|
|
190
|
+
return sigma.activity_at_utc
|
|
191
|
+
return _received_bound(received_at_epoch)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _received_bound(received_at_epoch) -> str | None:
|
|
195
|
+
if (isinstance(received_at_epoch, bool) or not isinstance(received_at_epoch, int)
|
|
196
|
+
or received_at_epoch < 0):
|
|
197
|
+
return None
|
|
198
|
+
import datetime as _dt
|
|
199
|
+
return _dt.datetime.fromtimestamp(received_at_epoch, tz=_dt.timezone.utc).strftime(
|
|
200
|
+
"%Y-%m-%dT%H:%M:%SZ")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def witness_bound(witness, received_at_epoch: int | None) -> str | None:
|
|
204
|
+
"""`drop_bound` of a retained support: whether `sample_identity` accepts
|
|
205
|
+
a witness depends on the witness alone, so the bound needs neither the
|
|
206
|
+
member's token nor its axis, value or window."""
|
|
207
|
+
if sample_identity("", witness, axis=WEEKLY, percent=0.0, window=0) is not None:
|
|
208
|
+
return witness.instant_utc
|
|
209
|
+
return _received_bound(received_at_epoch)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def bound_end(bound_utc: str | None) -> int | None:
|
|
213
|
+
"""The end of a bound's precision interval in nanoseconds: a sample
|
|
214
|
+
follows the bound when its request starts at or after it (`after`)."""
|
|
215
|
+
interval = _interval(bound_utc)
|
|
216
|
+
return None if interval is None else interval[1]
|
|
217
|
+
|
|
218
|
+
|
|
150
219
|
def _window_text(window: int) -> str:
|
|
151
220
|
return _samples.normalize_weekly_reset(int(window))
|
|
152
221
|
|
|
@@ -217,6 +286,31 @@ def proven_stale(sigma: SampleIdentity, samples: Iterable, *, after_utc: str,
|
|
|
217
286
|
return _latest(proving)
|
|
218
287
|
|
|
219
288
|
|
|
289
|
+
def proven_stale_low(bound_utc: str, samples: Iterable, *, axis: str, window: int,
|
|
290
|
+
percent: float, advertised_reset_epoch: int | None = None):
|
|
291
|
+
"""The latest provider sample proving a member's LOW stale, or None
|
|
292
|
+
(#1019 spec 4.1, Q1).
|
|
293
|
+
|
|
294
|
+
A sample proves it when it is in the member's exact window (weekly: the
|
|
295
|
+
normalized reset; five-hour: the canonical key; the caller passes only
|
|
296
|
+
the member's own account's samples), its request instant ``q`` follows
|
|
297
|
+
``bound_utc`` (`drop_bound`) under the precision intervals, its raw axis
|
|
298
|
+
value is strictly more than `TOLERANCE_PP` ABOVE ``percent``, and its
|
|
299
|
+
response was captured before the member's advertised reset (the
|
|
300
|
+
window's reset when it advertised none). No receipt, no ``e_σ``, and no
|
|
301
|
+
baseline or threshold cutoff."""
|
|
302
|
+
if axis not in AXES or _interval(bound_utc) is None:
|
|
303
|
+
return None
|
|
304
|
+
reset = int(window) if advertised_reset_epoch is None else int(advertised_reset_epoch)
|
|
305
|
+
proving = [
|
|
306
|
+
s for s in _axis_window(samples, axis, window)
|
|
307
|
+
if _axis_value(s, axis) > float(percent) + TOLERANCE_PP
|
|
308
|
+
and after(s.requested_at_utc, bound_utc)
|
|
309
|
+
and _before_epoch(s.captured_at_utc, reset)
|
|
310
|
+
]
|
|
311
|
+
return _latest(proving)
|
|
312
|
+
|
|
313
|
+
|
|
220
314
|
# --- accounting ------------------------------------------------------------------------
|
|
221
315
|
|
|
222
316
|
|
|
@@ -392,7 +486,8 @@ def receipt_valid(receipt: Receipt, transitions: Iterable[CreditTransition], *,
|
|
|
392
486
|
|
|
393
487
|
def refusal_facts(axis: str, members: Sequence[Member], samples: Iterable,
|
|
394
488
|
receipts: Iterable[Receipt], transitions: Iterable[CreditTransition], *,
|
|
395
|
-
index_identity: str, supports: Collection[str] | None = None
|
|
489
|
+
index_identity: str, supports: Collection[str] | None = None,
|
|
490
|
+
drop_members: Sequence[DropMember] = ()
|
|
396
491
|
) -> dict[str, MemberFacts]:
|
|
397
492
|
"""Per-member facts for one axis, keyed by contributor token.
|
|
398
493
|
|
|
@@ -400,7 +495,12 @@ def refusal_facts(axis: str, members: Sequence[Member], samples: Iterable,
|
|
|
400
495
|
and a sample requested after both ``t_m`` and ``e_σ`` proves it stale.
|
|
401
496
|
A member without a receipt is eligible with no proof reported; one whose
|
|
402
497
|
receipt is invalid is eligible with its proof reported. Each axis is
|
|
403
|
-
decided on its own members, samples and credits.
|
|
498
|
+
decided on its own members, samples and credits.
|
|
499
|
+
|
|
500
|
+
#1019 (spec 4.1): every ``drop_members`` entry gets its drop facts from
|
|
501
|
+
`proven_stale_low`, independently of ``receipts``, so one call per axis
|
|
502
|
+
yields complete facts; a token with drop facts only keeps the rise
|
|
503
|
+
fields of `ALL_ELIGIBLE`."""
|
|
404
504
|
if axis not in AXES:
|
|
405
505
|
return {}
|
|
406
506
|
samples = list(samples)
|
|
@@ -433,6 +533,15 @@ def refusal_facts(axis: str, members: Sequence[Member], samples: Iterable,
|
|
|
433
533
|
refused=bool(valid) and strict is not None,
|
|
434
534
|
proof_observation_id=None if proof is None else proof.observation_id,
|
|
435
535
|
receipt_valid=bool(valid))
|
|
536
|
+
for d in drop_members:
|
|
537
|
+
if d.axis != axis:
|
|
538
|
+
continue
|
|
539
|
+
low = proven_stale_low(d.bound_utc, samples, axis=axis, window=d.window,
|
|
540
|
+
percent=d.percent,
|
|
541
|
+
advertised_reset_epoch=d.advertised_reset_epoch)
|
|
542
|
+
facts[d.token] = dataclasses.replace(
|
|
543
|
+
facts.get(d.token, ALL_ELIGIBLE), drop_refused=low is not None,
|
|
544
|
+
drop_proof_observation_id=None if low is None else low.observation_id)
|
|
436
545
|
return facts
|
|
437
546
|
|
|
438
547
|
|
|
@@ -535,8 +644,8 @@ def floor_microsecond(text: str) -> str | None:
|
|
|
535
644
|
|
|
536
645
|
def compact_samples(samples: Iterable, *, window: int,
|
|
537
646
|
floor_epoch: int | None, axis: str = WEEKLY) -> tuple:
|
|
538
|
-
"""A subset of the window's samples that yields the same `proven_stale
|
|
539
|
-
and `earn_receipts` answers for every member (
|
|
647
|
+
"""A subset of the window's samples that yields the same `proven_stale`,
|
|
648
|
+
`proven_stale_low` and `earn_receipts` answers for every member (all pick the latest
|
|
540
649
|
qualifying sample, and qualification is monotone in ``q`` within one
|
|
541
650
|
value): per weekly value, the latest sample and the latest captured at
|
|
542
651
|
or after the floor, plus every sample captured within an hour of the
|
package/bin/_lib_statusline.py
CHANGED
|
@@ -10,6 +10,7 @@ for the full design.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
|
+
import math
|
|
13
14
|
import pathlib
|
|
14
15
|
import re
|
|
15
16
|
import sys
|
|
@@ -88,6 +89,12 @@ class StatuslineInput:
|
|
|
88
89
|
rate_limits_7d_pct: Optional[float] = None
|
|
89
90
|
rate_limits_7d_resets_at: Optional[int] = None # unix epoch
|
|
90
91
|
raw: dict = field(default_factory=dict) # full parsed JSON for diagnostics
|
|
92
|
+
# #1002. Claude Code's own `context_window`, already validated (see
|
|
93
|
+
# `_to_count`): the session's window and the last request's input-side
|
|
94
|
+
# token sum (input + cache_read + cache_creation). `None` when absent or
|
|
95
|
+
# malformed; the `context_pct` port then falls back per value.
|
|
96
|
+
context_window_size: Optional[int] = None
|
|
97
|
+
context_tokens: Optional[int] = None
|
|
91
98
|
|
|
92
99
|
|
|
93
100
|
# ---- CLI args (post-config-resolution) -------------------------------------
|
|
@@ -143,8 +150,13 @@ class StatuslineInjections:
|
|
|
143
150
|
[],
|
|
144
151
|
"Optional[tuple[Optional[float], Optional[int], Optional[float], Optional[int]]]",
|
|
145
152
|
]
|
|
146
|
-
# transcript_path
|
|
147
|
-
|
|
153
|
+
# (transcript_path, model_id, window_size, context_tokens) → context %
|
|
154
|
+
# or None if unavailable. The two payload values win when not None; the
|
|
155
|
+
# port falls back to the model table / transcript tail independently.
|
|
156
|
+
context_pct: Callable[
|
|
157
|
+
[Optional[str], Optional[str], Optional[int], Optional[int]],
|
|
158
|
+
Optional[float],
|
|
159
|
+
]
|
|
148
160
|
# Emits one-shot stderr warnings (deduped by message — caller maintains set).
|
|
149
161
|
warn_once: Callable[[str], None]
|
|
150
162
|
# #661 S2 §9. The stored metering regimes for this install, oldest
|
|
@@ -230,6 +242,13 @@ def parse_statusline_stdin(raw: "bytes | str") -> "StatuslineInput | ParseError"
|
|
|
230
242
|
def _to_str(v) -> Optional[str]:
|
|
231
243
|
return v if isinstance(v, str) and v else None
|
|
232
244
|
|
|
245
|
+
context_window = parsed.get("context_window")
|
|
246
|
+
if not isinstance(context_window, dict):
|
|
247
|
+
context_window = {}
|
|
248
|
+
window_size = _to_count(context_window.get("context_window_size"))
|
|
249
|
+
if window_size is not None and window_size <= 0:
|
|
250
|
+
window_size = None
|
|
251
|
+
|
|
233
252
|
return StatuslineInput(
|
|
234
253
|
session_id=_to_str(parsed.get("session_id")),
|
|
235
254
|
model_id=_to_str(_get(parsed, "model", "id")),
|
|
@@ -250,9 +269,56 @@ def parse_statusline_stdin(raw: "bytes | str") -> "StatuslineInput | ParseError"
|
|
|
250
269
|
_get(parsed, "rate_limits", "seven_day", "resets_at")
|
|
251
270
|
),
|
|
252
271
|
raw=parsed,
|
|
272
|
+
context_window_size=window_size,
|
|
273
|
+
context_tokens=_context_tokens(context_window.get("current_usage")),
|
|
253
274
|
)
|
|
254
275
|
|
|
255
276
|
|
|
277
|
+
# The input-side `current_usage` fields that occupy the context window.
|
|
278
|
+
# `output_tokens` is deliberately excluded (#1002 spec §5.1).
|
|
279
|
+
_CONTEXT_USAGE_FIELDS = (
|
|
280
|
+
"input_tokens", "cache_read_input_tokens", "cache_creation_input_tokens",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _to_count(v) -> Optional[int]:
|
|
285
|
+
"""A token count: a finite, integer-valued, non-negative JSON number.
|
|
286
|
+
|
|
287
|
+
Never a bool (an int subclass) or a string; an integral float such as
|
|
288
|
+
``1000000.0`` normalizes to ``int``. Anything else → ``None``.
|
|
289
|
+
"""
|
|
290
|
+
if isinstance(v, bool) or not isinstance(v, (int, float)):
|
|
291
|
+
return None
|
|
292
|
+
if isinstance(v, float):
|
|
293
|
+
if not math.isfinite(v) or not v.is_integer():
|
|
294
|
+
return None
|
|
295
|
+
v = int(v)
|
|
296
|
+
return v if v >= 0 else None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _context_tokens(usage) -> Optional[int]:
|
|
300
|
+
"""Sum of the input-side ``current_usage`` fields, or ``None``.
|
|
301
|
+
|
|
302
|
+
A missing or null field contributes zero, but at least one must be
|
|
303
|
+
present and non-null; an explicit zero is a valid observation. Any
|
|
304
|
+
invalid present field voids the whole sum rather than being skipped.
|
|
305
|
+
"""
|
|
306
|
+
if not isinstance(usage, dict):
|
|
307
|
+
return None
|
|
308
|
+
total = 0
|
|
309
|
+
observed = False
|
|
310
|
+
for key in _CONTEXT_USAGE_FIELDS:
|
|
311
|
+
value = usage.get(key)
|
|
312
|
+
if value is None:
|
|
313
|
+
continue
|
|
314
|
+
count = _to_count(value)
|
|
315
|
+
if count is None:
|
|
316
|
+
return None
|
|
317
|
+
total += count
|
|
318
|
+
observed = True
|
|
319
|
+
return total if observed else None
|
|
320
|
+
|
|
321
|
+
|
|
256
322
|
# ---- Segment 1: model -----------------------------------------------------
|
|
257
323
|
|
|
258
324
|
|
|
@@ -407,7 +473,10 @@ def resolve_context_pct(
|
|
|
407
473
|
this function only returns the plain `🧠 X%` form. ``render_statusline``
|
|
408
474
|
wraps the result in ANSI color codes per ``args.color``.
|
|
409
475
|
"""
|
|
410
|
-
pct = inj.context_pct(
|
|
476
|
+
pct = inj.context_pct(
|
|
477
|
+
inp.transcript_path, inp.model_id,
|
|
478
|
+
inp.context_window_size, inp.context_tokens,
|
|
479
|
+
)
|
|
411
480
|
if pct is None:
|
|
412
481
|
return "🧠 N/A"
|
|
413
482
|
return f"🧠 {int(round(pct))}%"
|