cctally 1.109.0 → 1.111.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 +52 -0
- package/README.md +6 -6
- package/bin/_cctally_cache.py +62 -13
- package/bin/_cctally_core.py +8 -1
- package/bin/_cctally_dashboard.py +422 -195
- package/bin/_cctally_dashboard_conversation.py +231 -40
- package/bin/_cctally_dashboard_envelope.py +48 -32
- package/bin/_cctally_dashboard_sources.py +503 -109
- package/bin/_cctally_db.py +44 -0
- package/bin/_cctally_diagnosis_sources.py +181 -15
- package/bin/_cctally_doctor.py +15 -3
- package/bin/_cctally_forecast.py +25 -12
- package/bin/_cctally_journal.py +59 -32
- package/bin/_cctally_milestone_history.py +65 -9
- package/bin/_cctally_parser.py +5 -0
- package/bin/_cctally_record.py +50 -12
- package/bin/_cctally_rederive.py +899 -30
- package/bin/_cctally_source_analytics.py +567 -133
- package/bin/_cctally_transcript.py +27 -2
- package/bin/_cctally_tui.py +39 -6
- package/bin/_cctally_update.py +420 -14
- package/bin/_lib_cache_report.py +7 -10
- package/bin/_lib_codex_conversation_query.py +286 -63
- package/bin/_lib_codex_metadata.py +207 -0
- package/bin/_lib_conversation_query.py +254 -37
- package/bin/_lib_doctor.py +16 -2
- package/bin/_lib_pricing.py +80 -276
- package/bin/_lib_rederive.py +903 -2
- package/bin/_lib_view_models.py +5 -0
- package/bin/cctally +18 -5
- package/dashboard/static/assets/ConversationsView-BB9vUiL6.js +72 -0
- package/dashboard/static/assets/{DoctorModal-BZGXOlc2.js → DoctorModal-DFY2iPDf.js} +1 -1
- package/dashboard/static/assets/ModalRoot-DvRKHbQE.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-KbO_KWa7.js → ProjectsDrillPanel-DAv39uSo.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BrOPLVRL.js → SourceDetailModal-DSAWo2Fp.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CYgEFAi4.js → UpdateModal-R_9dOVnh.js} +3 -3
- package/dashboard/static/assets/index-COhxe3vU.css +1 -0
- package/dashboard/static/assets/index-DdU62sov.js +13 -0
- package/dashboard/static/assets/outlineNavigation-BOCLcRa6.js +9 -0
- package/dashboard/static/assets/{useKeymap-DQx_A1MB.js → useKeymap-CW7Up0Ay.js} +1 -1
- package/dashboard/static/dashboard.html +3 -3
- package/package.json +2 -1
- package/dashboard/static/assets/ConversationsView-TO5bpIyc.js +0 -72
- package/dashboard/static/assets/ModalRoot-C_fvugTl.js +0 -1
- package/dashboard/static/assets/index-Cj_l7T7u.css +0 -1
- package/dashboard/static/assets/index-DLVwACUM.js +0 -13
- package/dashboard/static/assets/outlineNavigation-DXX-RjKl.js +0 -9
|
@@ -320,7 +320,42 @@ def _claude_cycle_rows(conn: sqlite3.Connection, ref) -> list:
|
|
|
320
320
|
).fetchall()
|
|
321
321
|
|
|
322
322
|
|
|
323
|
-
def
|
|
323
|
+
def _five_hour_detail_revision(conn, start_iso, end_iso) -> tuple[int, ...]:
|
|
324
|
+
"""Append-only 5h child revision for one cycle's intersecting blocks.
|
|
325
|
+
|
|
326
|
+
The detail cache uses the index stamp. Weekly crossing counts do not move
|
|
327
|
+
when another account records a 5h credit or crossing in an existing block.
|
|
328
|
+
These child tables insert new ids for those events; count and max id also
|
|
329
|
+
notice deletions and replacement rows without hashing an unbounded ledger.
|
|
330
|
+
"""
|
|
331
|
+
if not start_iso or not end_iso:
|
|
332
|
+
return (0, 0, 0, 0, 0, 0)
|
|
333
|
+
interval = (
|
|
334
|
+
"unixepoch(b.block_start_at) < unixepoch(?) "
|
|
335
|
+
"AND unixepoch(b.five_hour_resets_at) > unixepoch(?)"
|
|
336
|
+
)
|
|
337
|
+
bounds = (end_iso, start_iso)
|
|
338
|
+
blocks = conn.execute(
|
|
339
|
+
"SELECT COUNT(*), MAX(b.id) FROM five_hour_blocks b "
|
|
340
|
+
f"WHERE {interval}", bounds,
|
|
341
|
+
).fetchone()
|
|
342
|
+
milestones = conn.execute(
|
|
343
|
+
"SELECT COUNT(*), MAX(m.id) FROM five_hour_blocks b "
|
|
344
|
+
"JOIN five_hour_milestones m ON m.block_id = b.id "
|
|
345
|
+
f"WHERE {interval}", bounds,
|
|
346
|
+
).fetchone()
|
|
347
|
+
credits = conn.execute(
|
|
348
|
+
"SELECT COUNT(*), MAX(e.id) FROM five_hour_blocks b "
|
|
349
|
+
"JOIN five_hour_reset_events e "
|
|
350
|
+
"ON e.account_key = b.account_key "
|
|
351
|
+
"AND e.five_hour_window_key = b.five_hour_window_key "
|
|
352
|
+
f"WHERE {interval}", bounds,
|
|
353
|
+
).fetchone()
|
|
354
|
+
return tuple(int(value or 0) for row in (blocks, milestones, credits)
|
|
355
|
+
for value in row)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _index_entry(conn, ref, current_cycle_key, tz, *, decorated=False) -> dict:
|
|
324
359
|
rows = _claude_cycle_rows(conn, ref)
|
|
325
360
|
milestone_count = len(rows)
|
|
326
361
|
segment_count = 1 if rows else 0
|
|
@@ -329,6 +364,11 @@ def _index_entry(conn, ref, current_cycle_key, tz) -> dict:
|
|
|
329
364
|
end_z = _to_iso_z(ref.week_end_at)
|
|
330
365
|
block_count = _count_blocks(conn, start_z, end_z)
|
|
331
366
|
key = _claude_cycle_key(ref)
|
|
367
|
+
# R8: leave the <=1-real-account stamp byte-identical. On a decorated
|
|
368
|
+
# provider the fetched block list contains multiple account-owned rows,
|
|
369
|
+
# so its cache identity also needs their child-ledger revision.
|
|
370
|
+
block_revision = (
|
|
371
|
+
_five_hour_detail_revision(conn, start_z, end_z) if decorated else ())
|
|
332
372
|
|
|
333
373
|
# #750 S4 §5.3. A CURRENT single-segment cycle neither fetches its detail
|
|
334
374
|
# nor uses one that arrives, so a gap in the LIVE cycle would stay
|
|
@@ -356,7 +396,8 @@ def _index_entry(conn, ref, current_cycle_key, tz) -> dict:
|
|
|
356
396
|
"segment_count": segment_count,
|
|
357
397
|
"has_observation_gap": has_gap,
|
|
358
398
|
"detail_stamp": _mh.compute_detail_stamp(
|
|
359
|
-
key, milestone_count, block_count, segment_count, max_captured
|
|
399
|
+
key, milestone_count, block_count, segment_count, max_captured,
|
|
400
|
+
*block_revision,
|
|
360
401
|
),
|
|
361
402
|
}
|
|
362
403
|
|
|
@@ -377,7 +418,12 @@ def build_claude_week_index(conn: sqlite3.Connection) -> list:
|
|
|
377
418
|
)
|
|
378
419
|
current_cycle_key = _claude_cycle_key(current_ref) if current_ref else None
|
|
379
420
|
tz = _resolve_display_tz()
|
|
380
|
-
|
|
421
|
+
import _cctally_account
|
|
422
|
+
decorated = _cctally_account.provider_is_decorated(conn, "claude")
|
|
423
|
+
entries = [
|
|
424
|
+
_index_entry(conn, ref, current_cycle_key, tz, decorated=decorated)
|
|
425
|
+
for ref in refs
|
|
426
|
+
]
|
|
381
427
|
entries.sort(key=lambda e: e["start_at_utc"] or "", reverse=True)
|
|
382
428
|
return entries
|
|
383
429
|
|
|
@@ -443,14 +489,17 @@ def _shape_observation_gap_runs(disclosure, rows) -> list:
|
|
|
443
489
|
return out
|
|
444
490
|
|
|
445
491
|
|
|
446
|
-
def _load_block_credits(
|
|
492
|
+
def _load_block_credits(
|
|
493
|
+
conn: sqlite3.Connection, window_key: int, account_key: str,
|
|
494
|
+
) -> list:
|
|
447
495
|
"""5h in-place credit rows for a block, ascending by effective time —
|
|
448
496
|
same wire shape as the envelope's ``five_hour_block.credits``."""
|
|
449
497
|
rows = conn.execute(
|
|
450
498
|
"SELECT effective_reset_at_utc, prior_percent, post_percent "
|
|
451
|
-
"FROM five_hour_reset_events
|
|
499
|
+
"FROM five_hour_reset_events "
|
|
500
|
+
"WHERE five_hour_window_key = ? AND account_key = ? "
|
|
452
501
|
"ORDER BY effective_reset_at_utc ASC",
|
|
453
|
-
(int(window_key),),
|
|
502
|
+
(int(window_key), account_key),
|
|
454
503
|
).fetchall()
|
|
455
504
|
return [
|
|
456
505
|
{
|
|
@@ -470,6 +519,8 @@ def _build_blocks(conn: sqlite3.Connection, start_iso, end_iso) -> list:
|
|
|
470
519
|
if not start_iso or not end_iso:
|
|
471
520
|
return []
|
|
472
521
|
c = _cctally()
|
|
522
|
+
import _cctally_account
|
|
523
|
+
decorated = _cctally_account.provider_is_decorated(conn, "claude")
|
|
473
524
|
# #834 S1 (#836): `id` is selected so each block's milestones are loaded by
|
|
474
525
|
# the PRECISE selector. This route used to select blocks account-blind and
|
|
475
526
|
# then pass a bare `five_hour_window_key` to the milestone read, while block
|
|
@@ -477,13 +528,13 @@ def _build_blocks(conn: sqlite3.Connection, start_iso, end_iso) -> list:
|
|
|
477
528
|
# window's per-account blocks each rendered EVERY account's milestones. `id`
|
|
478
529
|
# stays internal: it is not added to the published `blocks[]` shape.
|
|
479
530
|
rows = conn.execute(
|
|
480
|
-
"SELECT id, five_hour_window_key, block_start_at, five_hour_resets_at, "
|
|
531
|
+
"SELECT id, account_key, five_hour_window_key, block_start_at, five_hour_resets_at, "
|
|
481
532
|
" final_five_hour_percent, total_cost_usd, "
|
|
482
533
|
" crossed_seven_day_reset, is_closed "
|
|
483
534
|
"FROM five_hour_blocks "
|
|
484
535
|
"WHERE unixepoch(block_start_at) < unixepoch(?) "
|
|
485
536
|
" AND unixepoch(five_hour_resets_at) > unixepoch(?) "
|
|
486
|
-
"ORDER BY unixepoch(block_start_at) ASC, five_hour_window_key ASC",
|
|
537
|
+
"ORDER BY unixepoch(block_start_at) ASC, five_hour_window_key ASC, id ASC",
|
|
487
538
|
(end_iso, start_iso),
|
|
488
539
|
).fetchall()
|
|
489
540
|
out: list = []
|
|
@@ -502,7 +553,12 @@ def _build_blocks(conn: sqlite3.Connection, start_iso, end_iso) -> list:
|
|
|
502
553
|
"is_closed": bool(b["is_closed"]),
|
|
503
554
|
"milestones": c._tui_build_five_hour_milestones(
|
|
504
555
|
conn, wk, block_id=int(b["id"])),
|
|
505
|
-
"credits": _load_block_credits(conn, wk),
|
|
556
|
+
"credits": _load_block_credits(conn, wk, b["account_key"]),
|
|
557
|
+
**({
|
|
558
|
+
"account_key": b["account_key"],
|
|
559
|
+
"account_label": _cctally_account.display_account_label(
|
|
560
|
+
conn, b["account_key"]),
|
|
561
|
+
} if decorated else {}),
|
|
506
562
|
}
|
|
507
563
|
)
|
|
508
564
|
return out
|
package/bin/_cctally_parser.py
CHANGED
|
@@ -3175,6 +3175,11 @@ def _build_db_parser(subparsers, name, *, help_text, xref=None):
|
|
|
3175
3175
|
action="store_true",
|
|
3176
3176
|
help="Append the correction batch and rebuild stats.db (default: preview)",
|
|
3177
3177
|
)
|
|
3178
|
+
db_rederive.add_argument(
|
|
3179
|
+
"--reviewed-weekly-decisions",
|
|
3180
|
+
metavar="PATH",
|
|
3181
|
+
help="Review exact Claude weekly observations from a pinned manifest",
|
|
3182
|
+
)
|
|
3178
3183
|
db_rederive.add_argument(
|
|
3179
3184
|
"--json",
|
|
3180
3185
|
action="store_true",
|
package/bin/_cctally_record.py
CHANGED
|
@@ -3922,7 +3922,7 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
3922
3922
|
five_hour_percent, as_of=None, commit=True,
|
|
3923
3923
|
ctx=None, account_key=_lib_accounts.UNATTRIBUTED,
|
|
3924
3924
|
origin_observation_id=None, capture_at=None,
|
|
3925
|
-
source=None):
|
|
3925
|
+
source=None, hold_weekly_axis=False):
|
|
3926
3926
|
"""Detect + record weekly and 5h reset/credit artifacts for one usage
|
|
3927
3927
|
observation (extracted from ``cmd_record_usage``; DB journal redesign
|
|
3928
3928
|
§5.2.3).
|
|
@@ -3986,6 +3986,10 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
3986
3986
|
one value. The weekly branch does not read it: extending source-local
|
|
3987
3987
|
confirmation to the weekly path would delay the immediate 63-to-0 reset
|
|
3988
3988
|
that #755 exists to stop delaying.
|
|
3989
|
+
- ``hold_weekly_axis`` (default ``False``): scratch replay's explicit
|
|
3990
|
+
reviewed observation hold. It bypasses weekly reset/credit detection
|
|
3991
|
+
while still running the independent five-hour detector below. Live
|
|
3992
|
+
callers retain the default.
|
|
3989
3993
|
"""
|
|
3990
3994
|
c = _cctally()
|
|
3991
3995
|
now_utc = _as_of_or_command(as_of)
|
|
@@ -4025,7 +4029,7 @@ def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
|
4025
4029
|
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
4026
4030
|
(account_key,),
|
|
4027
4031
|
).fetchone()
|
|
4028
|
-
if prior and prior["week_end_at"] and cur_end_canon:
|
|
4032
|
+
if not hold_weekly_axis and prior and prior["week_end_at"] and cur_end_canon:
|
|
4029
4033
|
prior_end_canon = _canonicalize_optional_iso(
|
|
4030
4034
|
prior["week_end_at"], "record.prior"
|
|
4031
4035
|
)
|
|
@@ -7265,6 +7269,13 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
7265
7269
|
if "resets_at" not in payload or "weekly_percent" not in payload:
|
|
7266
7270
|
return
|
|
7267
7271
|
|
|
7272
|
+
# The exact raw observation id is reviewed outside this pipeline. A hold
|
|
7273
|
+
# can only be supplied on scratch replay; live ingest has no event sink.
|
|
7274
|
+
hold_weekly_axis = (
|
|
7275
|
+
ctx.event_sink is not None
|
|
7276
|
+
and rec.get("id") in ctx.held_weekly_observation_ids
|
|
7277
|
+
)
|
|
7278
|
+
|
|
7268
7279
|
c = _cctally()
|
|
7269
7280
|
conn = ctx.conn
|
|
7270
7281
|
# `as_of` is the record's `at` (= `_command_as_of()` at capture, CCTALLY_AS_OF
|
|
@@ -7326,6 +7337,20 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
7326
7337
|
except (ValueError, TypeError) as exc:
|
|
7327
7338
|
eprint(f"[ingest] 5h window-key derivation failed: {exc}")
|
|
7328
7339
|
|
|
7340
|
+
import _cctally_journal as jr
|
|
7341
|
+
reviewed_weekly_basis = None
|
|
7342
|
+
if hold_weekly_axis:
|
|
7343
|
+
reviewed_weekly_basis = (
|
|
7344
|
+
ctx.reviewed_weekly_basis_by_account.get(account_key)
|
|
7345
|
+
or jr._latest_weekly_basis(conn, account_key)
|
|
7346
|
+
)
|
|
7347
|
+
if reviewed_weekly_basis is None:
|
|
7348
|
+
import _lib_rederive
|
|
7349
|
+
raise _lib_rederive.RederiveConflict(
|
|
7350
|
+
"reviewed weekly hold has no accepted account basis: "
|
|
7351
|
+
f"{rec['id']}"
|
|
7352
|
+
)
|
|
7353
|
+
|
|
7329
7354
|
# 1. Reset/credit detection (fold into the cycle txn; Design B suppression).
|
|
7330
7355
|
c.detect_reset_and_credit(
|
|
7331
7356
|
conn,
|
|
@@ -7350,11 +7375,11 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
7350
7375
|
# #769 S2 §3: the contributor discriminator for five-hour credit
|
|
7351
7376
|
# confirmation. `payload.source`, never `rec["src"]`.
|
|
7352
7377
|
source=source,
|
|
7378
|
+
hold_weekly_axis=hold_weekly_axis,
|
|
7353
7379
|
)
|
|
7354
7380
|
|
|
7355
7381
|
# 2. Accept/skip DECISION (clamp + dedup), made ONCE and journaled via the
|
|
7356
7382
|
# snapshot_accept evt (so replay never re-derives it — spec §5.3).
|
|
7357
|
-
import _cctally_journal as jr
|
|
7358
7383
|
fold = jr._usage_snapshot_fold_decision(conn, {
|
|
7359
7384
|
"week_start_date": week_start_date,
|
|
7360
7385
|
"week_start_at": week_start_at,
|
|
@@ -7363,10 +7388,11 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
7363
7388
|
"five_hour_percent": five_hour_percent,
|
|
7364
7389
|
"five_hour_window_key": five_hour_window_key,
|
|
7365
7390
|
"account_key": account_key,
|
|
7366
|
-
}
|
|
7367
|
-
|
|
7368
|
-
#
|
|
7369
|
-
#
|
|
7391
|
+
}, force_weekly_hold=hold_weekly_axis,
|
|
7392
|
+
reviewed_weekly_basis=reviewed_weekly_basis)
|
|
7393
|
+
# #769 S11 (#824). `weekly_held` says the weekly axis is held at the
|
|
7394
|
+
# accepted basis (an ordinary lower clamp or an explicit scratch hold);
|
|
7395
|
+
# `wrote_row` says whether this tick materialized a snapshot at
|
|
7370
7396
|
# all. They are independent now: a held tick DOES write a row when its
|
|
7371
7397
|
# five-hour axis carries evidence, and that row is the only place the
|
|
7372
7398
|
# evidence survives, because `db rebuild` reconstructs open blocks from
|
|
@@ -7453,12 +7479,25 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
7453
7479
|
if rowid is None:
|
|
7454
7480
|
return
|
|
7455
7481
|
saved["id"] = rowid
|
|
7482
|
+
if ctx.event_sink is not None and not weekly_held:
|
|
7483
|
+
# Keep the most recent accepted weekly source across scratch
|
|
7484
|
+
# records. A later five-hour credit may suppress its snapshot
|
|
7485
|
+
# row, but that independent effect must not erase the reviewed
|
|
7486
|
+
# weekly basis for another held observation.
|
|
7487
|
+
basis = jr._latest_weekly_basis(conn, account_key)
|
|
7488
|
+
if basis is not None:
|
|
7489
|
+
ctx.reviewed_weekly_basis_by_account[account_key] = basis
|
|
7456
7490
|
else:
|
|
7491
|
+
saved_week_start_date = (
|
|
7492
|
+
fold.weekly.basis.week_start_date
|
|
7493
|
+
if weekly_held and fold.weekly.basis is not None
|
|
7494
|
+
else week_start_date
|
|
7495
|
+
)
|
|
7457
7496
|
latest = conn.execute(
|
|
7458
7497
|
"SELECT * FROM weekly_usage_snapshots WHERE week_start_date = ? "
|
|
7459
7498
|
" AND account_key = ? "
|
|
7460
7499
|
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
7461
|
-
(
|
|
7500
|
+
(saved_week_start_date, account_key),
|
|
7462
7501
|
).fetchone()
|
|
7463
7502
|
if latest is None:
|
|
7464
7503
|
return # nothing recorded yet -> nothing to derive against
|
|
@@ -7472,10 +7511,9 @@ def _pipeline_claude_usage(ctx, rec):
|
|
|
7472
7511
|
# crosses a $ threshold still derives it.
|
|
7473
7512
|
# The one exception is a HELD weekly axis. A no-change tick AGREES with
|
|
7474
7513
|
# the stored row, so re-deriving against it is the self-heal; a held
|
|
7475
|
-
# weekly axis means the
|
|
7476
|
-
#
|
|
7477
|
-
#
|
|
7478
|
-
# from `saved`'s higher percent records a crossing the meter says did not
|
|
7514
|
+
# weekly axis means the observation's 7d value was not accepted,
|
|
7515
|
+
# so it CONTRADICTS `saved` and a weekly milestone derived
|
|
7516
|
+
# from `saved`'s carried percent can record a crossing the meter did not
|
|
7479
7517
|
# happen. That is how the 2026-09-01 incident fabricated a 13% milestone
|
|
7480
7518
|
# in a fresh post-credit epoch from a stale pre-credit replica, and
|
|
7481
7519
|
# because milestones are forward-only within an epoch that row forecloses
|