cctally 1.115.0 → 1.116.1

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/bin/_cctally_cache.py +85 -4
  3. package/bin/_cctally_completion.py +80 -12
  4. package/bin/_cctally_core.py +106 -9
  5. package/bin/_cctally_dashboard.py +6 -9
  6. package/bin/_cctally_dashboard_envelope.py +26 -32
  7. package/bin/_cctally_db.py +85 -4
  8. package/bin/_cctally_doctor.py +38 -5
  9. package/bin/_cctally_five_hour.py +83 -1017
  10. package/bin/_cctally_journal.py +1307 -180
  11. package/bin/_cctally_journal_repair.py +9 -7
  12. package/bin/_cctally_oauth_samples.py +280 -0
  13. package/bin/_cctally_parser.py +5 -0
  14. package/bin/_cctally_quarantine.py +14 -4
  15. package/bin/_cctally_quota.py +70 -8
  16. package/bin/_cctally_quota_model.py +35 -4
  17. package/bin/_cctally_record.py +414 -123
  18. package/bin/_cctally_rederive.py +632 -235
  19. package/bin/_cctally_refresh.py +29 -0
  20. package/bin/_cctally_statusline.py +984 -47
  21. package/bin/_cctally_store.py +972 -58
  22. package/bin/_cctally_tui.py +62 -7
  23. package/bin/_cctally_update.py +25 -0
  24. package/bin/_lib_account_protocol.py +201 -18
  25. package/bin/_lib_clone_copy.py +46 -11
  26. package/bin/_lib_dashboard_sources.py +57 -15
  27. package/bin/_lib_detector_access_registry.py +273 -52
  28. package/bin/_lib_doctor.py +302 -1
  29. package/bin/_lib_journal.py +145 -25
  30. package/bin/_lib_journal_router.py +166 -48
  31. package/bin/_lib_oauth_samples.py +238 -0
  32. package/bin/_lib_pricing_check.py +0 -12
  33. package/bin/_lib_provider_activity.py +770 -0
  34. package/bin/_lib_quarantine.py +3 -2
  35. package/bin/_lib_quota_model.py +96 -6
  36. package/bin/_lib_rebuild_progress.py +146 -0
  37. package/bin/_lib_record.py +22 -0
  38. package/bin/_lib_rederive.py +299 -3
  39. package/bin/_lib_snapshot_cache.py +43 -1
  40. package/bin/_lib_stale_proof.py +708 -0
  41. package/bin/_lib_statusline_candidates.py +65 -5
  42. package/bin/_lib_worker_launches.py +315 -0
  43. package/bin/cctally +6 -11
  44. package/dashboard/static/assets/{ConversationsView-Bc8LCOAZ.js → ConversationsView-Dflb_myA.js} +1 -1
  45. package/dashboard/static/assets/DoctorModal-DFX5CfIs.js +1 -0
  46. package/dashboard/static/assets/{ModalRoot-DLPgyAM_.js → ModalRoot-HlxoagIa.js} +1 -1
  47. package/dashboard/static/assets/{ProjectsDrillPanel-z53a4v4M.js → ProjectsDrillPanel-C3PJy1xx.js} +1 -1
  48. package/dashboard/static/assets/{SourceDetailModal-Bw0T8W-W.js → SourceDetailModal-BczsWKhD.js} +1 -1
  49. package/dashboard/static/assets/{UpdateModal-D5AKx4Yw.js → UpdateModal-Os-Lo5cs.js} +1 -1
  50. package/dashboard/static/assets/{index-BXDOzSng.js → index-CYZ3NLqF.js} +3 -3
  51. package/dashboard/static/assets/{outlineNavigation-JObY7cUY.js → outlineNavigation-k-zIif6W.js} +1 -1
  52. package/dashboard/static/dashboard.html +1 -1
  53. package/package.json +7 -2
  54. package/bin/_lib_merge_gate.py +0 -452
  55. package/dashboard/static/assets/DoctorModal-D--qVBwe.js +0 -1
@@ -217,9 +217,11 @@ def _select_current_block_for_envelope(
217
217
  Delta semantics:
218
218
  - Non-crossed block: ``current_used_pct - seven_day_pct_at_block_start``
219
219
  (the natural "how much 7d% has changed during this 5h block" read), where
220
- that axis is the CREDIT-AWARE read of the column rather than the stored
221
- number (#834 S1, #835) — so a value an in-place weekly credit retired makes
222
- both the published axis and the delta ``None``.
220
+ that axis is the block's stored ``seven_day_pct_at_block_start``. #835's
221
+ retirement band, which published ``None`` for a value within a point of
222
+ one an in-place weekly credit retired, is removed (#840): stale
223
+ contributors are refused before publication, and a stored value that is
224
+ already wrong is corrected by ``db rederive``'s reviewed ``retract``.
223
225
  - Crossed block (``crossed_seven_day_reset == 1``): the block straddles
224
226
  a weekly reset, so the natural delta would be dominated by the
225
227
  reset itself (e.g. −94pp) rather than the user's actual burn-rate.
@@ -281,21 +283,17 @@ def _select_current_block_for_envelope(
281
283
  # Account-qualified selection binds the block and its downstream milestone
282
284
  # read to the displayed weekly account; the unqualified order remains the
283
285
  # historical <=1-real-account fallback for published byte stability.
284
- # #834 S1 (#835) Gate A R8: `first_observed_at_utc` and `five_hour_resets_at`
285
- # are selected because the credit-aware read below needs them — they are the
286
- # START axis's capture instant and its fallback. `seven_day_pct_at_block_end`
287
- # is deliberately NOT selected, and round three's comment claiming the
288
- # credit-aware read needs it was wrong: this route publishes the start axis
289
- # alone, the helper's second return value is discarded, and the helper tests
290
- # each axis's stored value before resolving anything, so an absent end value
291
- # skips a second week resolution and a second band set for a number nobody
292
- # reads. `last_observed_at_utc` stays, and NOT because the helper needs it:
293
- # every column the helper reads goes through `block.get(...)`, so dropping
294
- # this one would raise nothing and change no published field — the end
295
- # instant it feeds is discarded with the end axis. It stays because the
296
- # helper's contract is a block ROW, and pruning this SELECT to the subset the
297
- # helper happens to read today would couple this route to the helper's
298
- # internals. Round four's comment implied necessity instead.
286
+ # #834 S1 (#835) Gate A R8 selected `first_observed_at_utc`,
287
+ # `five_hour_resets_at` and `last_observed_at_utc` for the retirement band's
288
+ # week resolution. Since the band's removal (#840) the axes helper below
289
+ # reads only the stored `seven_day_pct_at_block_start` /
290
+ # `seven_day_pct_at_block_end` through `block.get(...)`, so none of the three
291
+ # is needed by it; they stay because the helper's contract is a block ROW,
292
+ # and pruning this SELECT to the subset the helper happens to read today
293
+ # would couple this route to the helper's internals.
294
+ # `seven_day_pct_at_block_end` is not selected: this route publishes the
295
+ # start axis alone and discards the helper's second return value, which is
296
+ # then `None`.
299
297
  block = conn.execute(
300
298
  """
301
299
  SELECT id, account_key,
@@ -319,14 +317,14 @@ def _select_current_block_for_envelope(
319
317
 
320
318
  crossed = bool(block["crossed_seven_day_reset"])
321
319
  # #834 S1 (#835) Gate A R8: THE THIRD CONSUMER of the two derived weekly axes
322
- # goes through the one read-time filter, the same one `cmd_five_hour_blocks` and
323
- # `cmd_five_hour_breakdown` use. Reading the column raw published the value an
324
- # in-place weekly credit retired, and `seven_day_pct_delta_pp` then published
325
- # the difference between the live post-credit percentage and that retired
326
- # number, which is a burn nobody observed. Measured on a store credited at 63.0
327
- # with `current_used_pct` at 20.0: `seven_day_pct_at_block_start: 63.0` and
328
- # `seven_day_pct_delta_pp: -43.0`, while the helper answered `(None, None)` for
329
- # the same row. `sqlite3.Row` has no `.get()`, so the row is converted.
320
+ # goes through the one shared axes helper, the same one `cmd_five_hour_blocks`
321
+ # and `cmd_five_hour_breakdown` use. #835 made that helper a read-time filter
322
+ # that withheld a value within a point of one an in-place weekly credit
323
+ # retired; #840 removed that retirement band, so the helper now returns the
324
+ # block's stored axes. A stale contributor is refused before it is
325
+ # published, and a stored value that is already wrong is corrected by
326
+ # `db rederive`'s reviewed `retract`, never by a percentage heuristic here.
327
+ # `sqlite3.Row` has no `.get()`, so the row is converted.
330
328
  #
331
329
  # Neither field is rendered by any client today: both are declared in
332
330
  # `dashboard/web/src/types/envelope.ts` and used in one vitest module,
@@ -2193,18 +2191,14 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
2193
2191
  _ld = sys.modules["cctally"]._load_sibling("_lib_doctor")
2194
2192
  _doc_state = sys.modules["cctally"].doctor_gather_state(now_utc=now_utc, runtime_bind=runtime_bind)
2195
2193
  _doc_report = _ld.run_checks(_doc_state)
2196
- doctor_envelope = {
2197
- "severity": _doc_report.overall_severity,
2198
- "counts": dict(_doc_report.counts),
2199
- "generated_at": _ld._iso_z(_doc_report.generated_at),
2200
- "fingerprint": _ld.fingerprint(_doc_report),
2201
- }
2194
+ doctor_envelope = _ld.envelope_block(_doc_report)
2202
2195
  except Exception as exc: # noqa: BLE001 — never crash SSE on doctor failure
2203
2196
  doctor_envelope = {
2204
2197
  "severity": "fail",
2205
2198
  "counts": {"ok": 0, "warn": 0, "fail": 1},
2206
2199
  "generated_at": _iso_z(now_utc),
2207
2200
  "fingerprint": "sha1:" + ("0" * 40),
2201
+ "rebuild_worker": None,
2208
2202
  "_error": f"{type(exc).__name__}: {exc}",
2209
2203
  }
2210
2204
 
@@ -9154,6 +9154,13 @@ def _027_codex_fork_preamble_rebuild(conn: sqlite3.Connection) -> None:
9154
9154
  _release_cache_db_writer_flocks(held)
9155
9155
 
9156
9156
 
9157
+ #: Pages 028 returns per committed transaction when it reclaims the dropped
9158
+ #: transcript store. The chunk bounds each transaction's WAL growth at roughly,
9159
+ #: but more than, one SQLite WAL auto-checkpoint (1000 frames): each moved page
9160
+ #: also writes its pointer-map page, its parent and the header page.
9161
+ _028_VACUUM_CHUNK_PAGES = 1000
9162
+
9163
+
9157
9164
  @cache_migration("028_split_conversation_store")
9158
9165
  def _028_split_conversation_store(conn: sqlite3.Connection) -> None:
9159
9166
  """Move the re-derivable transcript corpus out of cache.db (#320).
@@ -9263,11 +9270,45 @@ def _028_split_conversation_store(conn: sqlite3.Connection) -> None:
9263
9270
  raise
9264
9271
  try:
9265
9272
  if conn.execute("PRAGMA auto_vacuum").fetchone()[0] == 2:
9266
- # Python 3.11/Linux may defer this zero-column PRAGMA until the
9267
- # cursor is exhausted; fetchall drives it to completion.
9268
- conn.execute("PRAGMA incremental_vacuum").fetchall()
9273
+ # Return the dropped tables' pages one page per statement. A
9274
+ # bare `PRAGMA incremental_vacuum` returns no columns, and
9275
+ # CPython 3.11's cursor steps such a statement exactly once,
9276
+ # fetchall() or not, so it frees ONE page (#998). A statement
9277
+ # capped at one page is complete after that first step on every
9278
+ # Python (the `_run_incremental_vacuum_chunk` pattern). Each
9279
+ # chunk commits so WAL auto-checkpoints run between chunks, and
9280
+ # the loop stops when a chunk frees nothing, so it is bounded.
9281
+ remaining = conn.execute("PRAGMA freelist_count").fetchone()[0]
9282
+ while remaining:
9283
+ # Any failure inside a chunk, including a non-database
9284
+ # error or an interrupt, rolls that chunk back before it
9285
+ # propagates, so no write transaction outlives the
9286
+ # conversation locks released below.
9287
+ try:
9288
+ conn.execute("BEGIN IMMEDIATE")
9289
+ for _ in range(min(remaining, _028_VACUUM_CHUNK_PAGES)):
9290
+ conn.execute("PRAGMA incremental_vacuum(1)").fetchall()
9291
+ conn.commit()
9292
+ except BaseException:
9293
+ try:
9294
+ conn.rollback()
9295
+ except sqlite3.Error:
9296
+ pass
9297
+ raise
9298
+ left = conn.execute("PRAGMA freelist_count").fetchone()[0]
9299
+ if left >= remaining:
9300
+ break
9301
+ remaining = left
9269
9302
  except sqlite3.DatabaseError:
9270
- pass
9303
+ # Intentional: the reclaim is best effort. Raising here would leave
9304
+ # 028 unapplied, and its next run would re-arm both conversation
9305
+ # store rebuild-from-scratch flags; free pages left behind are
9306
+ # harmless and `db vacuum` returns them. The guarded rollback keeps
9307
+ # a failing rollback from turning that into a handler failure.
9308
+ try:
9309
+ conn.rollback()
9310
+ except sqlite3.Error:
9311
+ pass
9271
9312
  finally:
9272
9313
  for fh in reversed(held):
9273
9314
  try:
@@ -11312,6 +11353,9 @@ def cmd_db_status(args: argparse.Namespace) -> int:
11312
11353
  ),
11313
11354
  },
11314
11355
  }
11356
+ # #997 D10: additive — each detached rebuild worker's state and its
11357
+ # read-only classification (the doctor `db.rebuild_worker` kernel).
11358
+ payload["rebuildWorkers"] = _db_status_rebuild_workers()
11315
11359
  if getattr(args, "json", False):
11316
11360
  print(json.dumps(payload, indent=2, sort_keys=True))
11317
11361
  return 0
@@ -11329,9 +11373,46 @@ def cmd_db_status(args: argparse.Namespace) -> int:
11329
11373
  line = _db_status_format_row(m)
11330
11374
  print(line)
11331
11375
  print() # blank line between DBs
11376
+ for worker in ("epoch", "heal"):
11377
+ info = payload["rebuildWorkers"][worker]
11378
+ if info["classification"]["row"] != "idle":
11379
+ print(info["classification"]["summary"])
11332
11380
  return 0
11333
11381
 
11334
11382
 
11383
+ def _db_status_rebuild_workers() -> dict:
11384
+ """``{"epoch": {...}, "heal": {...}}``: each worker's run state and its
11385
+ classification, gathered without any lock (#997 D8/D10)."""
11386
+ import _cctally_store
11387
+ import _lib_doctor
11388
+
11389
+ now_utc = _cctally_core._command_as_of()
11390
+ out: dict = {}
11391
+ for worker in ("epoch", "heal"):
11392
+ try:
11393
+ snapshot = _cctally_store.rebuild_worker_snapshot(worker)
11394
+ except Exception: # noqa: BLE001 — a diagnostic never fails status
11395
+ snapshot = None
11396
+ assessed = _lib_doctor.assess_rebuild_worker(worker, snapshot,
11397
+ now_utc)
11398
+ details = assessed["details"]
11399
+ out[worker] = {
11400
+ "state": details["state"],
11401
+ "stateUnreadable": details["stateUnreadable"],
11402
+ "ledger": details["ledger"],
11403
+ "liveness": details["liveness"],
11404
+ "pending": details["pending"],
11405
+ "recoveredDeaths": details["recoveredDeaths"],
11406
+ "classification": {
11407
+ "severity": assessed["severity"],
11408
+ "row": assessed["row"],
11409
+ "summary": assessed["summary"],
11410
+ "warnings": assessed["warnings"],
11411
+ },
11412
+ }
11413
+ return out
11414
+
11415
+
11335
11416
  def _db_status_for(
11336
11417
  db_path: pathlib.Path,
11337
11418
  registry: list[Migration],
@@ -507,6 +507,31 @@ def _fetch_dashboard_write_io(descriptor: dict, *, timeout_s: float) -> dict:
507
507
  return {**base, "probe": "ok", "reason": None, "write_io": write_io}
508
508
 
509
509
 
510
+ _REBUILD_WORKERS = ("epoch", "heal")
511
+
512
+
513
+ def _rebuild_worker_gather_failed(worker: str, exc: BaseException) -> dict:
514
+ """A snapshot that could not be gathered reads as an unreadable state
515
+ (the existing row), with the error in `details`, never as "has run"."""
516
+ return {"worker": worker, "state": None, "stateUnreadable": True,
517
+ "ledger": None, "ledgerUnreadable": False, "identities": {},
518
+ "pending": {}, "reread": False,
519
+ "gatherError": f"{type(exc).__name__}: {exc}"}
520
+
521
+
522
+ def _gather_rebuild_workers() -> dict:
523
+ """#997 D10: each detached rebuild worker's read-only snapshot; a worker
524
+ whose snapshot fails is reported unreadable, the other still gathered."""
525
+ out: dict = {}
526
+ for worker in _REBUILD_WORKERS:
527
+ try:
528
+ import _cctally_store
529
+ out[worker] = _cctally_store.rebuild_worker_snapshot(worker)
530
+ except Exception as exc: # noqa: BLE001 — reported, never fatal
531
+ out[worker] = _rebuild_worker_gather_failed(worker, exc)
532
+ return out
533
+
534
+
510
535
  def _gather_dashboard_disk_writes(
511
536
  *, fetch=None, timeout_s: float = DASHBOARD_WRITE_IO_TIMEOUT_SECONDS,
512
537
  discover: bool = True,
@@ -559,6 +584,7 @@ def _gather_legacy_writer(deep_excluded, deep_boundary,
559
584
  says the detection pause exists only when the validated index holds the
560
585
  durable `inventory-incomplete` marker. Read-only: the sidecar is never
561
586
  written here, the spool is only read, and nothing is journaled."""
587
+ import _cctally_journal
562
588
  import _cctally_store
563
589
  import _lib_account_protocol
564
590
 
@@ -579,11 +605,9 @@ def _gather_legacy_writer(deep_excluded, deep_boundary,
579
605
  pc.close()
580
606
  except Exception: # noqa: BLE001 — a diagnostic read degrades to unavailable
581
607
  row = None
582
- try:
583
- evidence = json.loads(
584
- _cctally_core.LEGACY_WRITER_EVIDENCE_PATH.read_bytes())
585
- except (OSError, ValueError):
586
- evidence = None
608
+ # #797/#823/#840 review RV-5: the v2 sidecar, else the previous
609
+ # release's v1 one (read only) as its seed.
610
+ evidence, _seeded = _cctally_journal.read_legacy_writer_evidence_document()
587
611
  inventory = _legacy_spool_inventory_probe(inventory_marker)
588
612
  if inventory is not None:
589
613
  out["legacySpoolInventory"] = inventory
@@ -3208,6 +3232,14 @@ def _doctor_gather_state_impl(
3208
3232
  discover=_discover_dashboards)
3209
3233
  except Exception: # noqa: BLE001 — a probe failure must not fail doctor
3210
3234
  dashboard_disk_writes = {"mode": "error", "instances": []}
3235
+ # #997 D10: the detached rebuild workers, read-only (no worker or
3236
+ # admission flock is taken; a probe would make a starting worker `busy`).
3237
+ try:
3238
+ with _lib_perf.phase("doctor.rebuild_workers"):
3239
+ rebuild_workers = _gather_rebuild_workers()
3240
+ except Exception as exc: # noqa: BLE001 — a gather failure must not fail doctor
3241
+ rebuild_workers = {worker: _rebuild_worker_gather_failed(worker, exc)
3242
+ for worker in _REBUILD_WORKERS}
3211
3243
  return _lib_doctor.DoctorState(
3212
3244
  symlink_state=symlink_state,
3213
3245
  path_includes_local_bin=path_includes,
@@ -3318,6 +3350,7 @@ def _doctor_gather_state_impl(
3318
3350
  conversations_db_freelist_count=conversations_db_freelist_count,
3319
3351
  conversations_db_page_size=conversations_db_page_size,
3320
3352
  dashboard_disk_writes=dashboard_disk_writes,
3353
+ rebuild_workers=rebuild_workers,
3321
3354
  conversations_reclaim_pending=conversations_reclaim_pending,
3322
3355
  codex_quota_windows=codex_quota_windows,
3323
3356
  codex_hook_roots=codex_hook_roots,