cctally 1.65.0 → 1.67.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 +63 -0
- package/README.md +6 -4
- package/bin/_cctally_alerts.py +1 -1
- package/bin/_cctally_cache.py +203 -22
- package/bin/_cctally_cache_report.py +7 -5
- package/bin/_cctally_core.py +125 -14
- package/bin/_cctally_dashboard.py +495 -4893
- package/bin/_cctally_dashboard_cache_report.py +714 -0
- package/bin/_cctally_dashboard_conversation.py +830 -0
- package/bin/_cctally_dashboard_envelope.py +1520 -0
- package/bin/_cctally_dashboard_share.py +2012 -0
- package/bin/_cctally_db.py +158 -5
- package/bin/_cctally_doctor.py +104 -3
- package/bin/_cctally_five_hour.py +19 -3
- package/bin/_cctally_forecast.py +118 -161
- package/bin/_cctally_parser.py +1003 -777
- package/bin/_cctally_percent_breakdown.py +1 -1
- package/bin/_cctally_pricing_check.py +39 -0
- package/bin/_cctally_project.py +18 -42
- package/bin/_cctally_record.py +279 -274
- package/bin/_cctally_reporting.py +1 -1
- package/bin/_cctally_setup.py +150 -43
- package/bin/_cctally_sync_week.py +1 -1
- package/bin/_cctally_telemetry.py +3 -5
- package/bin/_cctally_transcript.py +191 -0
- package/bin/_cctally_tui.py +42 -18
- package/bin/_lib_alert_dispatch.py +1 -1
- package/bin/_lib_blocks.py +6 -1
- package/bin/_lib_conversation_anon.py +254 -0
- package/bin/_lib_conversation_query.py +66 -0
- package/bin/_lib_credit.py +133 -0
- package/bin/_lib_diff_kernel.py +19 -7
- package/bin/_lib_doctor.py +150 -1
- package/bin/_lib_five_hour.py +61 -0
- package/bin/_lib_forecast.py +144 -0
- package/bin/_lib_json_envelope.py +38 -0
- package/bin/_lib_jsonl.py +115 -73
- package/bin/_lib_log.py +96 -0
- package/bin/_lib_pricing.py +47 -1
- package/bin/_lib_pricing_check.py +25 -3
- package/bin/_lib_record.py +179 -0
- package/bin/_lib_render.py +26 -11
- package/bin/_lib_snapshot_cache.py +61 -0
- package/bin/_lib_view_models.py +7 -1
- package/bin/cctally +59 -7
- package/bin/cctally-dashboard +2 -2
- package/bin/cctally-statusline +1 -0
- package/bin/cctally-transcript +5 -0
- package/bin/cctally-tui +2 -2
- package/dashboard/static/assets/index-BybNp_Di.js +80 -0
- package/dashboard/static/assets/{index-DQWNrIqu.css → index-DgAmLK65.css} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +13 -1
- package/dashboard/static/assets/index-CJTUCpKt.js +0 -80
package/bin/_cctally_record.py
CHANGED
|
@@ -111,8 +111,30 @@ What stays in bin/cctally:
|
|
|
111
111
|
``_RESET_PCT_DROP_THRESHOLD`` — boundary helpers, already-extracted
|
|
112
112
|
subsystems, or constants reached through the cctally namespace
|
|
113
113
|
(``_RESET_PCT_DROP_THRESHOLD`` now lives in ``bin/_cctally_weekrefs.py``,
|
|
114
|
-
re-exported on the cctally ns).
|
|
115
|
-
|
|
114
|
+
re-exported on the cctally ns). Accessed via the shim/``c.X`` pattern
|
|
115
|
+
EXCEPT the names honest-imported by the #279 S4 F5 collapse below.
|
|
116
|
+
|
|
117
|
+
#279 S4 F5 (the #50 treatment): the forwarding shims for
|
|
118
|
+
``open_cache_db`` (→ ``_cctally_cache``), ``_floor_to_hour`` (→
|
|
119
|
+
``_lib_blocks``), ``_resolve_display_tz_obj`` (→ ``_lib_display_tz``),
|
|
120
|
+
``_build_alert_payload_{weekly,five_hour,budget,project_budget,
|
|
121
|
+
codex_budget,projected}`` (→ ``_lib_alerts_payload``), and
|
|
122
|
+
``_get_oauth_usage_config`` (→ ``_cctally_refresh``) were replaced by
|
|
123
|
+
honest top-level imports — each real def lives in a sibling
|
|
124
|
+
bin/cctally eager-loads BEFORE _cctally_record, and none is
|
|
125
|
+
monkeypatched through the cctally namespace / this module's route.
|
|
126
|
+
The remaining 38 shims below STAY: patched surfaces (``load_config``,
|
|
127
|
+
``sync_cache``, ``_dispatch_alert_notification``, ``compute_budget_status``,
|
|
128
|
+
``_apply_reset_events_to_weekrefs``, ``resolve_display_tz``,
|
|
129
|
+
``_sum_cost_by_project``, ``_project_budget_labels``,
|
|
130
|
+
``get_claude_session_entries``, ``_compute_cost_for_weekref``,
|
|
131
|
+
``_get_canonical_boundary_for_date``, ``_hook_tick_oauth_refresh``),
|
|
132
|
+
bin/cctally-homed residues (``_resolve_primary_model_for_block``,
|
|
133
|
+
``_warn_alerts_bad_config_once``, ``_warn_budget_bad_config_once``,
|
|
134
|
+
``_hook_tick_make_mock_refresh``, ``cmd_sync_week``), and names whose
|
|
135
|
+
real home (``_cctally_milestones`` / ``_cctally_forecast`` /
|
|
136
|
+
``_cctally_weekrefs``) bin/cctally eager-loads AFTER _cctally_record —
|
|
137
|
+
honest-importing those at module top would force an early sibling load.
|
|
116
138
|
|
|
117
139
|
§5.6 audit on this extraction's monkeypatch surface:
|
|
118
140
|
- ``cmd_record_usage`` — patched via ``monkeypatch.setitem(ns, …)``
|
|
@@ -177,18 +199,99 @@ from _cctally_core import (
|
|
|
177
199
|
_BudgetConfigError,
|
|
178
200
|
_command_as_of,
|
|
179
201
|
)
|
|
180
|
-
from _lib_five_hour import _canonical_5h_window_key
|
|
202
|
+
from _lib_five_hour import _canonical_5h_window_key, five_hour_milestone_range
|
|
181
203
|
from _lib_pricing import _calculate_entry_cost
|
|
182
204
|
|
|
183
205
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
#
|
|
206
|
+
import importlib.util as _ilu
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _ensure_sibling_loaded(name: str) -> None:
|
|
210
|
+
"""Register a NON-eager-loaded ``_lib_*`` sibling in ``sys.modules``.
|
|
211
|
+
|
|
212
|
+
Every ``_lib_*`` this module imports at body-time is eager-loaded by
|
|
213
|
+
``bin/cctally`` EXCEPT the #279 S4 kernels (``_lib_credit``,
|
|
214
|
+
``_lib_record``) — those are consumer-only and were deliberately kept
|
|
215
|
+
out of ``bin/cctally``'s eager-load block so ``bin/cctally`` stays
|
|
216
|
+
byte-untouched (spec §2 re-export continuity). Under the
|
|
217
|
+
``SourceFileLoader`` harness path (``bin/`` absent from ``sys.path``) a
|
|
218
|
+
bare ``from _lib_X import`` would then miss, so this pre-registers the
|
|
219
|
+
sibling ``__file__``-relative first — mirroring ``_cctally_cache.
|
|
220
|
+
_load_lib`` and ``_lib_conversation_query``'s ``_lib_perf`` fallback.
|
|
221
|
+
The honest ``from _lib_X import`` that follows is a ``sys.modules`` hit
|
|
222
|
+
in every load context (prod script, conftest, harness).
|
|
223
|
+
"""
|
|
224
|
+
if name in sys.modules:
|
|
225
|
+
return
|
|
226
|
+
try:
|
|
227
|
+
__import__(name) # bin/ on sys.path: prod script / conftest / pytest
|
|
228
|
+
return
|
|
229
|
+
except ModuleNotFoundError:
|
|
230
|
+
pass
|
|
231
|
+
_p = os.path.join(os.path.dirname(__file__), f"{name}.py")
|
|
232
|
+
_spec = _ilu.spec_from_file_location(name, _p)
|
|
233
|
+
_mod = _ilu.module_from_spec(_spec)
|
|
234
|
+
sys.modules[name] = _mod
|
|
235
|
+
_spec.loader.exec_module(_mod)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
_ensure_sibling_loaded("_lib_credit")
|
|
239
|
+
from _lib_credit import (
|
|
240
|
+
_PERCENT_NORMALIZE_DECIMALS,
|
|
241
|
+
_normalize_percent,
|
|
242
|
+
CreditPlan,
|
|
243
|
+
_parse_credit_at,
|
|
244
|
+
_build_credit_plan,
|
|
245
|
+
)
|
|
246
|
+
_ensure_sibling_loaded("_lib_record")
|
|
247
|
+
from _lib_record import (
|
|
248
|
+
check_resets_at_plausibility,
|
|
249
|
+
plan_weekly_credit_debounce,
|
|
250
|
+
plan_five_hour_credit,
|
|
251
|
+
hwm_clamp_applies,
|
|
252
|
+
milestone_coverage_owes,
|
|
253
|
+
hwm_file_next,
|
|
254
|
+
projected_crossings,
|
|
255
|
+
FIRE_IMMEDIATE,
|
|
256
|
+
CONFIRM_RESET,
|
|
257
|
+
CLEAR_MARKER,
|
|
258
|
+
ARM_MARKER,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# === #279 S4 F5: forwarding-shim wall collapse (the #50 treatment) =========
|
|
262
|
+
# Names whose real def lives in a sibling that bin/cctally eager-loads BEFORE
|
|
263
|
+
# _cctally_record (so these are sys.modules hits in every load context) AND
|
|
264
|
+
# that no test monkeypatches through the cctally namespace / _cctally_record
|
|
265
|
+
# route are honest-imported here instead of routed through a
|
|
266
|
+
# ``sys.modules["cctally"].X`` shim. The audit (full table in the commit body)
|
|
267
|
+
# used the suite as the authority; every name below survived the full
|
|
268
|
+
# bin/cctally-test-all. Patched / bin/cctally-homed / post-889-homed names
|
|
269
|
+
# keep their shims below.
|
|
270
|
+
from _cctally_cache import open_cache_db
|
|
271
|
+
from _lib_blocks import _floor_to_hour
|
|
272
|
+
from _lib_display_tz import _resolve_display_tz_obj
|
|
273
|
+
from _lib_alerts_payload import (
|
|
274
|
+
_build_alert_payload_weekly,
|
|
275
|
+
_build_alert_payload_five_hour,
|
|
276
|
+
_build_alert_payload_budget,
|
|
277
|
+
_build_alert_payload_project_budget,
|
|
278
|
+
_build_alert_payload_codex_budget,
|
|
279
|
+
_build_alert_payload_projected,
|
|
280
|
+
)
|
|
281
|
+
from _cctally_refresh import _get_oauth_usage_config
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# Module-level back-ref shims (the REMAINING wall after the #279 S4 F5
|
|
285
|
+
# collapse above pulled 10 unpatched, pre-889-homed names up to honest
|
|
286
|
+
# imports). Each shim below resolves ``sys.modules['cctally'].X`` at CALL
|
|
287
|
+
# TIME (not bind time), so monkeypatches on cctally's namespace propagate
|
|
288
|
+
# into the moved code unchanged. `load_config` and
|
|
289
|
+
# `get_claude_session_entries` STAY as shims even though their natural
|
|
290
|
+
# homes are decentralized (_cctally_config / _cctally_cache) — tests
|
|
291
|
+
# monkeypatch them via `ns["X"]`; direct imports would silently bypass the
|
|
292
|
+
# patches. The rest stay because they are patched, bin/cctally-homed, or
|
|
293
|
+
# their real home is eager-loaded AFTER this module (see the F5 note in the
|
|
294
|
+
# module docstring for the full STAY/COLLAPSE audit).
|
|
192
295
|
# See spec §3.5 (carve-out) and §3.7 (stays-on-shim allowlist).
|
|
193
296
|
def load_config(*args, **kwargs):
|
|
194
297
|
return sys.modules["cctally"].load_config(*args, **kwargs)
|
|
@@ -198,18 +301,10 @@ def get_claude_session_entries(*args, **kwargs):
|
|
|
198
301
|
return sys.modules["cctally"].get_claude_session_entries(*args, **kwargs)
|
|
199
302
|
|
|
200
303
|
|
|
201
|
-
def open_cache_db(*args, **kwargs):
|
|
202
|
-
return sys.modules["cctally"].open_cache_db(*args, **kwargs)
|
|
203
|
-
|
|
204
|
-
|
|
205
304
|
def sync_cache(*args, **kwargs):
|
|
206
305
|
return sys.modules["cctally"].sync_cache(*args, **kwargs)
|
|
207
306
|
|
|
208
307
|
|
|
209
|
-
def _floor_to_hour(*args, **kwargs):
|
|
210
|
-
return sys.modules["cctally"]._floor_to_hour(*args, **kwargs)
|
|
211
|
-
|
|
212
|
-
|
|
213
308
|
def _get_canonical_boundary_for_date(*args, **kwargs):
|
|
214
309
|
return sys.modules["cctally"]._get_canonical_boundary_for_date(*args, **kwargs)
|
|
215
310
|
|
|
@@ -250,30 +345,6 @@ def _resolve_primary_model_for_block(*args, **kwargs):
|
|
|
250
345
|
return sys.modules["cctally"]._resolve_primary_model_for_block(*args, **kwargs)
|
|
251
346
|
|
|
252
347
|
|
|
253
|
-
def _resolve_display_tz_obj(*args, **kwargs):
|
|
254
|
-
return sys.modules["cctally"]._resolve_display_tz_obj(*args, **kwargs)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def _build_alert_payload_weekly(*args, **kwargs):
|
|
258
|
-
return sys.modules["cctally"]._build_alert_payload_weekly(*args, **kwargs)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
def _build_alert_payload_five_hour(*args, **kwargs):
|
|
262
|
-
return sys.modules["cctally"]._build_alert_payload_five_hour(*args, **kwargs)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def _build_alert_payload_budget(*args, **kwargs):
|
|
266
|
-
return sys.modules["cctally"]._build_alert_payload_budget(*args, **kwargs)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def _build_alert_payload_project_budget(*args, **kwargs):
|
|
270
|
-
return sys.modules["cctally"]._build_alert_payload_project_budget(*args, **kwargs)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def _build_alert_payload_codex_budget(*args, **kwargs):
|
|
274
|
-
return sys.modules["cctally"]._build_alert_payload_codex_budget(*args, **kwargs)
|
|
275
|
-
|
|
276
|
-
|
|
277
348
|
def _budget_crossings(*args, **kwargs):
|
|
278
349
|
return sys.modules["cctally"]._budget_crossings(*args, **kwargs)
|
|
279
350
|
|
|
@@ -334,10 +405,6 @@ def _projected_levels_already_latched(*args, **kwargs):
|
|
|
334
405
|
return sys.modules["cctally"]._projected_levels_already_latched(*args, **kwargs)
|
|
335
406
|
|
|
336
407
|
|
|
337
|
-
def _build_alert_payload_projected(*args, **kwargs):
|
|
338
|
-
return sys.modules["cctally"]._build_alert_payload_projected(*args, **kwargs)
|
|
339
|
-
|
|
340
|
-
|
|
341
408
|
def _fetch_current_week_snapshots(*args, **kwargs):
|
|
342
409
|
return sys.modules["cctally"]._fetch_current_week_snapshots(*args, **kwargs)
|
|
343
410
|
|
|
@@ -370,10 +437,6 @@ def _warn_budget_bad_config_once(*args, **kwargs):
|
|
|
370
437
|
return sys.modules["cctally"]._warn_budget_bad_config_once(*args, **kwargs)
|
|
371
438
|
|
|
372
439
|
|
|
373
|
-
def _get_oauth_usage_config(*args, **kwargs):
|
|
374
|
-
return sys.modules["cctally"]._get_oauth_usage_config(*args, **kwargs)
|
|
375
|
-
|
|
376
|
-
|
|
377
440
|
def _hook_tick_oauth_refresh(*args, **kwargs):
|
|
378
441
|
"""Shim for ``_hook_tick_oauth_refresh``.
|
|
379
442
|
|
|
@@ -400,10 +463,10 @@ def _hook_tick_make_mock_refresh(*args, **kwargs):
|
|
|
400
463
|
# ``cmd_hook_tick`` for the three rewrites.
|
|
401
464
|
|
|
402
465
|
|
|
403
|
-
#
|
|
404
|
-
#
|
|
405
|
-
#
|
|
406
|
-
|
|
466
|
+
# ``_PERCENT_NORMALIZE_DECIMALS`` + ``_normalize_percent`` now live in
|
|
467
|
+
# ``bin/_lib_credit.py`` (#279 S4 F1); re-imported at module top so
|
|
468
|
+
# ``bin/cctally``'s ``_cctally_record._PERCENT_NORMALIZE_DECIMALS`` /
|
|
469
|
+
# ``_normalize_percent`` re-exports keep resolving unchanged.
|
|
407
470
|
|
|
408
471
|
# Plausibility band for --resets-at / --five-hour-resets-at (issue #112).
|
|
409
472
|
# Out-of-band epochs are guarded at cmd_record_usage ingress before any
|
|
@@ -459,27 +522,8 @@ _logged_window_key_coerce_failure = False
|
|
|
459
522
|
# c._RESET_PCT_DROP_THRESHOLD — bin/_cctally_weekrefs.py constant (re-exported on cctally ns)
|
|
460
523
|
# c._is_reset_drop — bin/_cctally_weekrefs.py helper (re-exported on cctally ns)
|
|
461
524
|
# c.HOOK_TICK_DEFAULT_THROTTLE_SECONDS
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
def _normalize_percent(value: "float | int | None") -> "float | None":
|
|
465
|
-
"""Flush IEEE 754 ULP noise out of an ingress percent value.
|
|
466
|
-
|
|
467
|
-
Single chokepoint applied at every site where a raw percent enters
|
|
468
|
-
cctally's runtime path (OAuth fetch, hook-tick OAuth refresh, and
|
|
469
|
-
the cmd_record_usage CLI ingress). Downstream consumers — HWM
|
|
470
|
-
files, ``weekly_usage_snapshots.{weekly,five_hour}_percent`` REAL
|
|
471
|
-
columns, ``five_hour_blocks.final_five_hour_percent``, milestone
|
|
472
|
-
crossing values, and the SSE envelope's ``used_percent`` field —
|
|
473
|
-
all read the cleaned value, so a single round here stops
|
|
474
|
-
``5h=7.000000000000001`` style strings from reaching any log or
|
|
475
|
-
serialized surface.
|
|
476
|
-
|
|
477
|
-
``None`` is the canonical absent-percent sentinel; preserve it
|
|
478
|
-
unchanged so the optional-5h branches stay simple.
|
|
479
|
-
"""
|
|
480
|
-
if value is None:
|
|
481
|
-
return None
|
|
482
|
-
return round(float(value), _PERCENT_NORMALIZE_DECIMALS)
|
|
525
|
+
# (#279 S4 F5 collapsed only function-forwarding shims to honest imports;
|
|
526
|
+
# these call-time constant/helper accessors are untouched.)
|
|
483
527
|
|
|
484
528
|
|
|
485
529
|
def _resolve_active_five_hour_reset_event_id(
|
|
@@ -1316,16 +1360,18 @@ def maybe_record_projected_alert(
|
|
|
1316
1360
|
proj = _weekly_pct_week_avg_projection(conn, now_utc)
|
|
1317
1361
|
if proj is not None and not proj[1]:
|
|
1318
1362
|
value = proj[0]
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1363
|
+
# weekly_pct comparand == raw threshold (denominator 100).
|
|
1364
|
+
for t in projected_crossings(
|
|
1365
|
+
value, [(t, float(t)) for t in levels]
|
|
1366
|
+
):
|
|
1367
|
+
pending.append(dict(
|
|
1368
|
+
week_start_at=week_key,
|
|
1369
|
+
period="subscription-week",
|
|
1370
|
+
metric="weekly_pct",
|
|
1371
|
+
threshold=t,
|
|
1372
|
+
projected_value=value,
|
|
1373
|
+
denominator=100.0,
|
|
1374
|
+
))
|
|
1329
1375
|
|
|
1330
1376
|
# ── budget_usd leg (any Claude period — #135; shared factory) ────────
|
|
1331
1377
|
if budget_on:
|
|
@@ -1359,16 +1405,19 @@ def maybe_record_projected_alert(
|
|
|
1359
1405
|
status = compute_budget_status(inputs)
|
|
1360
1406
|
if not status.low_confidence:
|
|
1361
1407
|
value = status.week_avg_projection_usd
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1408
|
+
# budget comparand == (t/100)*target (glue pre-scales).
|
|
1409
|
+
for t in projected_crossings(
|
|
1410
|
+
value,
|
|
1411
|
+
[(t, (t / 100.0) * float(target)) for t in thresholds],
|
|
1412
|
+
):
|
|
1413
|
+
pending.append(dict(
|
|
1414
|
+
week_start_at=b_week_key,
|
|
1415
|
+
period=claude_period,
|
|
1416
|
+
metric="budget_usd",
|
|
1417
|
+
threshold=t,
|
|
1418
|
+
projected_value=value,
|
|
1419
|
+
denominator=float(target),
|
|
1420
|
+
))
|
|
1372
1421
|
|
|
1373
1422
|
# ── codex_budget_usd leg (#135; skip_sync=False — R5) ────────────────
|
|
1374
1423
|
if codex_on:
|
|
@@ -1405,16 +1454,19 @@ def maybe_record_projected_alert(
|
|
|
1405
1454
|
c_status = compute_budget_status(c_inputs)
|
|
1406
1455
|
if not c_status.low_confidence:
|
|
1407
1456
|
value = c_status.week_avg_projection_usd
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1457
|
+
# codex comparand == (t/100)*target (glue pre-scales).
|
|
1458
|
+
for t in projected_crossings(
|
|
1459
|
+
value,
|
|
1460
|
+
[(t, (t / 100.0) * float(c_target)) for t in c_thresholds],
|
|
1461
|
+
):
|
|
1462
|
+
pending.append(dict(
|
|
1463
|
+
week_start_at=c_week_key,
|
|
1464
|
+
period=c_period,
|
|
1465
|
+
metric="codex_budget_usd",
|
|
1466
|
+
threshold=t,
|
|
1467
|
+
projected_value=value,
|
|
1468
|
+
denominator=float(c_target),
|
|
1469
|
+
))
|
|
1418
1470
|
|
|
1419
1471
|
# ── arm (set-then-dispatch): INSERT + stamp alerted_at in one txn ────
|
|
1420
1472
|
fired: list[dict[str, Any]] = []
|
|
@@ -1865,12 +1917,17 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
1865
1917
|
).fetchone()
|
|
1866
1918
|
max_existing = row["m"] if row and row["m"] is not None else None
|
|
1867
1919
|
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1920
|
+
# Which integer 5h-% thresholds to attempt: the pure fencing
|
|
1921
|
+
# decision (floor snap + first-obs / resume-above-max rule).
|
|
1922
|
+
# `milestone_range.start` is the start_threshold used for the
|
|
1923
|
+
# marginal-cost check below; a non-empty range is exactly the
|
|
1924
|
+
# old `start_threshold <= current_floor` guard.
|
|
1925
|
+
milestone_range = five_hour_milestone_range(
|
|
1926
|
+
float(five_hour_percent), max_existing
|
|
1927
|
+
)
|
|
1928
|
+
start_threshold = milestone_range.start
|
|
1872
1929
|
|
|
1873
|
-
if
|
|
1930
|
+
if milestone_range:
|
|
1874
1931
|
# block_id was resolved above (before the children writes) and
|
|
1875
1932
|
# is still in scope here.
|
|
1876
1933
|
|
|
@@ -1893,7 +1950,7 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
1893
1950
|
if prev_row is not None:
|
|
1894
1951
|
prior_cost = float(prev_row["block_cost_usd"])
|
|
1895
1952
|
|
|
1896
|
-
for pct in
|
|
1953
|
+
for pct in milestone_range:
|
|
1897
1954
|
if pct == start_threshold and prior_cost is not None:
|
|
1898
1955
|
marginal: float | None = totals["cost_usd"] - prior_cost
|
|
1899
1956
|
else:
|
|
@@ -2175,85 +2232,10 @@ def _read_reset_zero_marker():
|
|
|
2175
2232
|
return (week_start_date, cur_end_canon, baseline_pct, first_zero_iso)
|
|
2176
2233
|
|
|
2177
2234
|
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
week_end_at: str
|
|
2183
|
-
cur_end_canon: str
|
|
2184
|
-
from_pct: float
|
|
2185
|
-
from_source: str # "hwm" | "explicit" | "prior_credit"
|
|
2186
|
-
to_pct: float
|
|
2187
|
-
effective_iso: str # weekly_credit_floors.effective_at_utc (floored to hour)
|
|
2188
|
-
captured_iso: str # synthetic snapshot captured_at_utc (un-floored), 'Z'
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
def _parse_credit_at(value, now):
|
|
2192
|
-
"""Parse --at as an aware UTC datetime; default to `now`. Naive => UTC
|
|
2193
|
-
(mirrors bin/_cctally_five_hour.py:88), NOT parse_iso_datetime (host-local)."""
|
|
2194
|
-
if value is None:
|
|
2195
|
-
return now
|
|
2196
|
-
try:
|
|
2197
|
-
parsed = dt.datetime.fromisoformat(value)
|
|
2198
|
-
except ValueError as e:
|
|
2199
|
-
raise ValueError(f"--at: {e}") from e
|
|
2200
|
-
if parsed.tzinfo is None:
|
|
2201
|
-
parsed = parsed.replace(tzinfo=dt.timezone.utc)
|
|
2202
|
-
return parsed.astimezone(dt.timezone.utc)
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
def _build_credit_plan(*, week_start_date, week_start_at, week_end_at,
|
|
2206
|
-
from_pct, from_source, to_pct, at_dt, now,
|
|
2207
|
-
effective_override=None):
|
|
2208
|
-
"""Validate inputs and build a CreditPlan. Pure (no DB/file I/O).
|
|
2209
|
-
Raises ValueError(msg) on any violation — caller maps to exit 2.
|
|
2210
|
-
|
|
2211
|
-
`effective_override` (an ISO string) is the completion-path reuse of an
|
|
2212
|
-
EXISTING `weekly_credit_floors.effective_at_utc` (spec §4a): when present,
|
|
2213
|
-
the plan's effective is that value rather than `floor_to_hour(at)`, so a
|
|
2214
|
-
rerun of a half-applied credit at a later wall-clock keeps the original
|
|
2215
|
-
floor moment and never leaks a stale pre-credit replay into the floored
|
|
2216
|
-
MAX. The synthetic snapshot's captured timestamp stays the un-floored
|
|
2217
|
-
`at` either way."""
|
|
2218
|
-
to_pct = _normalize_percent(to_pct)
|
|
2219
|
-
from_pct = _normalize_percent(from_pct)
|
|
2220
|
-
# Defensive None-guard (issue #212 N3). `_normalize_percent` returns None
|
|
2221
|
-
# for a None input. The CLI never reaches here with None (`--to` is
|
|
2222
|
-
# required + type=float; `--from` always resolves to a float or the caller
|
|
2223
|
-
# errors out first), but this is a public pure helper called directly by
|
|
2224
|
-
# tests and any future non-CLI caller — surface a None as a clear ValueError
|
|
2225
|
-
# (caller -> exit 2) instead of a TypeError from the `0.0 <= None` compare
|
|
2226
|
-
# immediately below.
|
|
2227
|
-
if to_pct is None or from_pct is None:
|
|
2228
|
-
raise ValueError("--to/--from must be numeric")
|
|
2229
|
-
if not (0.0 <= to_pct <= 100.0) or not (0.0 <= from_pct <= 100.0):
|
|
2230
|
-
raise ValueError("--to/--from must be in [0, 100]")
|
|
2231
|
-
if to_pct >= from_pct:
|
|
2232
|
-
raise ValueError(f"not a credit: to >= from ({to_pct} >= {from_pct})")
|
|
2233
|
-
ws = parse_iso_datetime(week_start_at, "week_start_at")
|
|
2234
|
-
we = parse_iso_datetime(week_end_at, "week_end_at")
|
|
2235
|
-
if not (ws <= at_dt < we):
|
|
2236
|
-
raise ValueError(f"--at {at_dt.isoformat()} is outside the week window")
|
|
2237
|
-
if at_dt > now:
|
|
2238
|
-
raise ValueError("--at is in the future")
|
|
2239
|
-
if effective_override is not None:
|
|
2240
|
-
effective_iso = parse_iso_datetime(
|
|
2241
|
-
effective_override, "effective_override"
|
|
2242
|
-
).astimezone(dt.timezone.utc).isoformat(timespec="seconds")
|
|
2243
|
-
else:
|
|
2244
|
-
effective_iso = _floor_to_hour(at_dt).isoformat(timespec="seconds")
|
|
2245
|
-
cur_end_canon = _canonicalize_optional_iso(week_end_at, "record-credit.week_end")
|
|
2246
|
-
return CreditPlan(
|
|
2247
|
-
week_start_date=week_start_date,
|
|
2248
|
-
week_start_at=week_start_at,
|
|
2249
|
-
week_end_at=week_end_at,
|
|
2250
|
-
cur_end_canon=cur_end_canon,
|
|
2251
|
-
from_pct=from_pct,
|
|
2252
|
-
from_source=from_source,
|
|
2253
|
-
to_pct=to_pct,
|
|
2254
|
-
effective_iso=effective_iso,
|
|
2255
|
-
captured_iso=at_dt.isoformat(timespec="seconds").replace("+00:00", "Z"),
|
|
2256
|
-
)
|
|
2235
|
+
# ``CreditPlan`` / ``_parse_credit_at`` / ``_build_credit_plan`` now live in
|
|
2236
|
+
# ``bin/_lib_credit.py`` (#279 S4 F1); re-imported at module top so the
|
|
2237
|
+
# ``bin/cctally`` re-exports and this module's own callers
|
|
2238
|
+
# (``cmd_record_credit``) resolve them unchanged.
|
|
2257
2239
|
|
|
2258
2240
|
|
|
2259
2241
|
def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
@@ -2734,6 +2716,12 @@ def cmd_record_credit(args) -> int:
|
|
|
2734
2716
|
f"{plan.from_pct:g}% -> {plan.to_pct:g}% "
|
|
2735
2717
|
f"(effective {plan.effective_iso})")
|
|
2736
2718
|
return 0
|
|
2719
|
+
except _cctally().StatsDbCorruptError:
|
|
2720
|
+
# #279 S1 F4: the global corrupt-DB contract (one-line diagnosis +
|
|
2721
|
+
# exit 2) wins over record-credit's documented exit-3 DB-error mapping.
|
|
2722
|
+
# StatsDbCorruptError subclasses sqlite3.DatabaseError, so without this
|
|
2723
|
+
# re-raise the handler below would swallow it and return 3.
|
|
2724
|
+
raise
|
|
2737
2725
|
except sqlite3.DatabaseError as e:
|
|
2738
2726
|
# Documented exit 3 (docs/commands/record-credit.md "3 — a database
|
|
2739
2727
|
# error"). The inner ValueError->2 / EOFError paths return before
|
|
@@ -2768,9 +2756,11 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
2768
2756
|
# of silently reporting success on a dropped payload.
|
|
2769
2757
|
now_dt = _command_as_of()
|
|
2770
2758
|
now_epoch = int(now_dt.timestamp())
|
|
2771
|
-
if not (
|
|
2772
|
-
|
|
2773
|
-
|
|
2759
|
+
if not check_resets_at_plausibility(
|
|
2760
|
+
resets_at, now_epoch,
|
|
2761
|
+
past_slack_s=_RECORD_USAGE_WEEK_PAST_SLACK_S,
|
|
2762
|
+
future_band_s=_RECORD_USAGE_WEEK_FUTURE_BAND_S,
|
|
2763
|
+
):
|
|
2774
2764
|
eprint(
|
|
2775
2765
|
f"[record-usage] rejecting --resets-at={resets_at}: outside "
|
|
2776
2766
|
f"plausibility band [now-30d, now+8d]; "
|
|
@@ -2799,9 +2789,11 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
2799
2789
|
# _compute_block_totals charges entries past the real
|
|
2800
2790
|
# reset to this block). Dropping the 5h portion here
|
|
2801
2791
|
# skips maybe_update_five_hour_block entirely.
|
|
2802
|
-
if not (
|
|
2803
|
-
|
|
2804
|
-
|
|
2792
|
+
if not check_resets_at_plausibility(
|
|
2793
|
+
five_hour_resets_at_epoch, now_epoch,
|
|
2794
|
+
past_slack_s=_RECORD_USAGE_5H_PAST_SLACK_S,
|
|
2795
|
+
future_band_s=_RECORD_USAGE_5H_FUTURE_BAND_S,
|
|
2796
|
+
):
|
|
2805
2797
|
eprint(
|
|
2806
2798
|
f"[record-usage] dropping --five-hour-resets-at="
|
|
2807
2799
|
f"{five_hour_resets_at_epoch}: outside plausibility band "
|
|
@@ -2980,14 +2972,28 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
2980
2972
|
# reachable. See the spec for the midpoint rationale.
|
|
2981
2973
|
prior_end_dt = parse_iso_datetime(prior_end_canon, "prior.week_end_at")
|
|
2982
2974
|
if prior_end_dt > now_utc and prior_pct is not None:
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2975
|
+
# Read the pending reset-to-zero marker up front (pure
|
|
2976
|
+
# file read) and compute whether it is armed for THIS
|
|
2977
|
+
# window; the debounce CLASSIFIER (pure) decides the
|
|
2978
|
+
# action from those values + the c._RESET_* constants,
|
|
2979
|
+
# then the glue below executes the decided I/O. The 5
|
|
2980
|
+
# branch outcomes (fire-immediate / confirm / clear /
|
|
2981
|
+
# arm / none) map 1:1 to the pre-extraction structure.
|
|
2982
|
+
marker = _read_reset_zero_marker()
|
|
2983
|
+
armed = (
|
|
2984
|
+
marker is not None
|
|
2985
|
+
and marker[0] == week_start_date
|
|
2986
|
+
and marker[1] == cur_end_canon
|
|
2989
2987
|
)
|
|
2990
|
-
|
|
2988
|
+
decision = plan_weekly_credit_debounce(
|
|
2989
|
+
prior_pct, weekly_percent,
|
|
2990
|
+
drop_threshold=c._RESET_PCT_DROP_THRESHOLD,
|
|
2991
|
+
zero_floor_pct=c._RESET_ZERO_FLOOR_PCT,
|
|
2992
|
+
zero_min_drop_pct=c._RESET_ZERO_MIN_DROP_PCT,
|
|
2993
|
+
marker_armed=armed,
|
|
2994
|
+
marker_baseline=(marker[2] if armed else None),
|
|
2995
|
+
).action
|
|
2996
|
+
if decision == FIRE_IMMEDIATE:
|
|
2991
2997
|
# >=25pp goodwill credit — fire immediately, never
|
|
2992
2998
|
# debounced. Clear any pending arm (now moot).
|
|
2993
2999
|
_clear_reset_zero_marker()
|
|
@@ -2996,53 +3002,42 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
2996
3002
|
observed_pre_credit_pct=float(prior_pct),
|
|
2997
3003
|
effective_dt=_floor_to_hour(now_utc),
|
|
2998
3004
|
)
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
+
elif decision == CONFIRM_RESET:
|
|
3006
|
+
# Second reading stayed low → confirm. Anchor the
|
|
3007
|
+
# reset at the FIRST-zero instant from the marker
|
|
3008
|
+
# (UTC-normalized like the backfill in-place path).
|
|
3009
|
+
first_zero_dt = parse_iso_datetime(
|
|
3010
|
+
marker[3], "reset_zero_marker.first_zero"
|
|
3011
|
+
).astimezone(dt.timezone.utc)
|
|
3012
|
+
_fire_in_place_credit(
|
|
3013
|
+
conn, week_start_date, cur_end_canon,
|
|
3014
|
+
weekly_percent,
|
|
3015
|
+
observed_pre_credit_pct=marker[2],
|
|
3016
|
+
effective_dt=_floor_to_hour(first_zero_dt),
|
|
3005
3017
|
)
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
# not a reset. Clear, do not fire.
|
|
3030
|
-
_clear_reset_zero_marker()
|
|
3031
|
-
elif zero_only:
|
|
3032
|
-
# First ~0 → arm; do NOT fire. The write clamp
|
|
3033
|
-
# suppresses this 0 (no event row yet), so the
|
|
3034
|
-
# prior snapshot stays at the baseline and this
|
|
3035
|
-
# shape re-evaluates next tick. first_zero_iso is
|
|
3036
|
-
# the _command_as_of() value (now_utc), NOT
|
|
3037
|
-
# wall-clock — it becomes the effective anchor.
|
|
3038
|
-
_arm_reset_zero_marker(
|
|
3039
|
-
week_start_date, cur_end_canon,
|
|
3040
|
-
baseline_pct=float(prior_pct),
|
|
3041
|
-
first_zero_iso=now_utc.isoformat(timespec="seconds"),
|
|
3042
|
-
)
|
|
3043
|
-
# else: not a reset shape and not armed → nothing.
|
|
3044
|
-
# A non-matching stale marker is inert (ignored
|
|
3045
|
-
# on key mismatch, overwritten by the next arm).
|
|
3018
|
+
# Clear ONLY after the fire completes (P2a): a
|
|
3019
|
+
# mid-fire crash leaves the marker armed so the next
|
|
3020
|
+
# zero re-confirms + re-runs the idempotent pivots.
|
|
3021
|
+
_clear_reset_zero_marker()
|
|
3022
|
+
elif decision == CLEAR_MARKER:
|
|
3023
|
+
# Recovered toward baseline → transient zero, not a
|
|
3024
|
+
# reset. Clear, do not fire.
|
|
3025
|
+
_clear_reset_zero_marker()
|
|
3026
|
+
elif decision == ARM_MARKER:
|
|
3027
|
+
# First ~0 → arm; do NOT fire. The write clamp
|
|
3028
|
+
# suppresses this 0 (no event row yet), so the prior
|
|
3029
|
+
# snapshot stays at the baseline and this shape
|
|
3030
|
+
# re-evaluates next tick. first_zero_iso is the
|
|
3031
|
+
# _command_as_of() value (now_utc), NOT wall-clock —
|
|
3032
|
+
# it becomes the effective anchor.
|
|
3033
|
+
_arm_reset_zero_marker(
|
|
3034
|
+
week_start_date, cur_end_canon,
|
|
3035
|
+
baseline_pct=float(prior_pct),
|
|
3036
|
+
first_zero_iso=now_utc.isoformat(timespec="seconds"),
|
|
3037
|
+
)
|
|
3038
|
+
# else NO_ACTION: not a reset shape and not armed →
|
|
3039
|
+
# nothing. A non-matching stale marker is inert
|
|
3040
|
+
# (ignored on key mismatch, overwritten by next arm).
|
|
3046
3041
|
|
|
3047
3042
|
# ── 5h in-place credit detection (parallel to weekly above) ──
|
|
3048
3043
|
# Spec §2.2 of
|
|
@@ -3098,10 +3093,10 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
3098
3093
|
# outer try block from
|
|
3099
3094
|
# ``dt.datetime.now(dt.timezone.utc)``; reuse it
|
|
3100
3095
|
# so both branches see the same instant.
|
|
3101
|
-
if (
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3096
|
+
if plan_five_hour_credit(
|
|
3097
|
+
prior_5h_pct, float(five_hour_percent),
|
|
3098
|
+
drop_threshold=c._FIVE_HOUR_RESET_PCT_DROP_THRESHOLD,
|
|
3099
|
+
prior_resets_in_future=(prior_5h_resets_dt > now_utc),
|
|
3105
3100
|
):
|
|
3106
3101
|
# Pair-check dedup pre-check (spec §2.2;
|
|
3107
3102
|
# refined by Codex r4 P1 finding). The
|
|
@@ -3305,7 +3300,7 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
3305
3300
|
""",
|
|
3306
3301
|
(week_start_date, clamp_floor_iso),
|
|
3307
3302
|
).fetchone()
|
|
3308
|
-
if
|
|
3303
|
+
if hwm_clamp_applies(weekly_percent, max_row["v"] if max_row else None):
|
|
3309
3304
|
should_insert = False
|
|
3310
3305
|
else:
|
|
3311
3306
|
# 5-hour usage is monotonically non-decreasing within a window
|
|
@@ -3350,11 +3345,8 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
3350
3345
|
""",
|
|
3351
3346
|
(int(five_hour_window_key), int(five_hour_window_key)),
|
|
3352
3347
|
).fetchone()
|
|
3353
|
-
if (
|
|
3354
|
-
max_5h_row
|
|
3355
|
-
and max_5h_row["v"] is not None
|
|
3356
|
-
and round(five_hour_percent, 1)
|
|
3357
|
-
< round(float(max_5h_row["v"]), 1)
|
|
3348
|
+
if hwm_clamp_applies(
|
|
3349
|
+
five_hour_percent, max_5h_row["v"] if max_5h_row else None
|
|
3358
3350
|
):
|
|
3359
3351
|
five_hour_percent = float(max_5h_row["v"])
|
|
3360
3352
|
|
|
@@ -3450,9 +3442,12 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
3450
3442
|
"WHERE week_start_date = ? AND reset_event_id = ?",
|
|
3451
3443
|
(week_start_date, heal_segment),
|
|
3452
3444
|
).fetchone()
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3445
|
+
existing_m = (
|
|
3446
|
+
int(max_existing["m"])
|
|
3447
|
+
if max_existing and max_existing["m"] is not None
|
|
3448
|
+
else None
|
|
3449
|
+
)
|
|
3450
|
+
if milestone_coverage_owes(existing_m, latest_floor):
|
|
3456
3451
|
need_milestone_heal = True
|
|
3457
3452
|
|
|
3458
3453
|
# Probe 2: do we owe a 5h-block update? Either no row
|
|
@@ -3515,14 +3510,14 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
3515
3510
|
" AND reset_event_id = ?",
|
|
3516
3511
|
(int(window_key), heal_5h_segment),
|
|
3517
3512
|
).fetchone()
|
|
3518
|
-
|
|
3519
|
-
max_5h_existing is None
|
|
3520
|
-
or max_5h_existing["m"] is None
|
|
3521
|
-
):
|
|
3522
|
-
need_5h_heal = True
|
|
3523
|
-
elif (
|
|
3513
|
+
existing_5h_m = (
|
|
3524
3514
|
int(max_5h_existing["m"])
|
|
3525
|
-
|
|
3515
|
+
if max_5h_existing
|
|
3516
|
+
and max_5h_existing["m"] is not None
|
|
3517
|
+
else None
|
|
3518
|
+
)
|
|
3519
|
+
if milestone_coverage_owes(
|
|
3520
|
+
existing_5h_m, latest_5h_floor
|
|
3526
3521
|
):
|
|
3527
3522
|
need_5h_heal = True
|
|
3528
3523
|
finally:
|
|
@@ -3651,7 +3646,7 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
3651
3646
|
existing_hwm = float(parts[1])
|
|
3652
3647
|
except (FileNotFoundError, ValueError, OSError):
|
|
3653
3648
|
pass
|
|
3654
|
-
if weekly_percent
|
|
3649
|
+
if hwm_file_next(existing_hwm, weekly_percent) is not None:
|
|
3655
3650
|
hwm_path.write_text(f"{week_start_date} {weekly_percent}\n")
|
|
3656
3651
|
except OSError:
|
|
3657
3652
|
pass
|
|
@@ -3674,7 +3669,7 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
3674
3669
|
existing_hwm5 = float(parts5[1])
|
|
3675
3670
|
except (FileNotFoundError, ValueError, OSError):
|
|
3676
3671
|
pass
|
|
3677
|
-
if five_hour_percent
|
|
3672
|
+
if hwm_file_next(existing_hwm5, five_hour_percent) is not None:
|
|
3678
3673
|
hwm5_path.write_text(f"{five_resets_key} {five_hour_percent}\n")
|
|
3679
3674
|
except OSError:
|
|
3680
3675
|
pass
|
|
@@ -3777,12 +3772,14 @@ def _hook_tick_session_short(sid: str) -> str:
|
|
|
3777
3772
|
|
|
3778
3773
|
|
|
3779
3774
|
def _hook_tick_format_log_line(
|
|
3780
|
-
event: str, session: str, ingested: int, oauth_status: str, dur_ms: int
|
|
3775
|
+
event: str, session: str, ingested: int, oauth_status: str, dur_ms: int,
|
|
3776
|
+
*, malformed: int = 0, skipped: int = 0,
|
|
3781
3777
|
) -> str:
|
|
3782
3778
|
ts = now_utc_iso()
|
|
3783
3779
|
return (
|
|
3784
3780
|
f"{ts} event={event:14s} session={session} "
|
|
3785
|
-
f"ingested={ingested}
|
|
3781
|
+
f"ingested={ingested} malformed={malformed} skipped={skipped} "
|
|
3782
|
+
f"oauth={oauth_status} dur_ms={dur_ms}"
|
|
3786
3783
|
)
|
|
3787
3784
|
|
|
3788
3785
|
|
|
@@ -3880,6 +3877,10 @@ def cmd_hook_tick(args: argparse.Namespace) -> int:
|
|
|
3880
3877
|
# --- Steps 3-7: wrap remainder in try/except (always exit 0 in normal mode) ---
|
|
3881
3878
|
start = time.monotonic()
|
|
3882
3879
|
ingested = 0
|
|
3880
|
+
# #279 S2 F1: parse-health counters from the local sync, surfaced on the
|
|
3881
|
+
# hook-tick log line (uniform fields — always emitted, defaulted to 0).
|
|
3882
|
+
parse_malformed = 0
|
|
3883
|
+
parse_skipped = 0
|
|
3883
3884
|
oauth_status = "skipped-no-oauth" if no_oauth else "throttled(age=?s)"
|
|
3884
3885
|
# Pre-fetch throttle state captured for --explain output. The OAuth
|
|
3885
3886
|
# block re-touches the throttle marker after a successful fetch, so
|
|
@@ -3898,6 +3899,8 @@ def cmd_hook_tick(args: argparse.Namespace) -> int:
|
|
|
3898
3899
|
try:
|
|
3899
3900
|
stats = sync_cache(cache_conn)
|
|
3900
3901
|
ingested = int(stats.rows_changed)
|
|
3902
|
+
parse_malformed = int(stats.lines_malformed)
|
|
3903
|
+
parse_skipped = int(stats.assistant_lines_skipped)
|
|
3901
3904
|
finally:
|
|
3902
3905
|
try:
|
|
3903
3906
|
cache_conn.close()
|
|
@@ -3958,6 +3961,8 @@ def cmd_hook_tick(args: argparse.Namespace) -> int:
|
|
|
3958
3961
|
ingested=ingested,
|
|
3959
3962
|
oauth_status=oauth_status,
|
|
3960
3963
|
dur_ms=dur_ms,
|
|
3964
|
+
malformed=parse_malformed,
|
|
3965
|
+
skipped=parse_skipped,
|
|
3961
3966
|
)
|
|
3962
3967
|
_hook_tick_log_line(line)
|
|
3963
3968
|
_hook_tick_log_rotate_if_needed()
|