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
|
@@ -31,13 +31,14 @@ from __future__ import annotations
|
|
|
31
31
|
import json
|
|
32
32
|
import fcntl
|
|
33
33
|
import os
|
|
34
|
+
import _lib_codex_rollout_container as _rc
|
|
34
35
|
import secrets
|
|
35
36
|
import sqlite3
|
|
36
37
|
import threading
|
|
37
38
|
import pathlib
|
|
38
39
|
import shlex
|
|
39
40
|
import time
|
|
40
|
-
from dataclasses import dataclass
|
|
41
|
+
from dataclasses import dataclass, replace
|
|
41
42
|
from typing import NamedTuple
|
|
42
43
|
from _lib_retained_size import retained_size_bytes
|
|
43
44
|
|
|
@@ -73,6 +74,160 @@ _COMPLETE_KEYS = {
|
|
|
73
74
|
"claude": "claude_ingest_walk_complete",
|
|
74
75
|
"codex": CODEX_FULL_WALK_COMPLETE_KEY,
|
|
75
76
|
}
|
|
77
|
+
#: Review N3. The durable debt-only tokens: cache.db for the accounting
|
|
78
|
+
#: frontier, conversations.db for the conversation frontier. A full walk
|
|
79
|
+
#: clean except for pending prune debt writes a fresh one; any other
|
|
80
|
+
#: unbudgeted or certifying full walk deletes it.
|
|
81
|
+
CODEX_FULL_WALK_DEBT_ONLY_KEY = "dashboard_codex_full_walk_debt_only"
|
|
82
|
+
CODEX_CONVERSATION_WALK_DEBT_ONLY_KEY = "dashboard_codex_conversation_walk_debt_only"
|
|
83
|
+
#: How long a frontier holds a debt-only store before walking it again: the
|
|
84
|
+
#: certificate's own staleness bound, so a store whose certificate is
|
|
85
|
+
#: withheld (spec 5.7) is never staler than a certified one.
|
|
86
|
+
FRONTIER_DEBT_HOLD_SECONDS = FRONTIER_CERTIFICATE_MAX_AGE_SECONDS
|
|
87
|
+
_DEBT_HOLD = {
|
|
88
|
+
"accounting": (CODEX_FULL_WALK_DEBT_ONLY_KEY, "codex_prune_debt"),
|
|
89
|
+
"conversation": (CODEX_CONVERSATION_WALK_DEBT_ONLY_KEY,
|
|
90
|
+
"codex_conversation_prune_debt"),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _DebtCleared:
|
|
95
|
+
"""``stats`` as if no prune debt were pending (review N3)."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, stats):
|
|
98
|
+
self._stats = stats
|
|
99
|
+
|
|
100
|
+
def __getattr__(self, name):
|
|
101
|
+
if name == "prune_debt_pending":
|
|
102
|
+
return 0
|
|
103
|
+
if name == "full_walk_complete":
|
|
104
|
+
return bool(getattr(self._stats, "full_walk_debt_only", False)
|
|
105
|
+
or getattr(self._stats, "full_walk_complete", False))
|
|
106
|
+
return getattr(self._stats, name)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def debt_only_refusal(predicate, mode: str, stats, **kwargs) -> bool:
|
|
110
|
+
"""True when pending prune debt is the ONLY reason ``predicate`` refuses
|
|
111
|
+
this result: the walk observed everything else clean (review N3). The
|
|
112
|
+
frontier's own predicate decides, so the walks and the planner can never
|
|
113
|
+
disagree about what "clean" means."""
|
|
114
|
+
if stats is None or not getattr(stats, "prune_debt_pending", 0):
|
|
115
|
+
return False
|
|
116
|
+
return (not predicate(mode, stats, **kwargs)
|
|
117
|
+
and bool(predicate(mode, _DebtCleared(stats), **kwargs)))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _debt_only_token(owner, conn) -> "str | None":
|
|
121
|
+
key, _table = _DEBT_HOLD[owner.CONSUMER]
|
|
122
|
+
try:
|
|
123
|
+
row = conn.execute(
|
|
124
|
+
"SELECT value FROM main.cache_meta WHERE key=?", (key,)).fetchone()
|
|
125
|
+
except sqlite3.Error:
|
|
126
|
+
return None
|
|
127
|
+
return None if row is None else str(row[0])
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _prune_debt_present(owner, conn) -> "bool | None":
|
|
131
|
+
_key, table = _DEBT_HOLD[owner.CONSUMER]
|
|
132
|
+
try:
|
|
133
|
+
return conn.execute(
|
|
134
|
+
f"SELECT 1 FROM main.{table} LIMIT 1").fetchone() is not None
|
|
135
|
+
except sqlite3.Error:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _marker_extent(owner) -> "tuple[int, int, int] | None":
|
|
140
|
+
"""The activity marker's (device, inode, size), or None when absent."""
|
|
141
|
+
try:
|
|
142
|
+
st = os.stat(activity_marker_path(owner.app_dir))
|
|
143
|
+
except OSError:
|
|
144
|
+
return None
|
|
145
|
+
return (st.st_dev, st.st_ino, st.st_size)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _codex_activity_since(owner, extent) -> bool:
|
|
149
|
+
"""Whether a Codex ticket reached the activity marker after ``extent``.
|
|
150
|
+
|
|
151
|
+
Review r3 F2: the Claude hook and the Codex hook append to one shared
|
|
152
|
+
marker (`record_activity`), so a moved extent alone would end the
|
|
153
|
+
conversation hold on any Claude Code activity. A ``codex`` record
|
|
154
|
+
appended after ``extent`` ends it, and so does a replaced marker (a
|
|
155
|
+
rotation or a recreated pathname, whose old tail this reader can no
|
|
156
|
+
longer see) or a removed one (`invalidate_activity_marker` runs after a
|
|
157
|
+
writer could not append its ticket). A marker that was absent at the plan
|
|
158
|
+
is read from its start. A slice that cannot be read or decoded ends the
|
|
159
|
+
hold too: failing open costs one walk, failing closed could hide a Codex
|
|
160
|
+
ticket for the rest of the hold. The slice holds at most one hold period
|
|
161
|
+
of tickets, because the hold ends FRONTIER_DEBT_HOLD_SECONDS after the
|
|
162
|
+
plan that recorded ``extent``.
|
|
163
|
+
"""
|
|
164
|
+
if _marker_extent(owner) == extent:
|
|
165
|
+
return False
|
|
166
|
+
try:
|
|
167
|
+
with activity_marker_path(owner.app_dir).open("rb") as fh:
|
|
168
|
+
st = os.fstat(fh.fileno())
|
|
169
|
+
if extent is None:
|
|
170
|
+
start = 0
|
|
171
|
+
elif ((st.st_dev, st.st_ino) != extent[:2]
|
|
172
|
+
or st.st_size < extent[2]):
|
|
173
|
+
return True # replaced
|
|
174
|
+
else:
|
|
175
|
+
start = extent[2]
|
|
176
|
+
fh.seek(start)
|
|
177
|
+
raw = fh.read()
|
|
178
|
+
except OSError:
|
|
179
|
+
return True # removed or unreadable
|
|
180
|
+
complete = raw[:raw.rfind(b"\n") + 1] # a writer may be mid-append
|
|
181
|
+
try:
|
|
182
|
+
records = _decode_marker_records(complete)
|
|
183
|
+
except ValueError:
|
|
184
|
+
return True
|
|
185
|
+
return any(record[0] == "codex" for record in records)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _debt_hold_plan(owner, provider: str, conn) -> "FrontierPlan | None":
|
|
189
|
+
"""Hold instead of re-walking a debt-only Codex store (spec 5.7; N3).
|
|
190
|
+
|
|
191
|
+
The certificate stays withheld while prune debt is pending, so without
|
|
192
|
+
this every tick re-planned a full walk until the drain finished: a ~7 s
|
|
193
|
+
median walk on the production estate, the unconditional per-tick walk
|
|
194
|
+
FRONTIER_CERTIFICATE_MAX_AGE_SECONDS' note measured at ~44 % of a core.
|
|
195
|
+
The hold answers ``caught_up`` (reason ``debt_hold``) while this owner's
|
|
196
|
+
last full plan is younger than FRONTIER_DEBT_HOLD_SECONDS, a full walk
|
|
197
|
+
has written a debt-only token since that plan, and debt is still
|
|
198
|
+
pending. It drops any certificate this
|
|
199
|
+
owner holds (none may stand while debt is pending), and an empty debt
|
|
200
|
+
table ends it at once, so the walk that certifies the store follows the
|
|
201
|
+
drain immediately. The conversation frontier's hold also ends when a
|
|
202
|
+
Codex ticket reached the activity marker since that plan, or the marker
|
|
203
|
+
was replaced or removed (`_codex_activity_since`; a Claude ticket does
|
|
204
|
+
not end it, review r3 F2): no hook writes conversations.db, so Codex
|
|
205
|
+
hook activity is transcript content only a walk can publish. The
|
|
206
|
+
accounting frontier's hold does not read tickets at all; the bounded
|
|
207
|
+
delay that costs is a stated residual (review r3 F3).
|
|
208
|
+
"""
|
|
209
|
+
if provider != "codex":
|
|
210
|
+
return None
|
|
211
|
+
last = getattr(owner, "_debt_walks", {}).get(provider)
|
|
212
|
+
if last is None or _now() - last[1] >= FRONTIER_DEBT_HOLD_SECONDS:
|
|
213
|
+
return None
|
|
214
|
+
state = owner._states.get(provider)
|
|
215
|
+
if state is not None and state.seeded_at >= last[1]:
|
|
216
|
+
# The last full walk certified, and a certifying walk deletes the
|
|
217
|
+
# token: the steady state answers here, with no database read.
|
|
218
|
+
return None
|
|
219
|
+
token = _debt_only_token(owner, conn)
|
|
220
|
+
if token is None or token == last[0]:
|
|
221
|
+
return None
|
|
222
|
+
if _prune_debt_present(owner, conn) is not True:
|
|
223
|
+
return None
|
|
224
|
+
if (owner.CONSUMER == "conversation"
|
|
225
|
+
and _codex_activity_since(owner, last[2])):
|
|
226
|
+
return None
|
|
227
|
+
owner._generations.drop(provider, owner.CONSUMER)
|
|
228
|
+
owner._states.pop(provider, None)
|
|
229
|
+
return FrontierPlan(provider, "caught_up", reason="debt_hold")
|
|
230
|
+
|
|
76
231
|
_PENDING_META_KEYS = (
|
|
77
232
|
"conversation_backfill_pending",
|
|
78
233
|
"conversation_sessions_backfill_pending",
|
|
@@ -263,11 +418,127 @@ def classify_ledger_continuity(
|
|
|
263
418
|
|
|
264
419
|
|
|
265
420
|
class ObservedFile(NamedTuple):
|
|
266
|
-
"""One restat of a tracked rollout: what is at that pathname right now.
|
|
421
|
+
"""One restat of a tracked rollout: what is at that pathname right now.
|
|
422
|
+
|
|
423
|
+
#968 spec 5.9: ``size`` is in DECODED space. A compressed container's byte
|
|
424
|
+
length is never compared with a decoded cursor: an accepted compressed
|
|
425
|
+
container reports the cursor's validated decoded size, and any other
|
|
426
|
+
compressed observation is pending container work (``container_pending``),
|
|
427
|
+
``container_known`` when a verification already holds that exact
|
|
428
|
+
container.
|
|
429
|
+
"""
|
|
267
430
|
|
|
268
431
|
size: int
|
|
269
432
|
device: int
|
|
270
433
|
inode: int
|
|
434
|
+
container_pending: bool = False
|
|
435
|
+
container_known: bool = False
|
|
436
|
+
kind: str = "plain"
|
|
437
|
+
mtime_ns: "int | None" = None
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
class ContainerEvidence(NamedTuple):
|
|
441
|
+
"""The container a cursor row last accepted, and what it holds pending."""
|
|
442
|
+
|
|
443
|
+
kind: str
|
|
444
|
+
container_size: "int | None"
|
|
445
|
+
decoded_size: "int | None"
|
|
446
|
+
mtime_ns: "int | None"
|
|
447
|
+
device: "int | None"
|
|
448
|
+
inode: "int | None"
|
|
449
|
+
state: str
|
|
450
|
+
pending_fingerprint: "str | None"
|
|
451
|
+
|
|
452
|
+
def _same_container(self, kind: str, st) -> bool:
|
|
453
|
+
if kind != self.kind or source_identity_replaced(
|
|
454
|
+
self.device, self.inode, st.st_dev, st.st_ino):
|
|
455
|
+
return False
|
|
456
|
+
if kind == _rc.PLAIN:
|
|
457
|
+
return True
|
|
458
|
+
# Review P3-2: a compressed container also needs the exact device,
|
|
459
|
+
# matching the walk's container-unchanged check and the generation
|
|
460
|
+
# token (Task 1, Task 3).
|
|
461
|
+
return (self.device is not None and int(self.device) == st.st_dev
|
|
462
|
+
and self.container_size == st.st_size
|
|
463
|
+
and self.mtime_ns == st.st_mtime_ns
|
|
464
|
+
and self.decoded_size is not None)
|
|
465
|
+
|
|
466
|
+
def accepts(self, kind: str, st) -> bool:
|
|
467
|
+
return self.state == "accepted" and self._same_container(kind, st)
|
|
468
|
+
|
|
469
|
+
def holds(self, kind: str, st) -> bool:
|
|
470
|
+
if self.state not in ("pending", "unverified"):
|
|
471
|
+
return False
|
|
472
|
+
if not self.pending_fingerprint: # a hold on the accepted container
|
|
473
|
+
return self._same_container(kind, st)
|
|
474
|
+
fp = _rc.ContainerFingerprint.from_json(self.pending_fingerprint)
|
|
475
|
+
return (fp is not None and fp.kind == kind and fp.size == st.st_size
|
|
476
|
+
and fp.mtime_ns == st.st_mtime_ns
|
|
477
|
+
and fp.device == st.st_dev and fp.inode == st.st_ino)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
_CONTAINER_EVIDENCE_COLUMNS = (
|
|
481
|
+
"path, container_kind, container_size_bytes, decoded_size_bytes, "
|
|
482
|
+
"mtime_ns, device_id, inode, container_state, pending_container_fingerprint")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _container_evidence_for(conn, table: str, paths) -> "dict[str, ContainerEvidence]":
|
|
486
|
+
"""Container evidence for the named cursor rows, one chunked indexed read.
|
|
487
|
+
|
|
488
|
+
No paths, no statement: an idle tick reads nothing. A store without the
|
|
489
|
+
#968 columns (an unmigrated opener, a hand-built fixture) has no
|
|
490
|
+
container evidence, which is exactly the plain-only world.
|
|
491
|
+
"""
|
|
492
|
+
if table not in _CODEX_CURSOR_TABLES:
|
|
493
|
+
raise ValueError("unknown codex cursor table")
|
|
494
|
+
wanted = sorted({str(p) for p in paths if p})
|
|
495
|
+
out: "dict[str, ContainerEvidence]" = {}
|
|
496
|
+
try:
|
|
497
|
+
for start in range(0, len(wanted), _CURSOR_QUERY_CHUNK):
|
|
498
|
+
chunk = wanted[start:start + _CURSOR_QUERY_CHUNK]
|
|
499
|
+
placeholders = ",".join("?" for _ in chunk)
|
|
500
|
+
for row in conn.execute(
|
|
501
|
+
f"SELECT {_CONTAINER_EVIDENCE_COLUMNS} FROM {table} "
|
|
502
|
+
f"WHERE path IN ({placeholders})", chunk):
|
|
503
|
+
out[str(row[0])] = ContainerEvidence(
|
|
504
|
+
row[1] or _rc.PLAIN, row[2], row[3], row[4], row[5],
|
|
505
|
+
row[6], row[7] or "accepted", row[8])
|
|
506
|
+
except sqlite3.OperationalError:
|
|
507
|
+
return {}
|
|
508
|
+
return out
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _observe_logical(path: str, evidence: "ContainerEvidence | None"):
|
|
512
|
+
"""Restat one LOGICAL rollout through its selected container (5.2, 5.9).
|
|
513
|
+
|
|
514
|
+
Plain wins and keeps today's single ``pathlib`` stat, so a plain estate
|
|
515
|
+
is observed exactly as before; the ``.zst`` twin is statted only when the
|
|
516
|
+
plain file is absent.
|
|
517
|
+
"""
|
|
518
|
+
kind = _rc.PLAIN
|
|
519
|
+
try:
|
|
520
|
+
st = pathlib.Path(path).stat()
|
|
521
|
+
except OSError:
|
|
522
|
+
try:
|
|
523
|
+
st = pathlib.Path(_rc.zstd_twin(path)).stat()
|
|
524
|
+
except OSError:
|
|
525
|
+
return None
|
|
526
|
+
kind = _rc.ZSTD
|
|
527
|
+
if evidence is not None and evidence.accepts(kind, st):
|
|
528
|
+
size = (st.st_size if kind == _rc.PLAIN
|
|
529
|
+
else int(evidence.decoded_size))
|
|
530
|
+
return ObservedFile(size, st.st_dev, st.st_ino, kind=kind,
|
|
531
|
+
mtime_ns=st.st_mtime_ns)
|
|
532
|
+
if kind == _rc.PLAIN and (evidence is None or (
|
|
533
|
+
evidence.state == "accepted" and evidence.kind == _rc.PLAIN)):
|
|
534
|
+
# Today's plain evidence: append, shrink and replacement are decided
|
|
535
|
+
# by the existing classifier from size and identity.
|
|
536
|
+
return ObservedFile(st.st_size, st.st_dev, st.st_ino, kind=kind,
|
|
537
|
+
mtime_ns=st.st_mtime_ns)
|
|
538
|
+
return ObservedFile(
|
|
539
|
+
0, st.st_dev, st.st_ino, container_pending=True,
|
|
540
|
+
container_known=evidence is not None and evidence.holds(kind, st),
|
|
541
|
+
kind=kind, mtime_ns=st.st_mtime_ns)
|
|
271
542
|
|
|
272
543
|
|
|
273
544
|
def source_identity_replaced(
|
|
@@ -325,7 +596,7 @@ def classify_recent_active_path(
|
|
|
325
596
|
"""Classify one recently-active rollout against its committed cursor.
|
|
326
597
|
|
|
327
598
|
Pure. Returns one of ``unchanged``, ``append``, ``resume``, ``replaced``,
|
|
328
|
-
``cursor_gap`` or ``
|
|
599
|
+
``cursor_gap``, ``vanished`` or ``container_pending``.
|
|
329
600
|
|
|
330
601
|
IDENTITY OUTRANKS SIZE. A larger file at a different inode is a
|
|
331
602
|
replacement, not an append: its first byte is not the continuation of the
|
|
@@ -339,6 +610,9 @@ def classify_recent_active_path(
|
|
|
339
610
|
"""
|
|
340
611
|
if observed is None:
|
|
341
612
|
return "vanished"
|
|
613
|
+
if getattr(observed, "container_pending", False):
|
|
614
|
+
# Pending container work is decided by verification, never identity.
|
|
615
|
+
return "container_pending"
|
|
342
616
|
if source_identity_replaced(
|
|
343
617
|
committed_device, committed_inode, observed.device, observed.inode,
|
|
344
618
|
):
|
|
@@ -506,13 +780,10 @@ def _bump_counter(name: str, amount: int = 1) -> None:
|
|
|
506
780
|
_FRONTIER_COUNTERS[name] = _FRONTIER_COUNTERS.get(name, 0) + amount
|
|
507
781
|
|
|
508
782
|
|
|
509
|
-
def _observe_file(path: str, table: str
|
|
783
|
+
def _observe_file(path: str, table: str,
|
|
784
|
+
evidence: "ContainerEvidence | None" = None) -> "ObservedFile | None":
|
|
510
785
|
_bump_counter(f"retained_file_visits.{table}")
|
|
511
|
-
|
|
512
|
-
st = pathlib.Path(path).stat()
|
|
513
|
-
except OSError:
|
|
514
|
-
return None
|
|
515
|
-
return ObservedFile(st.st_size, st.st_dev, st.st_ino)
|
|
786
|
+
return _observe_logical(path, evidence)
|
|
516
787
|
|
|
517
788
|
|
|
518
789
|
#: The two Codex cursor tables the restat may read, and whether each carries
|
|
@@ -572,19 +843,25 @@ def plan_recent_active_restat(
|
|
|
572
843
|
# `sqlite3.DatabaseError` and an escape here would end the tick.
|
|
573
844
|
return frozenset(), "codex_cursor_unavailable"
|
|
574
845
|
targets: set[str] = set()
|
|
846
|
+
evidence = _container_evidence_for(conn, table, candidates)
|
|
575
847
|
for path, (size, offset, complete, device, inode) in sorted(
|
|
576
848
|
candidates.items()
|
|
577
849
|
):
|
|
850
|
+
observed = _observe_file(path, table, evidence.get(path))
|
|
578
851
|
verdict = classify_recent_active_path(
|
|
579
852
|
committed_size=size,
|
|
580
853
|
committed_offset=offset,
|
|
581
854
|
committed_complete=complete,
|
|
582
855
|
committed_device=device,
|
|
583
856
|
committed_inode=inode,
|
|
584
|
-
observed=
|
|
857
|
+
observed=observed,
|
|
585
858
|
)
|
|
586
859
|
if verdict in {"append", "resume"}:
|
|
587
860
|
targets.add(path)
|
|
861
|
+
elif verdict == "container_pending":
|
|
862
|
+
# New transitions request verification once; known holds converge.
|
|
863
|
+
if not observed.container_known:
|
|
864
|
+
targets.add(path)
|
|
588
865
|
elif verdict == "replaced":
|
|
589
866
|
return frozenset(), "source_replaced"
|
|
590
867
|
elif verdict == "cursor_gap":
|
|
@@ -692,13 +969,23 @@ def _configuration_generation_reason(
|
|
|
692
969
|
|
|
693
970
|
|
|
694
971
|
def _ticket_target_paths(provider: str, records, roots):
|
|
695
|
-
"""The ticketed paths this plan may target, or a refusal reason.
|
|
972
|
+
"""The ticketed paths this plan may target, or a refusal reason.
|
|
973
|
+
|
|
974
|
+
#968 spec 5.9: a Codex ticket may name either spelling; it is
|
|
975
|
+
normalized to ONE logical target, and containment is enforced on the
|
|
976
|
+
selected physical container.
|
|
977
|
+
"""
|
|
696
978
|
provider_paths = tuple(
|
|
697
979
|
path for kind, path, _e, _s, _c in records if kind == provider)
|
|
698
980
|
if any(not path for path in provider_paths):
|
|
699
981
|
return None, "ambiguous_activity"
|
|
982
|
+
if provider == "codex":
|
|
983
|
+
provider_paths = tuple(
|
|
984
|
+
os.fspath(_rc.logical_path(path)) for path in provider_paths)
|
|
700
985
|
paths = frozenset(provider_paths)
|
|
701
|
-
if any(not _target_in_roots(path, roots
|
|
986
|
+
if any(not _target_in_roots(path, roots,
|
|
987
|
+
allow_compressed=provider == "codex")
|
|
988
|
+
for path in paths):
|
|
702
989
|
return None, "target_outside_scope"
|
|
703
990
|
return paths, None
|
|
704
991
|
|
|
@@ -771,6 +1058,12 @@ def _counted_plan(owner, body, provider, conn, **kwargs):
|
|
|
771
1058
|
_bump_counter(f"plans.{consumer}.{provider}.{plan.mode}")
|
|
772
1059
|
if plan.mode == "full":
|
|
773
1060
|
_bump_counter(f"full_walks.{consumer}.{provider}")
|
|
1061
|
+
debt_walks = getattr(owner, "_debt_walks", None)
|
|
1062
|
+
if provider == "codex" and debt_walks is not None:
|
|
1063
|
+
# Review N3: the debt-only token this full plan saw, when, and
|
|
1064
|
+
# the activity marker's extent at that moment.
|
|
1065
|
+
debt_walks[provider] = (
|
|
1066
|
+
_debt_only_token(owner, conn), _now(), _marker_extent(owner))
|
|
774
1067
|
if plan.paths:
|
|
775
1068
|
_bump_counter(
|
|
776
1069
|
f"targeted_paths.{consumer}.{provider}", len(plan.paths))
|
|
@@ -1699,19 +1992,41 @@ def _target_has_cursor_gap(conn, provider: str, source_path: str) -> bool:
|
|
|
1699
1992
|
).fetchone()
|
|
1700
1993
|
if row is None:
|
|
1701
1994
|
return False # a genuinely new file is a valid targeted append
|
|
1995
|
+
floor = max(int(row[0] or 0), int(row[1] or 0))
|
|
1996
|
+
if provider == "codex":
|
|
1997
|
+
observed = _observe_logical(
|
|
1998
|
+
source_path,
|
|
1999
|
+
_container_evidence_for(conn, table, [source_path]).get(source_path))
|
|
2000
|
+
if observed is None:
|
|
2001
|
+
return True
|
|
2002
|
+
if observed.container_pending:
|
|
2003
|
+
return False # pending container work, not a missing-file gap
|
|
2004
|
+
return observed.size < floor
|
|
1702
2005
|
try:
|
|
1703
2006
|
actual_size = pathlib.Path(source_path).stat().st_size
|
|
1704
2007
|
except OSError:
|
|
1705
2008
|
return True
|
|
1706
|
-
return actual_size <
|
|
2009
|
+
return actual_size < floor
|
|
1707
2010
|
|
|
1708
2011
|
|
|
1709
|
-
def _target_in_roots(source_path: str, roots
|
|
2012
|
+
def _target_in_roots(source_path: str, roots, *,
|
|
2013
|
+
allow_compressed: bool = False) -> bool:
|
|
1710
2014
|
path = pathlib.Path(source_path)
|
|
1711
|
-
if not path.is_absolute()
|
|
2015
|
+
if not path.is_absolute():
|
|
1712
2016
|
return False
|
|
2017
|
+
if allow_compressed:
|
|
2018
|
+
if not _rc.is_container_name(path.name):
|
|
2019
|
+
return False
|
|
2020
|
+
selected = _rc.select_container(path)
|
|
2021
|
+
if selected is None:
|
|
2022
|
+
return False
|
|
2023
|
+
physical = selected.path
|
|
2024
|
+
else:
|
|
2025
|
+
if path.suffix != ".jsonl":
|
|
2026
|
+
return False
|
|
2027
|
+
physical = path
|
|
1713
2028
|
try:
|
|
1714
|
-
resolved =
|
|
2029
|
+
resolved = physical.resolve(strict=True)
|
|
1715
2030
|
except OSError:
|
|
1716
2031
|
return False
|
|
1717
2032
|
if not resolved.is_file():
|
|
@@ -1752,8 +2067,11 @@ def provider_sync_certifiable(mode: str, stats: object) -> bool:
|
|
|
1752
2067
|
return False
|
|
1753
2068
|
common_clean = not any((
|
|
1754
2069
|
getattr(stats, "lock_contended", False),
|
|
2070
|
+
getattr(stats, "barrier_retry", False),
|
|
1755
2071
|
getattr(stats, "files_failed", 0),
|
|
1756
2072
|
getattr(stats, "files_deferred_torn", 0),
|
|
2073
|
+
getattr(stats, "container_pending", 0),
|
|
2074
|
+
getattr(stats, "files_deferred_reset", 0),
|
|
1757
2075
|
getattr(stats, "deferred_reason", None),
|
|
1758
2076
|
getattr(stats, "prune_refused", False),
|
|
1759
2077
|
getattr(stats, "budget_exhausted", False),
|
|
@@ -1777,6 +2095,7 @@ class DashboardIngestFrontier:
|
|
|
1777
2095
|
def __init__(self, app_dir: pathlib.Path):
|
|
1778
2096
|
self.app_dir = pathlib.Path(app_dir)
|
|
1779
2097
|
self._states: dict[str, _ProviderState] = {}
|
|
2098
|
+
self._debt_walks: dict = {}
|
|
1780
2099
|
self.last_seed_failure: dict[str, str] = {}
|
|
1781
2100
|
self._fallback_count = 0
|
|
1782
2101
|
self._generations = frontier_generations(self.app_dir)
|
|
@@ -1923,6 +2242,9 @@ class DashboardIngestFrontier:
|
|
|
1923
2242
|
self, provider: str, conn, *, roots, guard_paths=(),
|
|
1924
2243
|
configuration_generation=_DERIVE_CONFIGURATION_GENERATION,
|
|
1925
2244
|
) -> FrontierPlan:
|
|
2245
|
+
hold = _debt_hold_plan(self, provider, conn)
|
|
2246
|
+
if hold is not None:
|
|
2247
|
+
return hold
|
|
1926
2248
|
state = self._states.get(provider)
|
|
1927
2249
|
if state is None:
|
|
1928
2250
|
return FrontierPlan(provider, "full", reason="unseeded")
|
|
@@ -2079,12 +2401,29 @@ def _conversation_target_risk(conn, provider: str, source_path: str) -> str | No
|
|
|
2079
2401
|
).fetchone()
|
|
2080
2402
|
if row is None:
|
|
2081
2403
|
return None
|
|
2404
|
+
stored_size = int(row[0] or 0)
|
|
2405
|
+
floor = max(stored_size, int(row[2] or 0))
|
|
2406
|
+
if provider == "codex":
|
|
2407
|
+
# #968 spec 5.9: the same container-aware contract as the accounting
|
|
2408
|
+
# planner, so the two planners converge.
|
|
2409
|
+
observed = _observe_logical(
|
|
2410
|
+
source_path,
|
|
2411
|
+
_container_evidence_for(conn, table, [source_path]).get(source_path))
|
|
2412
|
+
if observed is None:
|
|
2413
|
+
return "cursor_gap"
|
|
2414
|
+
if observed.container_pending:
|
|
2415
|
+
return None
|
|
2416
|
+
if observed.size < floor:
|
|
2417
|
+
return "cursor_gap"
|
|
2418
|
+
if (observed.kind == _rc.PLAIN and observed.size == stored_size
|
|
2419
|
+
and observed.mtime_ns != int(row[1] or 0)):
|
|
2420
|
+
return "source_replaced"
|
|
2421
|
+
return None
|
|
2082
2422
|
try:
|
|
2083
2423
|
stat = pathlib.Path(source_path).stat()
|
|
2084
2424
|
except OSError:
|
|
2085
2425
|
return "cursor_gap"
|
|
2086
|
-
|
|
2087
|
-
if stat.st_size < max(stored_size, int(row[2] or 0)):
|
|
2426
|
+
if stat.st_size < floor:
|
|
2088
2427
|
return "cursor_gap"
|
|
2089
2428
|
if stat.st_size == stored_size and stat.st_mtime_ns != int(row[1] or 0):
|
|
2090
2429
|
return "source_replaced"
|
|
@@ -2101,8 +2440,11 @@ def conversation_sync_certifiable(
|
|
|
2101
2440
|
return False
|
|
2102
2441
|
if any((
|
|
2103
2442
|
getattr(stats, "lock_contended", False),
|
|
2443
|
+
getattr(stats, "barrier_retry", False),
|
|
2104
2444
|
getattr(stats, "files_failed", 0),
|
|
2105
2445
|
getattr(stats, "files_deferred_torn", 0),
|
|
2446
|
+
getattr(stats, "container_pending", 0),
|
|
2447
|
+
getattr(stats, "container_reads_held", 0),
|
|
2106
2448
|
getattr(stats, "deferred_reason", None),
|
|
2107
2449
|
getattr(stats, "prune_refused", False),
|
|
2108
2450
|
getattr(stats, "budget_exhausted", False),
|
|
@@ -2110,9 +2452,12 @@ def conversation_sync_certifiable(
|
|
|
2110
2452
|
)):
|
|
2111
2453
|
return False
|
|
2112
2454
|
total = int(getattr(stats, "files_total", 0) or 0)
|
|
2455
|
+
held = sum((getattr(stats, "container_skipped", None) or {}).values())
|
|
2113
2456
|
completed = (
|
|
2114
2457
|
int(getattr(stats, "files_processed", 0) or 0)
|
|
2115
2458
|
+ int(getattr(stats, "files_skipped_unchanged", 0) or 0)
|
|
2459
|
+
+ int(held)
|
|
2460
|
+
+ int(getattr(stats, "barrier_files_deferred", 0) or 0)
|
|
2116
2461
|
)
|
|
2117
2462
|
if mode == "targeted":
|
|
2118
2463
|
# A source deleted after planning is filtered before the sync sees it;
|
|
@@ -2121,10 +2466,39 @@ def conversation_sync_certifiable(
|
|
|
2121
2466
|
if mode == "full":
|
|
2122
2467
|
# There is no durable conversation-walk sentinel. The per-call census
|
|
2123
2468
|
# is therefore the proof that the exhaustive discovery actually drained.
|
|
2124
|
-
return completed == total
|
|
2469
|
+
return completed == total and not getattr(stats, "prune_debt_pending", 0)
|
|
2125
2470
|
return False
|
|
2126
2471
|
|
|
2127
2472
|
|
|
2473
|
+
def _codex_publication_backlog(conn) -> "tuple[bool, frozenset[str]]":
|
|
2474
|
+
"""Certificates newer than their publication (#968 spec 5.11).
|
|
2475
|
+
|
|
2476
|
+
A seal token the conversations store has not acknowledged means a
|
|
2477
|
+
publication pass is due; its contributor paths are the files that pass
|
|
2478
|
+
must read. Any read failure answers "nothing due": the conversation pass
|
|
2479
|
+
itself fails closed.
|
|
2480
|
+
"""
|
|
2481
|
+
import _lib_codex_publication_barrier as barrier
|
|
2482
|
+
try:
|
|
2483
|
+
sealed = conn.execute("SELECT value FROM cache_db.cache_meta WHERE key=?",
|
|
2484
|
+
(barrier.SEAL_SEQ_KEY,)).fetchone()
|
|
2485
|
+
if sealed is None:
|
|
2486
|
+
return False, frozenset()
|
|
2487
|
+
seen = conn.execute("SELECT value FROM main.cache_meta WHERE key=?",
|
|
2488
|
+
(barrier.PUBLISHED_SEQ_KEY,)).fetchone()
|
|
2489
|
+
if seen is not None and seen[0] == sealed[0]:
|
|
2490
|
+
return False, frozenset()
|
|
2491
|
+
paths: set = set()
|
|
2492
|
+
for _key, _revision, text in conn.execute(
|
|
2493
|
+
barrier.BACKLOG_CERTIFICATES_SQL):
|
|
2494
|
+
cert = barrier.parse_certificate(text)
|
|
2495
|
+
if cert is not None:
|
|
2496
|
+
paths.update(c["path"] for c in cert["contributors"])
|
|
2497
|
+
return True, frozenset(paths)
|
|
2498
|
+
except sqlite3.Error:
|
|
2499
|
+
return False, frozenset()
|
|
2500
|
+
|
|
2501
|
+
|
|
2128
2502
|
class ConversationSyncFrontier:
|
|
2129
2503
|
"""Independent hook-journal certificates for `conversations.db`.
|
|
2130
2504
|
|
|
@@ -2139,6 +2513,7 @@ class ConversationSyncFrontier:
|
|
|
2139
2513
|
def __init__(self, app_dir: pathlib.Path):
|
|
2140
2514
|
self.app_dir = pathlib.Path(app_dir)
|
|
2141
2515
|
self._states: dict[str, _ProviderState] = {}
|
|
2516
|
+
self._debt_walks: dict = {}
|
|
2142
2517
|
self.last_seed_failure: dict[str, str] = {}
|
|
2143
2518
|
self._fallback_count = 0
|
|
2144
2519
|
self._generations = frontier_generations(self.app_dir)
|
|
@@ -2257,6 +2632,17 @@ class ConversationSyncFrontier:
|
|
|
2257
2632
|
self, provider: str, conn, *, roots, guard_paths=(),
|
|
2258
2633
|
configuration_generation=_DERIVE_CONFIGURATION_GENERATION,
|
|
2259
2634
|
):
|
|
2635
|
+
hold = _debt_hold_plan(self, provider, conn)
|
|
2636
|
+
if hold is not None:
|
|
2637
|
+
if provider == "codex":
|
|
2638
|
+
due, backlog = _codex_publication_backlog(conn)
|
|
2639
|
+
if due:
|
|
2640
|
+
# The hold withholds a certificate, not a publication the
|
|
2641
|
+
# cache has sealed; with no certificate held,
|
|
2642
|
+
# commit_provider ignores the targeted result.
|
|
2643
|
+
return replace(hold, mode="targeted", paths=backlog,
|
|
2644
|
+
reason="publication_backlog")
|
|
2645
|
+
return hold
|
|
2260
2646
|
state = self._states.get(provider)
|
|
2261
2647
|
if state is None:
|
|
2262
2648
|
return FrontierPlan(provider, "full", reason="unseeded")
|
|
@@ -2313,11 +2699,19 @@ class ConversationSyncFrontier:
|
|
|
2313
2699
|
return FrontierPlan(provider, "full", reason="source_replaced")
|
|
2314
2700
|
if "cursor_gap" in target_risks:
|
|
2315
2701
|
return FrontierPlan(provider, "full", reason="cursor_gap")
|
|
2316
|
-
|
|
2702
|
+
plan = _restat_and_return(
|
|
2317
2703
|
provider, conn, cursor_table=_conversation_source_table(provider),
|
|
2318
2704
|
paths=paths, marker_end=marker_end, ledger_epoch=ledger_epoch,
|
|
2319
2705
|
ledger_sequence=ledger_sequence,
|
|
2320
2706
|
)
|
|
2707
|
+
if provider == "codex" and plan.mode in ("caught_up", "targeted"):
|
|
2708
|
+
due, backlog = _codex_publication_backlog(conn)
|
|
2709
|
+
if due:
|
|
2710
|
+
plan = replace(
|
|
2711
|
+
plan, mode="targeted", paths=plan.paths | backlog,
|
|
2712
|
+
reason=("publication_backlog" if plan.mode == "caught_up"
|
|
2713
|
+
else plan.reason))
|
|
2714
|
+
return plan
|
|
2321
2715
|
|
|
2322
2716
|
def commit_provider(
|
|
2323
2717
|
self, plan: FrontierPlan, conn, *, roots, guard_paths=(), trusted=True,
|