cctally 1.103.0 → 1.105.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 +94 -0
- package/README.md +6 -6
- package/bin/_cctally_alerts.py +104 -10
- package/bin/_cctally_cache.py +193 -71
- package/bin/_cctally_config.py +62 -2
- package/bin/_cctally_core.py +62 -1
- package/bin/_cctally_dashboard.py +694 -70
- package/bin/_cctally_dashboard_conversation.py +400 -6
- package/bin/_cctally_dashboard_envelope.py +336 -14
- package/bin/_cctally_dashboard_perf.py +93 -0
- package/bin/_cctally_dashboard_share.py +56 -25
- package/bin/_cctally_dashboard_sources.py +988 -94
- package/bin/_cctally_db.py +30 -3
- package/bin/_cctally_diagnosis_sources.py +699 -186
- package/bin/_cctally_doctor.py +77 -0
- package/bin/_cctally_forecast.py +932 -51
- package/bin/_cctally_journal.py +430 -24
- package/bin/_cctally_parser.py +66 -0
- package/bin/_cctally_project.py +535 -10
- package/bin/_cctally_quota.py +28 -0
- package/bin/_cctally_quota_calibration.py +146 -0
- package/bin/_cctally_quota_model.py +2142 -0
- package/bin/_cctally_record.py +204 -30
- package/bin/_cctally_share.py +16 -8
- package/bin/_cctally_statusline.py +34 -0
- package/bin/_cctally_tui.py +531 -115
- package/bin/_lib_codex_conversation_query.py +258 -12
- package/bin/_lib_codex_hooks.py +26 -0
- package/bin/_lib_conversation_query.py +145 -15
- package/bin/_lib_dashboard_json.py +105 -0
- package/bin/_lib_dashboard_settings_contract.py +2 -0
- package/bin/_lib_diagnosis.py +21 -2
- package/bin/_lib_doctor.py +215 -1
- package/bin/_lib_forecast.py +337 -43
- package/bin/_lib_ingest_frontier.py +889 -0
- package/bin/_lib_meter_rate_change.py +360 -0
- package/bin/_lib_perf.py +22 -0
- package/bin/_lib_pricing.py +30 -3
- package/bin/_lib_quota_calibration.py +311 -0
- package/bin/_lib_quota_copy.py +157 -0
- package/bin/_lib_quota_model.py +2520 -0
- package/bin/_lib_rate_change_delivery.py +119 -0
- package/bin/_lib_record.py +50 -0
- package/bin/_lib_rederive.py +10 -0
- package/bin/_lib_render.py +6 -0
- package/bin/_lib_retained_size.py +187 -0
- package/bin/_lib_share_templates.py +37 -5
- package/bin/_lib_snapshot_cache.py +504 -17
- package/bin/_lib_statusline.py +200 -2
- package/bin/_lib_tick_stats.py +28 -4
- package/bin/_lib_view_models.py +30 -12
- package/bin/cctally +32 -0
- package/dashboard/static/assets/ConversationsView-DA4j7Yso.js +72 -0
- package/dashboard/static/assets/DoctorModal-D4PbVnbE.js +1 -0
- package/dashboard/static/assets/ModalRoot-D4oSPxPp.js +1 -0
- package/dashboard/static/assets/ProjectsDrillPanel-DkPlB9JH.js +1 -0
- package/dashboard/static/assets/SourceDetailModal-BE7FONsS.js +1 -0
- package/dashboard/static/assets/UpdateModal-BJuI8glf.js +7 -0
- package/dashboard/static/assets/index-klO46NcU.css +1 -0
- package/dashboard/static/assets/index-w_bINkJ8.js +13 -0
- package/dashboard/static/assets/outlineNavigation-zDVm6Hdd.js +9 -0
- package/dashboard/static/assets/useKeymap-CJ-Pi17D.js +1 -0
- package/dashboard/static/dashboard.html +3 -2
- package/package.json +10 -1
- package/dashboard/static/assets/index-Di2hljvB.css +0 -1
- package/dashboard/static/assets/index-XYCIWjVG.js +0 -97
package/bin/_lib_statusline.py
CHANGED
|
@@ -10,12 +10,62 @@ for the full design.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
|
+
import pathlib
|
|
13
14
|
import re
|
|
15
|
+
import sys
|
|
14
16
|
from dataclasses import dataclass, field
|
|
15
17
|
from datetime import datetime, timezone
|
|
16
18
|
from typing import Callable, Optional
|
|
17
19
|
|
|
18
20
|
|
|
21
|
+
def _load_lib(name: str):
|
|
22
|
+
"""Late-import a sibling under ``bin/`` (the ``_lib_view_models`` recipe).
|
|
23
|
+
|
|
24
|
+
Late rather than top-level, so this KERNEL carries no import-time edge to
|
|
25
|
+
the quota modules and stays importable on its own.
|
|
26
|
+
|
|
27
|
+
WHAT THAT DOES NOT BUY, corrected after the #661 S2 review. An earlier
|
|
28
|
+
revision of this docstring claimed that a status line on an install which
|
|
29
|
+
has never fitted a calibration "loads none of them". At runtime it loads
|
|
30
|
+
all of them. ``_seven_day_projection`` imports ``_lib_forecast``
|
|
31
|
+
unconditionally, and ``_lib_forecast`` imports ``_lib_quota_model`` at
|
|
32
|
+
module scope, so the quota kernel arrives with the projection whether or
|
|
33
|
+
not a calibration exists. ``_rate_change_marker`` then calls
|
|
34
|
+
``inj.quota_regimes()`` whenever any 5h or 7d reading exists, and that
|
|
35
|
+
port imports ``_cctally_quota_model``; a withheld figure imports
|
|
36
|
+
``_lib_quota_copy``.
|
|
37
|
+
|
|
38
|
+
MEASURED here, as marginal cost in ONE process in the order the status
|
|
39
|
+
line pays it, three runs on an Apple M4 Max under CPython 3.13:
|
|
40
|
+
``_cctally_core`` 13.1-13.4ms, then ``_lib_forecast`` 12.1-13.5ms,
|
|
41
|
+
``_lib_quota_model`` 0.0ms because ``_lib_forecast`` already pulled it,
|
|
42
|
+
``_cctally_quota_model`` 1.5-1.8ms, and ``_lib_quota_copy`` 0.1-0.3ms.
|
|
43
|
+
So about 14ms per prompt beyond ``_cctally_core``. Those durations are
|
|
44
|
+
this machine's; the STRUCTURAL fact — four modules, of which the largest
|
|
45
|
+
is on the unconditional projection path — is not.
|
|
46
|
+
|
|
47
|
+
That is an unstated cost rather than a violated constraint. Spec §9's
|
|
48
|
+
rule is about ``cache.db`` and ``analyse``, and neither is touched; see
|
|
49
|
+
``tests/test_statusline_quota.py``, which makes both explode rather than
|
|
50
|
+
reading the source for their names. Gating the quota port behind a cheap
|
|
51
|
+
``Path.exists()`` on the calibration file was measured and NOT taken: it
|
|
52
|
+
would remove ``_cctally_quota_model`` alone, 1.5ms of the 14, because
|
|
53
|
+
``_lib_forecast`` and the kernel it pulls are already loaded by then, and
|
|
54
|
+
it would add a stat(2) to every prompt on an install that does have a
|
|
55
|
+
calibration.
|
|
56
|
+
"""
|
|
57
|
+
cached = sys.modules.get(name)
|
|
58
|
+
if cached is not None:
|
|
59
|
+
return cached
|
|
60
|
+
import importlib.util as _ilu
|
|
61
|
+
p = pathlib.Path(__file__).resolve().parent / f"{name}.py"
|
|
62
|
+
spec = _ilu.spec_from_file_location(name, p)
|
|
63
|
+
mod = _ilu.module_from_spec(spec)
|
|
64
|
+
sys.modules[name] = mod
|
|
65
|
+
spec.loader.exec_module(mod)
|
|
66
|
+
return mod
|
|
67
|
+
|
|
68
|
+
|
|
19
69
|
# ---- Stdin payload (subset we care about) ----------------------------------
|
|
20
70
|
|
|
21
71
|
|
|
@@ -97,6 +147,18 @@ class StatuslineInjections:
|
|
|
97
147
|
context_pct: Callable[[Optional[str], Optional[str]], Optional[float]]
|
|
98
148
|
# Emits one-shot stderr warnings (deduped by message — caller maintains set).
|
|
99
149
|
warn_once: Callable[[str], None]
|
|
150
|
+
# #661 S2 §9. The stored metering regimes for this install, oldest
|
|
151
|
+
# first, as plain dicts — a dedicated FILESYSTEM port and deliberately
|
|
152
|
+
# NOT the DB port beside it. The read takes no flock and no throttle:
|
|
153
|
+
# `save_calibrations` writes through `os.replace` in the same directory,
|
|
154
|
+
# so a lock-free reader always sees a complete old or new inode, and a
|
|
155
|
+
# flock here would let a writer stall a prompt. It returns `()` for
|
|
156
|
+
# every unusable state — absent, malformed, version-ahead, quarantined
|
|
157
|
+
# — because the status line cannot distinguish them without scanning
|
|
158
|
+
# quarantine sidecars and renders the same thing for all of them: no
|
|
159
|
+
# marker. The default supplies no regimes, so a caller that has not
|
|
160
|
+
# wired the port renders exactly what it rendered before.
|
|
161
|
+
quota_regimes: Callable[[], tuple] = staticmethod(lambda: ())
|
|
100
162
|
|
|
101
163
|
|
|
102
164
|
# ---- ParseError sentinel ---------------------------------------------------
|
|
@@ -369,20 +431,147 @@ def _fmt_countdown(seconds: int) -> str:
|
|
|
369
431
|
return f"{minutes}m"
|
|
370
432
|
|
|
371
433
|
|
|
434
|
+
#: The 7d meter's window length. The projection measures a rate over the
|
|
435
|
+
#: elapsed part of THIS window, so it needs the window's span, and the reset
|
|
436
|
+
#: epoch alone gives only its end.
|
|
437
|
+
_SEVEN_DAY_HOURS = 7 * 24.0
|
|
438
|
+
|
|
439
|
+
#: The marker §6.6's predicate raises. Short because it shares a line with
|
|
440
|
+
#: four other segments, and literal because a bare glyph would have to be
|
|
441
|
+
#: looked up: the word says the rate changed and `cctally quota` says how.
|
|
442
|
+
_RATE_CHANGE_MARKER = "Δrate"
|
|
443
|
+
|
|
444
|
+
#: How much of the window must have elapsed before a pace projection is
|
|
445
|
+
#: rendered here. NOT a new threshold: it is
|
|
446
|
+
#: `ForecastConfidenceCause.ELAPSED_HOURS`, the estate's existing rule for
|
|
447
|
+
#: when a week-pace projection is low-confidence, restated as a number
|
|
448
|
+
#: because the status line has no room for the `LOW CONF` qualifier
|
|
449
|
+
#: `forecast` prints beside one. Without it the slot publishes arithmetic
|
|
450
|
+
#: that is correct and useless: a 42% reading ten hours into a week
|
|
451
|
+
#: projects to 697%, and a per-prompt line is the worst place to put a
|
|
452
|
+
#: figure that moves by hundreds of points between prompts.
|
|
453
|
+
_PROJECTION_MIN_ELAPSED_HOURS = 24.0
|
|
454
|
+
|
|
455
|
+
@dataclass(frozen=True)
|
|
456
|
+
class _ProjectionInputs:
|
|
457
|
+
"""The duck-typed operand `select_projection_basis` reads.
|
|
458
|
+
|
|
459
|
+
The selector is shared with `forecast` and the dashboard and takes
|
|
460
|
+
whatever object carries these attributes, so the status line builds the
|
|
461
|
+
smallest one that answers it rather than reaching for a forecast.
|
|
462
|
+
|
|
463
|
+
`calibrated_projection_pct` is deliberately absent. Producing it needs a
|
|
464
|
+
whole-week `session_entries` scan, and spec §9 forbids this surface from
|
|
465
|
+
touching `cache.db` for quota at all, so the basis on this surface is
|
|
466
|
+
always the corrected meter. The selector is still asked rather than
|
|
467
|
+
bypassed, so the token the line prints names the basis it actually used
|
|
468
|
+
and would name a different one the day the basis becomes cheap.
|
|
469
|
+
"""
|
|
470
|
+
p_now: "float | None"
|
|
471
|
+
p_now_corrected: "float | None"
|
|
472
|
+
right_censored: bool
|
|
473
|
+
elapsed_hours: "float | None"
|
|
474
|
+
remaining_hours: "float | None"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _seven_day_projection(seven_pct, seven_resets, now_epoch) -> Optional[str]:
|
|
478
|
+
"""The `→ …` token for the 7d slot, or None when there is none.
|
|
479
|
+
|
|
480
|
+
None — rather than a withheld token — in the three cases where there is
|
|
481
|
+
nothing to say rather than a typed reason not to say it: the reset epoch
|
|
482
|
+
is unknown, so there is no window to project over; the reset instant has
|
|
483
|
+
already passed, so the window it named has closed; or too little of the
|
|
484
|
+
window has elapsed for a pace to mean anything. Otherwise the slot
|
|
485
|
+
carries a value with its basis, or the withholding cause in the short
|
|
486
|
+
register.
|
|
487
|
+
"""
|
|
488
|
+
if seven_pct is None or seven_resets is None:
|
|
489
|
+
return None
|
|
490
|
+
if seven_resets <= now_epoch:
|
|
491
|
+
# A CLOSED window. The clamp below would drive `remaining` to 0 and
|
|
492
|
+
# `elapsed` to the whole 168 hours, so the projection would equal the
|
|
493
|
+
# corrected reading and the line would print a projected end-of-week
|
|
494
|
+
# percent for a week that has already ended. It is reachable from the
|
|
495
|
+
# DB-latest fallback row and from a long-idle machine, and it is a
|
|
496
|
+
# missing window rather than a statement about the reading — so it is
|
|
497
|
+
# withheld the way an unknown reset is, above the censoring branch
|
|
498
|
+
# and above the elapsed gate.
|
|
499
|
+
return None
|
|
500
|
+
fc = _load_lib("_lib_forecast")
|
|
501
|
+
remaining = (seven_resets - now_epoch) / 3600.0
|
|
502
|
+
remaining = max(0.0, min(_SEVEN_DAY_HOURS, remaining))
|
|
503
|
+
elapsed = _SEVEN_DAY_HOURS - remaining
|
|
504
|
+
shown = float(seven_pct)
|
|
505
|
+
corrected = fc.corrected_percent_point(shown)
|
|
506
|
+
selected = fc.select_projection_basis(_ProjectionInputs(
|
|
507
|
+
p_now=shown,
|
|
508
|
+
p_now_corrected=corrected,
|
|
509
|
+
# Censoring is read off the correction rather than re-tested here.
|
|
510
|
+
# `corrected_percent_point` returns None exactly when the reading is
|
|
511
|
+
# right-censored — a displayed 100 denotes `[99, +inf)` and has no
|
|
512
|
+
# finite point estimate — and a second spelling of that test would
|
|
513
|
+
# be a second place to disagree with it.
|
|
514
|
+
right_censored=corrected is None,
|
|
515
|
+
elapsed_hours=elapsed,
|
|
516
|
+
remaining_hours=remaining,
|
|
517
|
+
))
|
|
518
|
+
copy = _load_lib("_lib_quota_copy")
|
|
519
|
+
if selected.basis is fc.ProjectionBasis.WITHHELD:
|
|
520
|
+
# A withholding is a statement about the READING and does not
|
|
521
|
+
# depend on how much of the window has elapsed, so it is rendered
|
|
522
|
+
# before the confidence gate below. A right-censored 100 is the
|
|
523
|
+
# case: it has no point estimate at any point in the week.
|
|
524
|
+
return f"→ {copy.short_form(selected.code)}"
|
|
525
|
+
if elapsed < _PROJECTION_MIN_ELAPSED_HOURS:
|
|
526
|
+
# Too early in the window for a pace to mean anything. The slot
|
|
527
|
+
# keeps the shape it has always had rather than printing a token,
|
|
528
|
+
# for the same reason an unknown reset epoch does: there is nothing
|
|
529
|
+
# to say, and a per-prompt line must not fill with non-statements.
|
|
530
|
+
return None
|
|
531
|
+
basis = copy.basis_presentation(selected.basis.value).get(
|
|
532
|
+
"short", selected.basis.value)
|
|
533
|
+
return f"→ {int(round(selected.value))}% {basis}"
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _rate_change_marker(inj: StatuslineInjections) -> Optional[str]:
|
|
537
|
+
"""`Δrate` while §6.6's predicate holds, else None.
|
|
538
|
+
|
|
539
|
+
The predicate is DERIVED — the active open regime has a confirmed
|
|
540
|
+
predecessor — so the marker clears on its own when that regime closes
|
|
541
|
+
and needs no durable state, which §1 forbids this session from adding.
|
|
542
|
+
"""
|
|
543
|
+
try:
|
|
544
|
+
regimes = list(inj.quota_regimes() or ())
|
|
545
|
+
except Exception: # noqa: BLE001
|
|
546
|
+
# A port that raises must not take the prompt down with it. The
|
|
547
|
+
# marker is information; the status line is the shell's.
|
|
548
|
+
return None
|
|
549
|
+
if not regimes:
|
|
550
|
+
return None
|
|
551
|
+
mrc = _load_lib("_lib_meter_rate_change")
|
|
552
|
+
return _RATE_CHANGE_MARKER if mrc.active_rate_change(regimes) else None
|
|
553
|
+
|
|
554
|
+
|
|
372
555
|
def resolve_cctally_extensions(
|
|
373
556
|
inp: StatuslineInput,
|
|
374
557
|
now: datetime,
|
|
375
558
|
inj: StatuslineInjections,
|
|
376
559
|
*,
|
|
377
560
|
include_countdowns: bool = True,
|
|
561
|
+
include_projection: bool = True,
|
|
378
562
|
) -> Optional[str]:
|
|
379
|
-
"""Segment 5 — cctally-only `5h X% (...) · 7d Y% (...)`.
|
|
563
|
+
"""Segment 5 — cctally-only `5h X% (...) · 7d Y% (...) → Z% meter`.
|
|
380
564
|
|
|
381
565
|
Source priority chain (spec §3.5):
|
|
382
566
|
1. stdin rate_limits (preferred — freshest)
|
|
383
567
|
2. DB latest weekly_usage_snapshots row (if stdin EMPTY)
|
|
384
568
|
3. HWM monotonic clamp (within window only)
|
|
385
569
|
4. If all empty → return None (segment 5 suppressed)
|
|
570
|
+
|
|
571
|
+
#661 S2 §9: the 7d slot also carries the projected end-of-week percent
|
|
572
|
+
with the basis it came from, and a `Δrate` marker while §6.6's predicate
|
|
573
|
+
holds. `include_projection` is False for `--usage-only`, which is the
|
|
574
|
+
compact two-reading form other tools embed and keeps its shape.
|
|
386
575
|
"""
|
|
387
576
|
five_pct = inp.rate_limits_5h_pct
|
|
388
577
|
five_resets = inp.rate_limits_5h_resets_at
|
|
@@ -421,7 +610,16 @@ def resolve_cctally_extensions(
|
|
|
421
610
|
s = f"7d {int(round(seven_pct))}%"
|
|
422
611
|
if include_countdowns and seven_resets is not None:
|
|
423
612
|
s += f" ({_fmt_countdown(seven_resets - now_epoch)})"
|
|
613
|
+
if include_projection:
|
|
614
|
+
projection = _seven_day_projection(
|
|
615
|
+
seven_pct, seven_resets, now_epoch)
|
|
616
|
+
if projection is not None:
|
|
617
|
+
s += f" {projection}"
|
|
424
618
|
parts.append(s)
|
|
619
|
+
if include_projection:
|
|
620
|
+
marker = _rate_change_marker(inj)
|
|
621
|
+
if marker is not None:
|
|
622
|
+
parts.append(marker)
|
|
425
623
|
return " · ".join(parts)
|
|
426
624
|
|
|
427
625
|
|
|
@@ -471,7 +669,7 @@ def render_statusline(
|
|
|
471
669
|
"""
|
|
472
670
|
if args.usage_only:
|
|
473
671
|
ext = resolve_cctally_extensions(
|
|
474
|
-
inp, now, inj, include_countdowns=False
|
|
672
|
+
inp, now, inj, include_countdowns=False, include_projection=False
|
|
475
673
|
)
|
|
476
674
|
return "" if ext is None else _colorize_usage_segment(ext, args)
|
|
477
675
|
|
package/bin/_lib_tick_stats.py
CHANGED
|
@@ -57,6 +57,7 @@ PUBLICATIONS = ("final", "partial", "seed", "degraded")
|
|
|
57
57
|
#: validation lives in the recorder rather than in the dataclass because a
|
|
58
58
|
#: frozen field typed `str` enforces nothing on its own.
|
|
59
59
|
CONVERSATION_STATUSES = ("ok", "store_unavailable", "error")
|
|
60
|
+
CONVERSATION_MODES = ("caught_up", "targeted", "full", "not_observed")
|
|
60
61
|
|
|
61
62
|
_INGEST = "ingest"
|
|
62
63
|
_BUILD = "build"
|
|
@@ -82,6 +83,7 @@ class TickRecord:
|
|
|
82
83
|
published_at: str
|
|
83
84
|
period_ns: "int | None"
|
|
84
85
|
cache_pin_ns: int
|
|
86
|
+
cpu_ns: int
|
|
85
87
|
|
|
86
88
|
def as_wire(self) -> dict:
|
|
87
89
|
return {f.name: getattr(self, f.name) for f in dataclasses.fields(self)}
|
|
@@ -119,6 +121,10 @@ class ConversationSyncRecord:
|
|
|
119
121
|
cpu_ns: int
|
|
120
122
|
period_ns: "int | None"
|
|
121
123
|
status: str
|
|
124
|
+
claude_mode: str
|
|
125
|
+
codex_mode: str
|
|
126
|
+
claude_files: int
|
|
127
|
+
codex_files: int
|
|
122
128
|
|
|
123
129
|
def as_wire(self) -> dict:
|
|
124
130
|
return {f.name: getattr(self, f.name) for f in dataclasses.fields(self)}
|
|
@@ -233,14 +239,17 @@ class _Span:
|
|
|
233
239
|
class TickContext:
|
|
234
240
|
"""A tick in progress. Thread-confined; only ``finish`` touches the lock."""
|
|
235
241
|
|
|
236
|
-
__slots__ = ("_now", "
|
|
242
|
+
__slots__ = ("_now", "_cpu_now", "_standalone", "_started_ns",
|
|
243
|
+
"_started_cpu_ns", "_spans", "_finished",
|
|
237
244
|
"_ingest_ns", "_builder_ns", "_ingest_ran", "_dispatch",
|
|
238
245
|
"_codex_regime", "_publication", "_cold", "_cache_pin_ns")
|
|
239
246
|
|
|
240
|
-
def __init__(self, *, monotonic_ns, standalone: bool):
|
|
247
|
+
def __init__(self, *, monotonic_ns, thread_time_ns, standalone: bool):
|
|
241
248
|
self._now = monotonic_ns
|
|
249
|
+
self._cpu_now = thread_time_ns
|
|
242
250
|
self._standalone = standalone
|
|
243
251
|
self._started_ns = monotonic_ns()
|
|
252
|
+
self._started_cpu_ns = thread_time_ns()
|
|
244
253
|
self._spans: list[_Span] = []
|
|
245
254
|
self._finished = False
|
|
246
255
|
self._ingest_ns = 0
|
|
@@ -388,6 +397,7 @@ class TickContext:
|
|
|
388
397
|
while self._spans:
|
|
389
398
|
self._spans.pop().__exit__(None, None, None)
|
|
390
399
|
ended_ns = self._now()
|
|
400
|
+
ended_cpu_ns = self._cpu_now()
|
|
391
401
|
if getattr(_tls, "tick", None) is self:
|
|
392
402
|
_tls.tick = None
|
|
393
403
|
|
|
@@ -419,6 +429,7 @@ class TickContext:
|
|
|
419
429
|
published_at=published_at,
|
|
420
430
|
period_ns=period,
|
|
421
431
|
cache_pin_ns=self._cache_pin_ns,
|
|
432
|
+
cpu_ns=max(0, ended_cpu_ns - self._started_cpu_ns),
|
|
422
433
|
)
|
|
423
434
|
if self._standalone:
|
|
424
435
|
_STATE = dataclasses.replace(prior, standalone=record)
|
|
@@ -442,7 +453,9 @@ class TickContext:
|
|
|
442
453
|
|
|
443
454
|
|
|
444
455
|
def record_conversation_pass(
|
|
445
|
-
*, seq, started_ns, ended_ns, duration_ns, cpu_ns, status
|
|
456
|
+
*, seq, started_ns, ended_ns, duration_ns, cpu_ns, status,
|
|
457
|
+
claude_mode="not_observed", codex_mode="not_observed",
|
|
458
|
+
claude_files=0, codex_files=0,
|
|
446
459
|
) -> None:
|
|
447
460
|
"""Append one conversation sync pass to its ring, under the SHARED lock.
|
|
448
461
|
|
|
@@ -469,6 +482,10 @@ def record_conversation_pass(
|
|
|
469
482
|
"""
|
|
470
483
|
global _STATE
|
|
471
484
|
safe_status = status if status in CONVERSATION_STATUSES else "error"
|
|
485
|
+
safe_claude_mode = (
|
|
486
|
+
claude_mode if claude_mode in CONVERSATION_MODES else "not_observed")
|
|
487
|
+
safe_codex_mode = (
|
|
488
|
+
codex_mode if codex_mode in CONVERSATION_MODES else "not_observed")
|
|
472
489
|
start = int(started_ns)
|
|
473
490
|
record = ConversationSyncRecord(
|
|
474
491
|
seq=int(seq),
|
|
@@ -478,6 +495,10 @@ def record_conversation_pass(
|
|
|
478
495
|
cpu_ns=max(0, int(cpu_ns)),
|
|
479
496
|
period_ns=None,
|
|
480
497
|
status=safe_status,
|
|
498
|
+
claude_mode=safe_claude_mode,
|
|
499
|
+
codex_mode=safe_codex_mode,
|
|
500
|
+
claude_files=max(0, int(claude_files)),
|
|
501
|
+
codex_files=max(0, int(codex_files)),
|
|
481
502
|
)
|
|
482
503
|
with _LOCK:
|
|
483
504
|
prior = _STATE
|
|
@@ -495,7 +516,9 @@ def record_conversation_pass(
|
|
|
495
516
|
)
|
|
496
517
|
|
|
497
518
|
|
|
498
|
-
def begin_tick(
|
|
519
|
+
def begin_tick(
|
|
520
|
+
*, monotonic_ns=None, thread_time_ns=None, standalone: bool = False,
|
|
521
|
+
) -> TickContext:
|
|
499
522
|
"""Open a tick context and make it ``current()`` on this thread.
|
|
500
523
|
|
|
501
524
|
``standalone`` records a ``tui --render-once`` or
|
|
@@ -505,6 +528,7 @@ def begin_tick(*, monotonic_ns=None, standalone: bool = False) -> TickContext:
|
|
|
505
528
|
"""
|
|
506
529
|
ctx = TickContext(
|
|
507
530
|
monotonic_ns=monotonic_ns or time.monotonic_ns,
|
|
531
|
+
thread_time_ns=thread_time_ns or time.thread_time_ns,
|
|
508
532
|
standalone=standalone,
|
|
509
533
|
)
|
|
510
534
|
_tls.tick = ctx
|
package/bin/_lib_view_models.py
CHANGED
|
@@ -1515,7 +1515,12 @@ def _forecast_verdict_of(output) -> str:
|
|
|
1515
1515
|
inputs = getattr(output, "inputs", None)
|
|
1516
1516
|
if inputs is not None and getattr(inputs, "confidence", "high") == "low":
|
|
1517
1517
|
return _FORECAST_VERDICT_LOW_CONF
|
|
1518
|
-
high =
|
|
1518
|
+
high = getattr(output, "final_percent_high", 0.0)
|
|
1519
|
+
if high is None:
|
|
1520
|
+
# #661 S2 spec section 3.2: a right-censored reading publishes no
|
|
1521
|
+
# projection, and the meter is at its cap by construction.
|
|
1522
|
+
return _FORECAST_VERDICT_OVER
|
|
1523
|
+
high = float(high)
|
|
1519
1524
|
if high >= 100:
|
|
1520
1525
|
return _FORECAST_VERDICT_OVER
|
|
1521
1526
|
if high >= 90:
|
|
@@ -1538,25 +1543,38 @@ def _forecast_dashboard_verdict_of(output) -> str:
|
|
|
1538
1543
|
def _forecast_projection_pcts(output) -> "tuple[float | None, float | None]":
|
|
1539
1544
|
"""Return (week_avg_projection_pct, recent_24h_projection_pct).
|
|
1540
1545
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1546
|
+
The week-average value is READ from ``ForecastOutput`` rather than
|
|
1547
|
+
re-derived (#661 S2 spec section 3.3). It is the one selector's answer,
|
|
1548
|
+
so it is the calibrated model's number where that basis was reached and
|
|
1549
|
+
``None`` where the kernel withheld it. Re-deriving
|
|
1550
|
+
``p_now + r_avg * remaining`` here published a second answer to the same
|
|
1551
|
+
question: the committed ``over`` dashboard golden carried
|
|
1552
|
+
``week_avg_projection_pct: 126.0`` in the envelope while its own nested
|
|
1553
|
+
``forecast.explain`` block said the projection was withheld because the
|
|
1554
|
+
meter was right-censored.
|
|
1555
|
+
|
|
1556
|
+
The recent-24h value is the SECOND method and stays derived, because the
|
|
1557
|
+
kernel publishes only one selected projection. It is ``None`` when
|
|
1558
|
+
``r_recent`` is ``None`` — which includes every censored reading — or
|
|
1559
|
+
when its projection equals the week-average one (no new info; a second
|
|
1560
|
+
method that agrees with the first contributes nothing to the range).
|
|
1547
1561
|
"""
|
|
1548
1562
|
if output is None:
|
|
1549
1563
|
return None, None
|
|
1550
1564
|
inputs = getattr(output, "inputs", None)
|
|
1551
1565
|
if inputs is None:
|
|
1552
1566
|
return None, None
|
|
1553
|
-
|
|
1567
|
+
from _lib_forecast import projection_base
|
|
1568
|
+
|
|
1569
|
+
week_avg_pct = getattr(output, "week_avg_projection_pct", None)
|
|
1570
|
+
if getattr(output, "projection_basis", None) == "withheld":
|
|
1571
|
+
# The kernel withheld the projection itself, so there is no second
|
|
1572
|
+
# method to offer either: the recent-24h rate over a censored or
|
|
1573
|
+
# empty meter would publish the very number the withholding removed.
|
|
1574
|
+
return None, None
|
|
1575
|
+
p_now = projection_base(inputs)
|
|
1554
1576
|
rem = getattr(inputs, "remaining_hours", None)
|
|
1555
|
-
r_avg = getattr(output, "r_avg", None)
|
|
1556
1577
|
r_recent = getattr(output, "r_recent", None)
|
|
1557
|
-
week_avg_pct = None
|
|
1558
|
-
if p_now is not None and rem is not None and r_avg is not None:
|
|
1559
|
-
week_avg_pct = p_now + r_avg * rem
|
|
1560
1578
|
recent_pct = None
|
|
1561
1579
|
if p_now is not None and rem is not None and r_recent is not None:
|
|
1562
1580
|
candidate = p_now + r_recent * rem
|
package/bin/cctally
CHANGED
|
@@ -568,6 +568,29 @@ _iter_codex_jsonl_entries_with_offsets = _lib_jsonl._iter_codex_jsonl_entries_wi
|
|
|
568
568
|
# Provider-neutral quota interpretation kernel (#294 S2). The module is pure
|
|
569
569
|
# and contains no database, config, filesystem, notifier, parser, or command
|
|
570
570
|
# dependency; adapters added in later stages own those concerns.
|
|
571
|
+
# Quota model kernel (#661 S1): the pure-fn layer behind `cctally quota`,
|
|
572
|
+
# re-exported here like the other _lib_* kernels so tests and the glue reach
|
|
573
|
+
# one loaded instance.
|
|
574
|
+
_lib_quota_model = _load_sibling("_lib_quota_model")
|
|
575
|
+
CalibrationStatus = _lib_quota_model.CalibrationStatus
|
|
576
|
+
WithholdingCause = _lib_quota_model.WithholdingCause
|
|
577
|
+
Verdict = _lib_quota_model.Verdict
|
|
578
|
+
BlockingReason = _lib_quota_model.BlockingReason
|
|
579
|
+
CalibrationEvidence = _lib_quota_model.CalibrationEvidence
|
|
580
|
+
QuotaAnalysis = _lib_quota_model.QuotaAnalysis
|
|
581
|
+
DailyObservation = _lib_quota_model.DailyObservation
|
|
582
|
+
DetectorResult = _lib_quota_model.DetectorResult
|
|
583
|
+
EntryRecord = _lib_quota_model.EntryRecord
|
|
584
|
+
SnapshotRecord = _lib_quota_model.SnapshotRecord
|
|
585
|
+
CreditRecord = _lib_quota_model.CreditRecord
|
|
586
|
+
Segment = _lib_quota_model.Segment
|
|
587
|
+
Interval = _lib_quota_model.Interval
|
|
588
|
+
Support = _lib_quota_model.Support
|
|
589
|
+
quota_model_analyse = _lib_quota_model.analyse
|
|
590
|
+
QUOTA_MODEL_ALGORITHM_REVISION = _lib_quota_model.QUOTA_MODEL_ALGORITHM_REVISION
|
|
591
|
+
QUOTA_MODEL_CONSTANTS_FINGERPRINT = (
|
|
592
|
+
_lib_quota_model.QUOTA_MODEL_CONSTANTS_FINGERPRINT)
|
|
593
|
+
|
|
571
594
|
_lib_quota = _load_sibling("_lib_quota")
|
|
572
595
|
QuotaWindowIdentity = _lib_quota.QuotaWindowIdentity
|
|
573
596
|
QuotaObservation = _lib_quota.QuotaObservation
|
|
@@ -889,6 +912,12 @@ cmd_pricing_check = _cctally_pricing_check.cmd_pricing_check
|
|
|
889
912
|
_pricing_observed_models = _cctally_pricing_check._pricing_observed_models
|
|
890
913
|
_PRICING_SCAN_DEFAULT_WINDOW = _cctally_pricing_check._PRICING_SCAN_DEFAULT_WINDOW
|
|
891
914
|
|
|
915
|
+
# Eager re-export of bin/_cctally_quota_model.py — `cmd_quota` is invoked
|
|
916
|
+
# via the parser's `set_defaults(func=c.cmd_quota)`, which resolves through
|
|
917
|
+
# cctally's namespace, so it must live here.
|
|
918
|
+
_cctally_quota_model = _load_sibling("_cctally_quota_model")
|
|
919
|
+
cmd_quota = _cctally_quota_model.cmd_quota
|
|
920
|
+
|
|
892
921
|
# Eager re-export of bin/_cctally_doctor.py — `cmd_doctor` is the parser's
|
|
893
922
|
# `set_defaults(func=c.cmd_doctor)` target; `doctor_gather_state` is a
|
|
894
923
|
# PATCHABLE binding reached by the dashboard shim
|
|
@@ -1477,6 +1506,7 @@ _project_detail_for_window = _cctally_dashboard._project_detail_for_window
|
|
|
1477
1506
|
_handle_get_project_detail_impl = _cctally_dashboard._handle_get_project_detail_impl
|
|
1478
1507
|
_select_current_block_for_envelope = _cctally_dashboard._select_current_block_for_envelope
|
|
1479
1508
|
_build_alerts_envelope_array = _cctally_dashboard._build_alerts_envelope_array
|
|
1509
|
+
_build_meter_rate_change_array = _cctally_dashboard._build_meter_rate_change_array
|
|
1480
1510
|
snapshot_to_envelope = _cctally_dashboard.snapshot_to_envelope
|
|
1481
1511
|
_session_detail_to_envelope = _cctally_dashboard._session_detail_to_envelope
|
|
1482
1512
|
DashboardHTTPHandler = _cctally_dashboard.DashboardHTTPHandler
|
|
@@ -1654,6 +1684,8 @@ _assess_forecast_confidence = _cctally_forecast._assess_forecast_confidence
|
|
|
1654
1684
|
_pick_p_24h_ago = _cctally_forecast._pick_p_24h_ago
|
|
1655
1685
|
_load_forecast_inputs = _cctally_forecast._load_forecast_inputs
|
|
1656
1686
|
_compute_forecast = _cctally_forecast._compute_forecast
|
|
1687
|
+
corrected_percent_point = _cctally_forecast.corrected_percent_point
|
|
1688
|
+
corrected_percent_interval = _cctally_forecast.corrected_percent_interval
|
|
1657
1689
|
_parse_forecast_targets = _cctally_forecast._parse_forecast_targets
|
|
1658
1690
|
TOOL_VERSION = _cctally_forecast.TOOL_VERSION
|
|
1659
1691
|
_build_forecast_json_payload = _cctally_forecast._build_forecast_json_payload
|