cctally 1.113.0 → 1.115.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 +47 -0
- package/README.md +4 -6
- package/bin/_cctally_account.py +32 -8
- package/bin/_cctally_cache.py +6035 -722
- package/bin/_cctally_completion.py +1287 -0
- package/bin/_cctally_config.py +37 -0
- package/bin/_cctally_core.py +400 -2
- package/bin/_cctally_dashboard.py +98 -20
- package/bin/_cctally_dashboard_conversation.py +292 -23
- package/bin/_cctally_dashboard_envelope.py +16 -3
- package/bin/_cctally_dashboard_perf.py +128 -9
- package/bin/_cctally_dashboard_sources.py +285 -213
- package/bin/_cctally_db.py +1177 -23
- package/bin/_cctally_diagnosis_sources.py +22 -4
- package/bin/_cctally_doctor.py +572 -45
- package/bin/_cctally_five_hour.py +84 -33
- package/bin/_cctally_forecast.py +108 -15
- package/bin/_cctally_journal.py +3950 -116
- package/bin/_cctally_journal_repair.py +101 -12
- package/bin/_cctally_milestone_history.py +82 -9
- package/bin/_cctally_parser.py +42 -0
- package/bin/_cctally_percent_breakdown.py +3 -2
- package/bin/_cctally_project.py +8 -2
- package/bin/_cctally_quarantine.py +652 -0
- package/bin/_cctally_quota.py +227 -37
- package/bin/_cctally_quota_model.py +360 -83
- package/bin/_cctally_record.py +691 -149
- package/bin/_cctally_rederive.py +320 -41
- package/bin/_cctally_refresh.py +84 -13
- package/bin/_cctally_source_analytics.py +83 -17
- package/bin/_cctally_statusline.py +6154 -517
- package/bin/_cctally_store.py +367 -53
- package/bin/_cctally_transcript.py +8 -7
- package/bin/_cctally_tui.py +61 -56
- package/bin/_cctally_weekrefs.py +128 -21
- package/bin/_lib_account_binding.py +152 -0
- package/bin/_lib_account_protocol.py +386 -0
- package/bin/_lib_aggregators.py +4 -0
- package/bin/_lib_cache_coverage.py +13 -1
- package/bin/_lib_cache_report.py +35 -6
- package/bin/_lib_cache_writer_lock.py +19 -3
- package/bin/_lib_clone_copy.py +142 -0
- package/bin/_lib_codex_conversation.py +32 -0
- package/bin/_lib_codex_conversation_query.py +297 -49
- package/bin/_lib_codex_conversation_watch.py +59 -14
- package/bin/_lib_codex_drain_admission.py +214 -0
- package/bin/_lib_codex_metadata.py +18 -1
- package/bin/_lib_codex_publication_barrier.py +277 -0
- package/bin/_lib_codex_rollout_container.py +502 -0
- package/bin/_lib_conversation_dispatch.py +3 -0
- package/bin/_lib_conversation_query.py +106 -59
- package/bin/_lib_conversation_retention.py +1605 -640
- package/bin/_lib_cost_provenance.py +53 -2
- package/bin/_lib_detector_access_registry.py +3817 -0
- package/bin/_lib_doctor.py +714 -69
- package/bin/_lib_five_hour.py +126 -0
- package/bin/_lib_fmt.py +11 -0
- package/bin/_lib_ingest_frontier.py +414 -20
- package/bin/_lib_journal.py +141 -10
- package/bin/_lib_jsonl.py +5 -4
- package/bin/_lib_legacy_spool.py +126 -0
- package/bin/_lib_legacy_writer_evidence.py +136 -0
- package/bin/_lib_merge_gate.py +231 -86
- package/bin/_lib_meter_rate_change.py +10 -0
- package/bin/_lib_outcome_quarantine.py +690 -0
- package/bin/_lib_perf.py +27 -1
- package/bin/_lib_pricing.py +199 -9
- package/bin/_lib_pricing_check.py +20 -4
- package/bin/_lib_quarantine.py +154 -0
- package/bin/_lib_quota_calibration.py +53 -8
- package/bin/_lib_quota_copy.py +2 -1
- package/bin/_lib_quota_model.py +288 -46
- package/bin/_lib_reclaim_planner.py +792 -0
- package/bin/_lib_rederive.py +319 -82
- package/bin/_lib_segment_summary.py +35 -2
- package/bin/_lib_snapshot_cache.py +21 -0
- package/bin/_lib_sqlite_close.py +81 -0
- package/bin/_lib_statusline_candidates.py +292 -23
- package/bin/_lib_subscription_weeks.py +100 -17
- package/bin/_lib_tick_stats.py +217 -17
- package/bin/_lib_wal_checkpoint.py +1077 -0
- package/bin/_lib_write_budget.py +483 -0
- package/bin/_lib_write_io.py +703 -0
- package/bin/cctally +31 -1
- package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
- package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
- package/dashboard/static/assets/index-BKITk1CT.css +1 -0
- package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
- package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +19 -1
- package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
- package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
|
@@ -764,6 +764,7 @@ from _lib_snapshot_cache import (
|
|
|
764
764
|
_reset_sig as _snapshot_reset_sig,
|
|
765
765
|
)
|
|
766
766
|
import _lib_tick_stats
|
|
767
|
+
import _lib_write_io
|
|
767
768
|
|
|
768
769
|
|
|
769
770
|
# === F22a: count the silent Group A cache-open failures (#583 S1 §1.6) =====
|
|
@@ -2585,9 +2586,9 @@ def _dashboard_maybe_prune_retention() -> None:
|
|
|
2585
2586
|
crash the sync thread)."""
|
|
2586
2587
|
try:
|
|
2587
2588
|
c = sys.modules["cctally"]
|
|
2588
|
-
from _cctally_config import
|
|
2589
|
+
from _cctally_config import resolve_prune_retention_days
|
|
2589
2590
|
import _lib_conversation_retention as retention
|
|
2590
|
-
retention_days =
|
|
2591
|
+
retention_days = resolve_prune_retention_days(c.load_config())
|
|
2591
2592
|
if retention_days <= 0:
|
|
2592
2593
|
return
|
|
2593
2594
|
conn = c.open_conversations_db(attach_cache=False)
|
|
@@ -2855,11 +2856,13 @@ def _conversation_sync_loop(
|
|
|
2855
2856
|
while not stop.is_set():
|
|
2856
2857
|
t0 = monotonic()
|
|
2857
2858
|
cpu0 = thread_time_ns()
|
|
2859
|
+
written = _lib_write_io.begin_interval("conversation")
|
|
2858
2860
|
try:
|
|
2859
2861
|
status = run_iteration() or "ok"
|
|
2860
2862
|
except Exception: # noqa: BLE001 — the worker must outlive one bad pass
|
|
2861
2863
|
_log_sync_iteration_failure()
|
|
2862
2864
|
status = "error"
|
|
2865
|
+
observation = _lib_write_io.end_interval(written)
|
|
2863
2866
|
work = max(0.0, monotonic() - t0)
|
|
2864
2867
|
cpu_ns = max(0, thread_time_ns() - cpu0)
|
|
2865
2868
|
seq += 1
|
|
@@ -2882,6 +2885,12 @@ def _conversation_sync_loop(
|
|
|
2882
2885
|
"codex", "not_observed"),
|
|
2883
2886
|
claude_files=getattr(status, "files", {}).get("claude", 0),
|
|
2884
2887
|
codex_files=getattr(status, "files", {}).get("codex", 0),
|
|
2888
|
+
# #901 §5.5: the pass's process write delta. It includes the
|
|
2889
|
+
# main loop's concurrent work, which `write_overlap` flags; it
|
|
2890
|
+
# is never summed with a tick's.
|
|
2891
|
+
process_write_bytes=observation.process_write_bytes,
|
|
2892
|
+
write_status=observation.status,
|
|
2893
|
+
write_overlap=observation.overlap,
|
|
2885
2894
|
)
|
|
2886
2895
|
deadline = _conversation_next_deadline(t0, interval, work)
|
|
2887
2896
|
remaining = deadline - monotonic()
|
|
@@ -6894,6 +6903,38 @@ def _assemble_projects_via_cache(
|
|
|
6894
6903
|
return buckets, total_cost_by_week, key_by_bucket
|
|
6895
6904
|
|
|
6896
6905
|
|
|
6906
|
+
def _projects_week_usage_rows(
|
|
6907
|
+
conn: "sqlite3.Connection", since_date: str, end_date: str,
|
|
6908
|
+
) -> list:
|
|
6909
|
+
"""The anchored, non-NULL weekly readings of the projects window, oldest
|
|
6910
|
+
capture first: ``(week_start_date, week_start_at, weekly_percent,
|
|
6911
|
+
captured_at_utc, id)`` for every week date in ``[since_date, end_date)``.
|
|
6912
|
+
|
|
6913
|
+
The window holds ``weeks_back`` weeks of status-line readings, which is
|
|
6914
|
+
not a bound on a sorter, so the rows are ordered here rather than by an
|
|
6915
|
+
``ORDER BY`` that built a temp b-tree over all of them on every build
|
|
6916
|
+
(#901 Q11). ``(captured_at_utc, id)`` is a total order — ``id`` is the
|
|
6917
|
+
primary key and ``captured_at_utc`` is NOT NULL TEXT, which Python
|
|
6918
|
+
compares in the same code-point order as SQLite's BINARY collation — so
|
|
6919
|
+
the result is exactly the former ``ORDER BY captured_at_utc ASC, id ASC``.
|
|
6920
|
+
"""
|
|
6921
|
+
rows = conn.execute(
|
|
6922
|
+
" SELECT week_start_date, week_start_at, weekly_percent,"
|
|
6923
|
+
" captured_at_utc, id"
|
|
6924
|
+
" FROM weekly_usage_snapshots"
|
|
6925
|
+
# Held rows excluded: these rows feed `latest_usage_by_segment`, which
|
|
6926
|
+
# is a weekly read, and the pre-fetch must select the same population
|
|
6927
|
+
# the reducer would select for itself (#769 S11, #824).
|
|
6928
|
+
" WHERE week_start_date >= ? AND week_start_date < ?"
|
|
6929
|
+
" AND date(week_start_date) IS NOT NULL"
|
|
6930
|
+
" AND weekly_percent IS NOT NULL"
|
|
6931
|
+
+ _cctally_core.weekly_held_exclusion(conn),
|
|
6932
|
+
(since_date, end_date),
|
|
6933
|
+
).fetchall()
|
|
6934
|
+
rows.sort(key=lambda row: (row[3], row[4]))
|
|
6935
|
+
return rows
|
|
6936
|
+
|
|
6937
|
+
|
|
6897
6938
|
def _build_projects_envelope(
|
|
6898
6939
|
conn: "sqlite3.Connection",
|
|
6899
6940
|
*,
|
|
@@ -7165,24 +7206,8 @@ def _build_projects_envelope(
|
|
|
7165
7206
|
# valid rendered-week row (#620 S1 A3).
|
|
7166
7207
|
weekly_pct_by_week: dict[dt.datetime, float] = {}
|
|
7167
7208
|
try:
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
" captured_at_utc, id"
|
|
7171
|
-
" FROM weekly_usage_snapshots"
|
|
7172
|
-
# Held rows excluded: these rows feed `latest_usage_by_segment`,
|
|
7173
|
-
# which is a weekly read, and the pre-fetch must select the same
|
|
7174
|
-
# population the reducer would select for itself (#769 S11, #824).
|
|
7175
|
-
" WHERE week_start_date >= ? AND week_start_date < ?"
|
|
7176
|
-
" AND date(week_start_date) IS NOT NULL"
|
|
7177
|
-
" AND weekly_percent IS NOT NULL"
|
|
7178
|
-
+ _cctally_core.weekly_held_exclusion(conn) +
|
|
7179
|
-
" ORDER BY captured_at_utc ASC, id ASC",
|
|
7180
|
-
(
|
|
7181
|
-
since_dt.date().isoformat(),
|
|
7182
|
-
cw_end.date().isoformat(),
|
|
7183
|
-
),
|
|
7184
|
-
)
|
|
7185
|
-
rows = cur.fetchall()
|
|
7209
|
+
rows = _projects_week_usage_rows(
|
|
7210
|
+
conn, since_dt.date().isoformat(), cw_end.date().isoformat())
|
|
7186
7211
|
except sqlite3.OperationalError:
|
|
7187
7212
|
# No weekly_usage_snapshots table — leaves attributed_pct = None
|
|
7188
7213
|
# throughout (acceptable per spec §2.7).
|
|
@@ -8086,6 +8111,15 @@ class _DiagnosisSelectorError(Exception):
|
|
|
8086
8111
|
|
|
8087
8112
|
|
|
8088
8113
|
# ── /api/debug/backend on-demand cache-state helpers (issue #276, Session A) ──
|
|
8114
|
+
|
|
8115
|
+
def _debug_write_io() -> dict:
|
|
8116
|
+
"""`writeIo` for the debug surfaces (#901 §5.5). A telemetry fault never
|
|
8117
|
+
takes the diagnostic down, and is never reported as a zero."""
|
|
8118
|
+
try:
|
|
8119
|
+
return _lib_write_io.write_io_payload()
|
|
8120
|
+
except Exception: # noqa: BLE001 — diagnostics fail closed
|
|
8121
|
+
return {"status": "unavailable", "reason": "counter_error"}
|
|
8122
|
+
|
|
8089
8123
|
# All read-only, cheap, and privacy-safe: they leak ONLY row counts, signature
|
|
8090
8124
|
# legs (ints/tuples), pending-flag names, and the tool version — never prompt /
|
|
8091
8125
|
# prose / paths. Computed on demand so cache_state is available even with
|
|
@@ -8494,6 +8528,7 @@ _GET_ROUTES = (
|
|
|
8494
8528
|
("exact", "/api/diagnosis", "_handle_get_diagnosis",
|
|
8495
8529
|
("scope", "endpoint.diagnosis"), False),
|
|
8496
8530
|
("exact", "/api/debug/backend", "_handle_get_debug_backend", None, False),
|
|
8531
|
+
("exact", "/api/debug/backend/write-io", "_handle_get_debug_backend_write_io", None, False),
|
|
8497
8532
|
("exact", "/api/conversations/facets", "_handle_get_conversations_facets",
|
|
8498
8533
|
("scope", "endpoint.conversations_facets"), False),
|
|
8499
8534
|
("exact", "/api/conversations", "_handle_get_conversations",
|
|
@@ -9224,11 +9259,26 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
|
|
|
9224
9259
|
# stats failure reported as a cache one sends the user to
|
|
9225
9260
|
# `cctally cache-sync --rebuild`.
|
|
9226
9261
|
"stats_faults": stats_faults,
|
|
9262
|
+
# #901 §5.5: kernel-accounted process writes, the trailing
|
|
9263
|
+
# five-minute statistic and its budget verdict. Additive camelCase.
|
|
9264
|
+
"writeIo": _debug_write_io(),
|
|
9227
9265
|
}
|
|
9228
9266
|
if body["phases"] is None:
|
|
9229
9267
|
body["note"] = "tracing_disabled"
|
|
9230
9268
|
self._respond_json(200, body)
|
|
9231
9269
|
|
|
9270
|
+
def _handle_get_debug_backend_write_io(self) -> None:
|
|
9271
|
+
"""GET ``/api/debug/backend/write-io`` — the ``writeIo`` object alone
|
|
9272
|
+
(#901 §5.6): what ``cctally doctor`` reads from a running dashboard
|
|
9273
|
+
inside its bounded timeout, without ``/api/debug/backend``'s
|
|
9274
|
+
on-demand cache-table counts. Same gates, same order: the bearer
|
|
9275
|
+
(``_require_api_auth``, before dispatch), then the loopback TCP peer
|
|
9276
|
+
and the IP-literal ``Host``. No bypass exists."""
|
|
9277
|
+
if not self._require_debug_backend_allowed():
|
|
9278
|
+
return
|
|
9279
|
+
self._respond_json(200, {"schemaVersion": 1,
|
|
9280
|
+
"writeIo": _debug_write_io()})
|
|
9281
|
+
|
|
9232
9282
|
def _handle_post_debug_backend_trace(self) -> None:
|
|
9233
9283
|
"""POST ``/api/debug/backend/trace`` — arm the deep phase trace (§3.2).
|
|
9234
9284
|
|
|
@@ -12485,6 +12535,13 @@ def cmd_dashboard(args: argparse.Namespace) -> int:
|
|
|
12485
12535
|
**_make_sync_loop_collaborators(ref=ref, hub=hub),
|
|
12486
12536
|
)
|
|
12487
12537
|
|
|
12538
|
+
# #901 §5.6: this process's write-telemetry identity, registered before
|
|
12539
|
+
# the first build so the in-process doctor reads its own snapshot from the
|
|
12540
|
+
# first tick; the bound port replaces the requested one once known.
|
|
12541
|
+
write_instance_id = _lib_write_io.new_instance_id()
|
|
12542
|
+
_lib_write_io.register_local_instance(
|
|
12543
|
+
instance_id=write_instance_id, host=args.host,
|
|
12544
|
+
port=_resolve_dashboard_port(args.port))
|
|
12488
12545
|
sync_thread = (
|
|
12489
12546
|
None if args.no_sync
|
|
12490
12547
|
else _DashboardSyncThread(
|
|
@@ -12566,6 +12623,20 @@ def cmd_dashboard(args: argparse.Namespace) -> int:
|
|
|
12566
12623
|
http_thread = threading.Thread(target=srv.serve_forever, daemon=True,
|
|
12567
12624
|
name="dashboard-http")
|
|
12568
12625
|
http_thread.start()
|
|
12626
|
+
# The owner-readable descriptor `cctally doctor` discovers this instance
|
|
12627
|
+
# through: written once now, removed at clean shutdown. A crash leaves it
|
|
12628
|
+
# for the next startup to remove; doctor ignores a dead or reused pid's.
|
|
12629
|
+
_lib_write_io.register_local_instance(
|
|
12630
|
+
instance_id=write_instance_id, host=bind_host, port=bind_port)
|
|
12631
|
+
write_descriptor_written = False
|
|
12632
|
+
try:
|
|
12633
|
+
_lib_write_io.remove_stale_descriptors(_cctally_core.APP_DIR)
|
|
12634
|
+
_lib_write_io.write_instance_descriptor(
|
|
12635
|
+
_cctally_core.APP_DIR, instance_id=write_instance_id,
|
|
12636
|
+
host=bind_host, port=bind_port)
|
|
12637
|
+
write_descriptor_written = True
|
|
12638
|
+
except OSError as exc:
|
|
12639
|
+
eprint(f"[dashboard] instance descriptor not written ({exc})")
|
|
12569
12640
|
|
|
12570
12641
|
# Surface any pending migration errors at server startup. Browser is a
|
|
12571
12642
|
# separate UI surface (out of scope for this fix); this prints once to
|
|
@@ -12609,6 +12680,13 @@ def cmd_dashboard(args: argparse.Namespace) -> int:
|
|
|
12609
12680
|
finally:
|
|
12610
12681
|
# First, so a shutdown can never be followed by an automatic restart.
|
|
12611
12682
|
_stop_install_watch()
|
|
12683
|
+
if write_descriptor_written:
|
|
12684
|
+
try:
|
|
12685
|
+
_lib_write_io.remove_instance_descriptor(
|
|
12686
|
+
_cctally_core.APP_DIR, write_instance_id)
|
|
12687
|
+
except OSError:
|
|
12688
|
+
pass
|
|
12689
|
+
_lib_write_io.clear_local_instance()
|
|
12612
12690
|
if sync_thread is not None:
|
|
12613
12691
|
sync_thread.stop()
|
|
12614
12692
|
conversation_sync_stop.set()
|
|
@@ -65,6 +65,7 @@ from _lib_dashboard_json import (
|
|
|
65
65
|
encode_dashboard_json_bytes_capped,
|
|
66
66
|
)
|
|
67
67
|
from _lib_retained_size import retained_size_bytes
|
|
68
|
+
import _lib_codex_rollout_container as _rc
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
def _advance_live_tail_accounting(handler, changed, sync, label):
|
|
@@ -443,40 +444,280 @@ def _cached_file_sigs(conn, paths):
|
|
|
443
444
|
return out
|
|
444
445
|
|
|
445
446
|
|
|
447
|
+
#: The cursor columns a Codex live-tail signature is computed from (#968 5.9).
|
|
448
|
+
_CODEX_WATCH_SIG_COLUMNS = (
|
|
449
|
+
"path, size_bytes, container_kind, container_size_bytes, mtime_ns, "
|
|
450
|
+
"device_id, inode, pending_container_fingerprint")
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _codex_watch_kernel():
|
|
454
|
+
return sys.modules["cctally"]._load_sibling("_lib_codex_conversation_watch")
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _codex_acknowledged_sig(row):
|
|
458
|
+
"""The live-tail signature one ``codex_conversation_source_files`` row
|
|
459
|
+
acknowledges (#968 spec 5.9).
|
|
460
|
+
|
|
461
|
+
A container already recorded as pending (or unverified) work is
|
|
462
|
+
acknowledged at its recorded fingerprint: the verification executor owns
|
|
463
|
+
it, and rescheduling it on every poll is the non-convergence I9 forbids.
|
|
464
|
+
Otherwise the accepted container's signature: ``("plain", size)`` for
|
|
465
|
+
plain, which is today's committed size, or the container identity for a
|
|
466
|
+
zstd container.
|
|
467
|
+
"""
|
|
468
|
+
_path, size, kind, csize, mtime_ns, device, inode, pending = row
|
|
469
|
+
if pending:
|
|
470
|
+
fingerprint = _rc.ContainerFingerprint.from_json(pending)
|
|
471
|
+
if fingerprint is not None:
|
|
472
|
+
return fingerprint.watch_sig()
|
|
473
|
+
return _codex_watch_kernel().committed_watch_sig(
|
|
474
|
+
kind, size, csize, mtime_ns, device, inode)
|
|
475
|
+
|
|
476
|
+
|
|
446
477
|
def _codex_cached_file_sigs(conn, paths):
|
|
447
|
-
"""{path:
|
|
448
|
-
Codex analogue of ``_cached_file_sigs`` (spec
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
Codex live-tail watch against the cache's own committed
|
|
452
|
-
during an ingest is re-detected next cycle (the per-path
|
|
453
|
-
invariant — the S6 physical mutation sequence is at most
|
|
454
|
-
certificate, never this cursor's replacement)."""
|
|
478
|
+
"""{path: acknowledged signature} from ``codex_conversation_source_files``
|
|
479
|
+
for the given paths — the Codex analogue of ``_cached_file_sigs`` (spec
|
|
480
|
+
§5.2), in the container-aware shape the Codex observation seam returns
|
|
481
|
+
(#968 spec 5.9). Paths with no row are absent → treated as changed.
|
|
482
|
+
Baselines the Codex live-tail watch against the cache's own committed
|
|
483
|
+
cursor so growth during an ingest is re-detected next cycle (the per-path
|
|
484
|
+
committed-cursor invariant — the S6 physical mutation sequence is at most
|
|
485
|
+
an extra certificate, never this cursor's replacement)."""
|
|
455
486
|
out = {}
|
|
456
487
|
if not paths:
|
|
457
488
|
return out
|
|
458
489
|
placeholders = ",".join("?" for _ in paths)
|
|
459
490
|
try:
|
|
460
491
|
rows = conn.execute(
|
|
461
|
-
f"SELECT
|
|
492
|
+
f"SELECT {_CODEX_WATCH_SIG_COLUMNS} "
|
|
493
|
+
f"FROM codex_conversation_source_files "
|
|
462
494
|
f"WHERE path IN ({placeholders})", list(paths)).fetchall()
|
|
463
495
|
except sqlite3.OperationalError:
|
|
464
496
|
return out
|
|
465
|
-
for
|
|
466
|
-
out[
|
|
497
|
+
for row in rows:
|
|
498
|
+
out[row[0]] = _codex_acknowledged_sig(row)
|
|
467
499
|
return out
|
|
468
500
|
|
|
469
501
|
|
|
470
502
|
def _codex_all_committed_sizes(conn):
|
|
471
|
-
"""{path:
|
|
472
|
-
both the ``known_paths`` diff set and the ``committed_sizes``
|
|
473
|
-
baseline
|
|
503
|
+
"""{path: acknowledged signature} for EVERY tracked Codex file — fed to the
|
|
504
|
+
frontier as both the ``known_paths`` diff set and the ``committed_sizes``
|
|
505
|
+
growth baseline, which the kernel compares with ``codex_sig_advanced``
|
|
506
|
+
(#968 spec 5.9). A plain SELECT (no lock), so it never widens the SSE lock
|
|
507
|
+
scope."""
|
|
474
508
|
try:
|
|
475
509
|
rows = conn.execute(
|
|
476
|
-
"SELECT
|
|
510
|
+
f"SELECT {_CODEX_WATCH_SIG_COLUMNS} "
|
|
511
|
+
"FROM codex_conversation_source_files").fetchall()
|
|
477
512
|
except sqlite3.OperationalError:
|
|
478
513
|
return {}
|
|
479
|
-
return {
|
|
514
|
+
return {row[0]: _codex_acknowledged_sig(row) for row in rows}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _codex_seal_token(conn):
|
|
518
|
+
"""The publication barrier's seal token (Task 11's ``SEAL_SEQ_KEY``), read
|
|
519
|
+
through the attached cache; ``None`` when unreadable, which never re-arms
|
|
520
|
+
anything."""
|
|
521
|
+
barrier = sys.modules["cctally"]._load_sibling(
|
|
522
|
+
"_lib_codex_publication_barrier")
|
|
523
|
+
try:
|
|
524
|
+
row = conn.execute(
|
|
525
|
+
"SELECT value FROM cache_db.cache_meta WHERE key=?",
|
|
526
|
+
(barrier.SEAL_SEQ_KEY,)).fetchone()
|
|
527
|
+
except sqlite3.Error:
|
|
528
|
+
return None
|
|
529
|
+
return None if row is None else row[0]
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@dataclasses.dataclass
|
|
533
|
+
class _CodexWatchIngest:
|
|
534
|
+
"""Stream-local observations, separate from the durable ingest result.
|
|
535
|
+
|
|
536
|
+
``stats.targeted_clean`` remains authoritative for ingest completion. A
|
|
537
|
+
held container can acknowledge its observed signature without certifying
|
|
538
|
+
any accounting or transcript work as complete (T12-INTEG01).
|
|
539
|
+
"""
|
|
540
|
+
|
|
541
|
+
stats: object
|
|
542
|
+
accounting_blocked: bool = False
|
|
543
|
+
acknowledged: dict = dataclasses.field(default_factory=dict)
|
|
544
|
+
observation_visible: bool = False
|
|
545
|
+
|
|
546
|
+
def __getattr__(self, name):
|
|
547
|
+
return getattr(self.stats, name)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _codex_observation_hold_only(stats, *, executor_owned=False):
|
|
551
|
+
"""Recognize container holds without overlooking an actual ingest loss.
|
|
552
|
+
|
|
553
|
+
This is only admission to a stream-local observation acknowledgment,
|
|
554
|
+
never a replacement for ``targeted_clean`` or a frontier certificate.
|
|
555
|
+
Unknown skipped reasons and every failed/contended ingest stay dirty.
|
|
556
|
+
"""
|
|
557
|
+
if any(getattr(stats, name, False) for name in (
|
|
558
|
+
"lock_contended", "deferred_reason", "files_failed",
|
|
559
|
+
"files_deferred_torn", "files_deferred_reset", "maintenance_failed",
|
|
560
|
+
"budget_exhausted", "drain_yielded", "rebuild_refused")):
|
|
561
|
+
return False
|
|
562
|
+
if getattr(stats, "barrier_retry", False) and not executor_owned:
|
|
563
|
+
return False
|
|
564
|
+
skipped = getattr(stats, "container_skipped", {})
|
|
565
|
+
return (bool(getattr(stats, "container_pending", 0) or any(skipped.values()))
|
|
566
|
+
and set(skipped) <= {
|
|
567
|
+
"decoder-unavailable", "decode-error", "read-error",
|
|
568
|
+
"unstable-stat", "verification-pending", "verification-unverified"})
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _codex_watch_probes(conn):
|
|
572
|
+
"""Container observations and committed cursors for both Codex polls.
|
|
573
|
+
|
|
574
|
+
Pending fingerprints acknowledge executor-owned work. A container held
|
|
575
|
+
without a recorded fingerprint acknowledges the pre-ingest observation,
|
|
576
|
+
so replacement during ingest is still detected. These are observation
|
|
577
|
+
acknowledgments only: durable pending work remains dirty.
|
|
578
|
+
|
|
579
|
+
A scoped accounting hold cannot run transcript ingest. It acknowledges
|
|
580
|
+
the observation quietly and retries once accounting accepts that exact
|
|
581
|
+
container. Failed or contended accounting remains unacknowledged. Plain
|
|
582
|
+
barrier-deferred files wait at their pre-ingest observation until a real
|
|
583
|
+
file change or a readable new seal token re-arms them once.
|
|
584
|
+
"""
|
|
585
|
+
observed: dict = {}
|
|
586
|
+
deferred: dict = {} # path -> (pre-ingest signature, seal token)
|
|
587
|
+
accounting_wait: dict = {} # path -> observed container awaiting accounting
|
|
588
|
+
rearmed: set = set()
|
|
589
|
+
|
|
590
|
+
def accounting_accepted(path, sig):
|
|
591
|
+
try:
|
|
592
|
+
row = conn.execute(
|
|
593
|
+
f"SELECT {_CODEX_WATCH_SIG_COLUMNS} "
|
|
594
|
+
"FROM cache_db.codex_session_files "
|
|
595
|
+
"WHERE path=? AND container_state='accepted'", (path,)).fetchone()
|
|
596
|
+
except sqlite3.Error:
|
|
597
|
+
return False
|
|
598
|
+
return row is not None and _codex_acknowledged_sig(row) == sig
|
|
599
|
+
|
|
600
|
+
def executor_owns_observations(paths):
|
|
601
|
+
# Publication-unit planning can encounter the same pending container
|
|
602
|
+
# as phase one and set barrier_retry. Acknowledge only when EVERY
|
|
603
|
+
# changed path has durable executor ownership of the exact observation;
|
|
604
|
+
# a failed plain/accepted contributor must keep the whole poll retrying.
|
|
605
|
+
if not paths:
|
|
606
|
+
return False
|
|
607
|
+
marks = ",".join("?" for _ in paths)
|
|
608
|
+
try:
|
|
609
|
+
rows = conn.execute(
|
|
610
|
+
"SELECT path,pending_container_fingerprint "
|
|
611
|
+
"FROM codex_conversation_source_files "
|
|
612
|
+
f"WHERE path IN ({marks}) AND container_state='pending'",
|
|
613
|
+
list(paths)).fetchall()
|
|
614
|
+
except sqlite3.Error:
|
|
615
|
+
return False
|
|
616
|
+
if len(rows) != len(paths):
|
|
617
|
+
return False
|
|
618
|
+
for path, pending in rows:
|
|
619
|
+
fingerprint = _rc.ContainerFingerprint.from_json(pending)
|
|
620
|
+
before = observed.get(path)
|
|
621
|
+
if (fingerprint is None or before is None
|
|
622
|
+
or before[0] != _rc.ZSTD
|
|
623
|
+
or fingerprint.watch_sig() != before):
|
|
624
|
+
return False
|
|
625
|
+
return True
|
|
626
|
+
|
|
627
|
+
def stat_fn(path):
|
|
628
|
+
sig = _rc.observe_watch_sig(path)
|
|
629
|
+
observed[path] = sig
|
|
630
|
+
waiting = accounting_wait.get(path)
|
|
631
|
+
if waiting is not None:
|
|
632
|
+
if sig != waiting:
|
|
633
|
+
del accounting_wait[path]
|
|
634
|
+
elif accounting_accepted(path, sig):
|
|
635
|
+
del accounting_wait[path]
|
|
636
|
+
rearmed.add(path)
|
|
637
|
+
held = deferred.get(path)
|
|
638
|
+
if held is not None:
|
|
639
|
+
token = _codex_seal_token(conn)
|
|
640
|
+
if sig != held[0] or (token is not None and token != held[1]):
|
|
641
|
+
del deferred[path]
|
|
642
|
+
if sig == held[0]:
|
|
643
|
+
rearmed.add(path)
|
|
644
|
+
if path in rearmed and sig is not None:
|
|
645
|
+
return ("rearm",) + tuple(sig)
|
|
646
|
+
return sig
|
|
647
|
+
|
|
648
|
+
def committed_sig(path):
|
|
649
|
+
rearmed.discard(path) # reached only after a clean ingest
|
|
650
|
+
held = deferred.get(path)
|
|
651
|
+
if held is not None:
|
|
652
|
+
return held[0]
|
|
653
|
+
acknowledged = _codex_cached_file_sigs(conn, [path]).get(path)
|
|
654
|
+
before = observed.get(path)
|
|
655
|
+
if (before is not None and before[0] == _rc.ZSTD
|
|
656
|
+
and acknowledged != before):
|
|
657
|
+
return before
|
|
658
|
+
return acknowledged
|
|
659
|
+
|
|
660
|
+
def settle_ingest(ingest):
|
|
661
|
+
def settled(changed):
|
|
662
|
+
token = _codex_seal_token(conn)
|
|
663
|
+
result = ingest(changed)
|
|
664
|
+
if isinstance(result, _CodexWatchIngest):
|
|
665
|
+
stats = result.stats
|
|
666
|
+
else:
|
|
667
|
+
stats = result
|
|
668
|
+
result = _CodexWatchIngest(stats)
|
|
669
|
+
clean = getattr(stats, "targeted_clean", False)
|
|
670
|
+
executor_owned = (
|
|
671
|
+
getattr(stats, "barrier_retry", False)
|
|
672
|
+
and not result.accounting_blocked
|
|
673
|
+
and not getattr(stats, "barrier_files_deferred", 0)
|
|
674
|
+
and executor_owns_observations(changed))
|
|
675
|
+
held_only = _codex_observation_hold_only(
|
|
676
|
+
stats, executor_owned=executor_owned)
|
|
677
|
+
if not clean and not held_only:
|
|
678
|
+
return result
|
|
679
|
+
acknowledged = _codex_cached_file_sigs(conn, list(changed))
|
|
680
|
+
plain_deferred = 0
|
|
681
|
+
for path in changed:
|
|
682
|
+
before = observed.get(path)
|
|
683
|
+
if before is None:
|
|
684
|
+
continue
|
|
685
|
+
if before[0] == _rc.ZSTD and held_only:
|
|
686
|
+
# The pass actually observed this container, even if the
|
|
687
|
+
# verification executor still owes its durable work.
|
|
688
|
+
result.acknowledged[path] = before
|
|
689
|
+
rearmed.discard(path)
|
|
690
|
+
if result.accounting_blocked:
|
|
691
|
+
accounting_wait[path] = before
|
|
692
|
+
elif (not result.accounting_blocked
|
|
693
|
+
and getattr(stats, "barrier_files_deferred", 0)
|
|
694
|
+
and before[0] != _rc.ZSTD
|
|
695
|
+
and _codex_watch_kernel().codex_sig_advanced(
|
|
696
|
+
before, acknowledged.get(path))):
|
|
697
|
+
# Phase-one deferrals can publish in phase two. Only a
|
|
698
|
+
# committed cursor behind the probe still owes this work;
|
|
699
|
+
# ingestion may already have consumed a later append.
|
|
700
|
+
deferred[path] = (before, token)
|
|
701
|
+
result.acknowledged[path] = before
|
|
702
|
+
plain_deferred += 1
|
|
703
|
+
elif not result.accounting_blocked and not clean:
|
|
704
|
+
# A mixed pass may have committed a plain contributor
|
|
705
|
+
# while holding a container. Keep its committed cursor.
|
|
706
|
+
sig = acknowledged.get(path)
|
|
707
|
+
if sig is not None:
|
|
708
|
+
result.acknowledged[path] = sig
|
|
709
|
+
if (plain_deferred == len(changed)
|
|
710
|
+
and not getattr(stats, "targeted_visible", False)):
|
|
711
|
+
stats.targeted_visible = False
|
|
712
|
+
result.observation_visible = (
|
|
713
|
+
not clean and bool(result.acknowledged)
|
|
714
|
+
and not result.accounting_blocked
|
|
715
|
+
and not getattr(stats, "barrier_retry", False)
|
|
716
|
+
and bool(getattr(stats, "targeted_visible", True)))
|
|
717
|
+
return result
|
|
718
|
+
return settled
|
|
719
|
+
|
|
720
|
+
return stat_fn, committed_sig, settle_ingest
|
|
480
721
|
|
|
481
722
|
|
|
482
723
|
def _codex_classified_paths(conn, paths):
|
|
@@ -1220,7 +1461,7 @@ def _send_sse_headers(handler) -> None:
|
|
|
1220
1461
|
|
|
1221
1462
|
def _run_conversation_events_stream(
|
|
1222
1463
|
handler, conn, *, passive, tail_data, resolve, ingest, cached_sigs,
|
|
1223
|
-
discovery_step=None,
|
|
1464
|
+
discovery_step=None, stat_fn=None, committed_sig=None,
|
|
1224
1465
|
) -> None:
|
|
1225
1466
|
"""Shared per-conversation live-tail SSE loop (spec §5.2), driving both the
|
|
1226
1467
|
bare (``sessionId``) and qualified (``conversationKey``) streams from ONE
|
|
@@ -1236,9 +1477,18 @@ def _run_conversation_events_stream(
|
|
|
1236
1477
|
~10-cycle Codex child-discovery frontier (``None`` for Claude — a bare or
|
|
1237
1478
|
qualified-Claude stream never runs it). The cache lock is held ONLY inside
|
|
1238
1479
|
``ingest`` / ``discovery_step``, never across a sleep (the #297 WAL
|
|
1239
|
-
discipline).
|
|
1480
|
+
discipline).
|
|
1481
|
+
|
|
1482
|
+
``stat_fn`` / ``committed_sig`` are the provider's observation seam
|
|
1483
|
+
(#968 spec 5.9): Codex passes ``_codex_watch_probes`` so both poll sites
|
|
1484
|
+
compare one container-aware signature shape; ``None`` keeps the kernel's
|
|
1485
|
+
literal-path size probe and the committed-cursor lookup through
|
|
1486
|
+
``cached_sigs`` (Claude, unchanged).
|
|
1487
|
+
"""
|
|
1240
1488
|
import time as _time
|
|
1241
1489
|
watch = sys.modules["cctally"]._load_sibling("_lib_conversation_watch")
|
|
1490
|
+
probe = stat_fn or watch.file_sig
|
|
1491
|
+
committed = committed_sig or (lambda p: cached_sigs([p]).get(p))
|
|
1242
1492
|
tail_frame = (
|
|
1243
1493
|
"event: tail\ndata: " + encode_dashboard_json(tail_data) + "\n\n"
|
|
1244
1494
|
).encode("utf-8")
|
|
@@ -1286,12 +1536,26 @@ def _run_conversation_events_stream(
|
|
|
1286
1536
|
if _request_peer_gone(handler, timeout=_LIVE_TAIL_POLL_INTERVAL):
|
|
1287
1537
|
return
|
|
1288
1538
|
cycles += 1
|
|
1289
|
-
changed = watch.changed_paths(files, seen)
|
|
1539
|
+
changed = watch.changed_paths(files, seen, probe)
|
|
1290
1540
|
if changed:
|
|
1291
1541
|
_time.sleep(_LIVE_TAIL_DEBOUNCE)
|
|
1542
|
+
observation = None
|
|
1543
|
+
|
|
1544
|
+
def poll_ingest(changed):
|
|
1545
|
+
nonlocal observation
|
|
1546
|
+
observation = ingest(changed)
|
|
1547
|
+
return observation
|
|
1548
|
+
|
|
1292
1549
|
new_seen, emitted = watch.watch_step(
|
|
1293
|
-
files, seen, ingest_fn=
|
|
1294
|
-
committed_sig_fn=
|
|
1550
|
+
files, seen, stat_fn=probe, ingest_fn=poll_ingest,
|
|
1551
|
+
committed_sig_fn=committed)
|
|
1552
|
+
if isinstance(observation, _CodexWatchIngest):
|
|
1553
|
+
# A held container was observed, not successfully ingested.
|
|
1554
|
+
# Keep that distinction out of the generic watch kernel and
|
|
1555
|
+
# out of every durable cursor/certification decision.
|
|
1556
|
+
new_seen = dict(new_seen)
|
|
1557
|
+
new_seen.update(observation.acknowledged)
|
|
1558
|
+
emitted = emitted or observation.observation_visible
|
|
1295
1559
|
seen = new_seen
|
|
1296
1560
|
if emitted:
|
|
1297
1561
|
handler.wfile.write(tail_frame)
|
|
@@ -1643,7 +1907,8 @@ def _qualified_conversation_events(
|
|
|
1643
1907
|
finally:
|
|
1644
1908
|
accounting.close()
|
|
1645
1909
|
if not accounting_stats.targeted_clean:
|
|
1646
|
-
return
|
|
1910
|
+
return _CodexWatchIngest(
|
|
1911
|
+
accounting_stats, accounting_blocked=True)
|
|
1647
1912
|
writer = sys.modules["_cctally_dashboard"].open_conversations_db()
|
|
1648
1913
|
try:
|
|
1649
1914
|
stats = sync_codex_conversations(writer, only_paths=set(changed))
|
|
@@ -1658,6 +1923,8 @@ def _qualified_conversation_events(
|
|
|
1658
1923
|
return stats
|
|
1659
1924
|
|
|
1660
1925
|
cached = lambda paths: _codex_cached_file_sigs(conn, paths)
|
|
1926
|
+
stat_fn, committed_sig, settle_ingest = _codex_watch_probes(conn)
|
|
1927
|
+
_ingest = settle_ingest(_ingest)
|
|
1661
1928
|
discovery = (
|
|
1662
1929
|
None if account_key is not None
|
|
1663
1930
|
else _make_codex_discovery_step(handler, conn, key, cq_codex)
|
|
@@ -1694,6 +1961,7 @@ def _qualified_conversation_events(
|
|
|
1694
1961
|
return stats
|
|
1695
1962
|
|
|
1696
1963
|
cached = lambda paths: _cached_file_sigs(conn, paths)
|
|
1964
|
+
stat_fn = committed_sig = None
|
|
1697
1965
|
discovery = None
|
|
1698
1966
|
|
|
1699
1967
|
try:
|
|
@@ -1701,7 +1969,8 @@ def _qualified_conversation_events(
|
|
|
1701
1969
|
handler, conn, passive=passive,
|
|
1702
1970
|
tail_data={"conversationKey": key},
|
|
1703
1971
|
resolve=_resolve, ingest=_ingest, cached_sigs=cached,
|
|
1704
|
-
discovery_step=discovery
|
|
1972
|
+
discovery_step=discovery, stat_fn=stat_fn,
|
|
1973
|
+
committed_sig=committed_sig)
|
|
1705
1974
|
except (BrokenPipeError, ConnectionResetError,
|
|
1706
1975
|
ConnectionAbortedError, socket.timeout):
|
|
1707
1976
|
pass
|
|
@@ -96,6 +96,7 @@ from _cctally_dashboard_cache_report import _cache_report_snapshot_to_dict
|
|
|
96
96
|
# copy was already exactly this — collapse to the single definition).
|
|
97
97
|
_ensure_sibling_loaded("_lib_json_envelope")
|
|
98
98
|
from _lib_json_envelope import _iso_z
|
|
99
|
+
from _lib_five_hour import latest_five_hour_reading
|
|
99
100
|
|
|
100
101
|
|
|
101
102
|
def _model_breakdowns_to_models(model_breakdowns: list[dict[str, Any]],
|
|
@@ -235,9 +236,17 @@ def _select_current_block_for_envelope(
|
|
|
235
236
|
# statement of that key. Its `week_start_at` is a byte copy of the
|
|
236
237
|
# non-held row it was derived from, so scoping the post-reset lookup
|
|
237
238
|
# below by it is unaffected.
|
|
239
|
+
#
|
|
240
|
+
# #825/#832 spec 4.7 revision 7: the current window is the newest five-hour
|
|
241
|
+
# READING's, through the one shared selection under the same as-of and
|
|
242
|
+
# account restrictions. A retired replica (its five-hour fields cleared in
|
|
243
|
+
# place by a five-hour credit) or a weekly-only reading can be the newest
|
|
244
|
+
# row and names no window. The newest row still supplies `week_start_at`
|
|
245
|
+
# for the post-reset lookup below: that is weekly evidence, selected
|
|
246
|
+
# independently.
|
|
238
247
|
snap = conn.execute(
|
|
239
248
|
"""
|
|
240
|
-
SELECT
|
|
249
|
+
SELECT week_start_at
|
|
241
250
|
FROM weekly_usage_snapshots
|
|
242
251
|
WHERE captured_at_utc <= ?
|
|
243
252
|
AND (? IS NULL OR account_key = ?)
|
|
@@ -246,7 +255,11 @@ def _select_current_block_for_envelope(
|
|
|
246
255
|
""",
|
|
247
256
|
(_iso_z(now_utc), account_key, account_key),
|
|
248
257
|
).fetchone()
|
|
249
|
-
if snap is None
|
|
258
|
+
if snap is None:
|
|
259
|
+
return None
|
|
260
|
+
reading = latest_five_hour_reading(
|
|
261
|
+
conn, account_key=account_key, as_of_utc=_iso_z(now_utc))
|
|
262
|
+
if reading is None:
|
|
250
263
|
return None
|
|
251
264
|
|
|
252
265
|
# The account supplied by `_tui_build_current_week` is the owner of the
|
|
@@ -298,7 +311,7 @@ def _select_current_block_for_envelope(
|
|
|
298
311
|
ORDER BY id ASC
|
|
299
312
|
LIMIT 1
|
|
300
313
|
""",
|
|
301
|
-
(
|
|
314
|
+
(reading.window_key, account_key, account_key,
|
|
302
315
|
_iso_z(now_utc)),
|
|
303
316
|
).fetchone()
|
|
304
317
|
if block is None:
|