cctally 1.110.0 → 1.112.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 +23 -0
- package/README.md +5 -6
- package/bin/_cctally_cache.py +14 -7
- package/bin/_cctally_core.py +12 -1
- package/bin/_cctally_dashboard.py +60 -154
- package/bin/_cctally_dashboard_envelope.py +7 -6
- package/bin/_cctally_dashboard_sources.py +90 -55
- package/bin/_cctally_doctor.py +73 -27
- package/bin/_cctally_forecast.py +2 -1
- package/bin/_cctally_journal.py +147 -6
- package/bin/_cctally_milestones.py +29 -3
- package/bin/_cctally_quota.py +10 -1
- package/bin/_cctally_record.py +136 -4
- package/bin/_cctally_rederive.py +311 -8
- package/bin/_cctally_source_analytics.py +20 -8
- package/bin/_cctally_tui.py +546 -45
- package/bin/_cctally_update.py +420 -14
- package/bin/_lib_cache_report.py +7 -10
- package/bin/_lib_codex_conversation_query.py +40 -4
- package/bin/_lib_codex_metadata.py +57 -11
- package/bin/_lib_cost_provenance.py +303 -0
- package/bin/_lib_dashboard_sources.py +263 -28
- package/bin/_lib_doctor.py +2 -2
- package/bin/_lib_ingest_frontier.py +137 -38
- package/bin/_lib_pricing.py +24 -263
- package/bin/_lib_rederive.py +704 -36
- package/bin/_lib_snapshot_cache.py +332 -9
- package/bin/cctally +18 -5
- package/dashboard/static/assets/{ConversationsView-BB9vUiL6.js → ConversationsView-CEZ-BUyi.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-DFY2iPDf.js → DoctorModal-CE1oH_Zy.js} +1 -1
- package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-DAv39uSo.js → ProjectsDrillPanel-B3DZoCeD.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-DSAWo2Fp.js → SourceDetailModal-CHp9EgLP.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-R_9dOVnh.js → UpdateModal-5iKST6jo.js} +1 -1
- package/dashboard/static/assets/{index-DdU62sov.js → index-Cm0VTxJC.js} +2 -2
- package/dashboard/static/assets/index-Dt4tqf43.css +1 -0
- package/dashboard/static/assets/{outlineNavigation-BOCLcRa6.js → outlineNavigation-DkXGL8J6.js} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/ModalRoot-DvRKHbQE.js +0 -1
- package/dashboard/static/assets/index-COhxe3vU.css +0 -1
|
@@ -44,8 +44,10 @@ reason.
|
|
|
44
44
|
"""
|
|
45
45
|
from __future__ import annotations
|
|
46
46
|
|
|
47
|
+
import contextlib
|
|
47
48
|
import datetime as dt
|
|
48
49
|
import hashlib
|
|
50
|
+
import itertools
|
|
49
51
|
import os
|
|
50
52
|
import sqlite3
|
|
51
53
|
import sys
|
|
@@ -274,6 +276,210 @@ def compute_signature(
|
|
|
274
276
|
)
|
|
275
277
|
|
|
276
278
|
|
|
279
|
+
# === #857 — the Codex quota-dependency identity =============================
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
#: Bumped whenever the leg set below changes, so an identity captured by an
|
|
283
|
+
#: older build can never compare equal to one read by a newer rule.
|
|
284
|
+
CODEX_QUOTA_DEPENDENCY_VERSION = "codex-quota-dependency-v1"
|
|
285
|
+
|
|
286
|
+
#: `_cctally_quota._DASHBOARD_PROJECTION_CERTIFICATE_KEY`, spelled here because
|
|
287
|
+
#: this kernel may not import the quota glue; a test pins the two together.
|
|
288
|
+
_CODEX_PROJECTION_CERTIFICATE_KEY = "codex_quota_projection_certificate"
|
|
289
|
+
|
|
290
|
+
@contextlib.contextmanager
|
|
291
|
+
def one_read_snapshot(conn: sqlite3.Connection):
|
|
292
|
+
"""Run the enclosed reads on ONE database snapshot.
|
|
293
|
+
|
|
294
|
+
Joins a transaction the caller already holds — the dashboard's pinned
|
|
295
|
+
source build, or the dispatch read that wraps its signature and its Codex
|
|
296
|
+
quota-dependency identity in one — rather than nesting one; otherwise opens
|
|
297
|
+
a deferred read transaction and rolls it back, leaving the connection as it
|
|
298
|
+
was found. Without it each statement reads its own snapshot, so a commit
|
|
299
|
+
landing between two legs yields an identity no database state ever had. A
|
|
300
|
+
deferred ``BEGIN`` takes no lock and pins no snapshot until the first read,
|
|
301
|
+
so work done inside the block before that read holds nothing. A failing
|
|
302
|
+
``BEGIN`` raises ``sqlite3.Error`` to the caller, whose own handler decides.
|
|
303
|
+
"""
|
|
304
|
+
if conn.in_transaction:
|
|
305
|
+
yield
|
|
306
|
+
return
|
|
307
|
+
conn.execute("BEGIN")
|
|
308
|
+
try:
|
|
309
|
+
yield
|
|
310
|
+
finally:
|
|
311
|
+
try:
|
|
312
|
+
conn.rollback()
|
|
313
|
+
except sqlite3.Error:
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def codex_cache_file_identity(path: object) -> tuple[int, int] | None:
|
|
318
|
+
"""``(st_dev, st_ino)`` of the file ``path`` names NOW (#857).
|
|
319
|
+
|
|
320
|
+
``None`` when it cannot be stat'ed. A path is only a name: this describes
|
|
321
|
+
whichever file holds it at the instant of the call, which is why
|
|
322
|
+
`codex_quota_dependency_identity` brackets its reads with it rather than
|
|
323
|
+
trusting one stat.
|
|
324
|
+
"""
|
|
325
|
+
try:
|
|
326
|
+
file_stat = os.stat(path) # type: ignore[arg-type]
|
|
327
|
+
except (OSError, TypeError, ValueError):
|
|
328
|
+
return None
|
|
329
|
+
return (int(file_stat.st_dev), int(file_stat.st_ino))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def codex_quota_observation_legs(
|
|
333
|
+
conn: sqlite3.Connection,
|
|
334
|
+
) -> tuple[int, str] | None:
|
|
335
|
+
"""The cache.db half of the quota-observation identity (#583 S5, #857).
|
|
336
|
+
|
|
337
|
+
``(quota_window_change_log high-water, codex_window_attribution_revision)``,
|
|
338
|
+
read on one snapshot. Its callers are the doctor's quota-probe signal, the
|
|
339
|
+
dashboard's cross-build quota memo (`_codex_quota_reuse_identity`) and the
|
|
340
|
+
quota-dependency identity below; each leg's query is stated once, the
|
|
341
|
+
revision's in `_read_codex_window_attribution_revision`, which the
|
|
342
|
+
accounting population signature reads too.
|
|
343
|
+
|
|
344
|
+
The high-water mark is read from ``sqlite_sequence``, not ``MAX(seq)``: the
|
|
345
|
+
projector PRUNES consumed ledger entries, and ``AUTOINCREMENT`` keeps its
|
|
346
|
+
high-water across that ``DELETE``, so a prune never reads as a regression.
|
|
347
|
+
Every insert, delete and semantic update of ``quota_window_snapshots`` is
|
|
348
|
+
recorded by the ledger triggers, including a write that forgot to advance
|
|
349
|
+
``codex_physical_mutation_seq``. The attribution revision covers the overlay
|
|
350
|
+
the journal applies with no ``quota_window_snapshots`` write at all.
|
|
351
|
+
|
|
352
|
+
``None`` means "cannot establish identity" and every caller must treat it
|
|
353
|
+
as a cold read: an absent or unreadable ledger is not an idle ledger.
|
|
354
|
+
"""
|
|
355
|
+
try:
|
|
356
|
+
with one_read_snapshot(conn):
|
|
357
|
+
table = conn.execute(
|
|
358
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' "
|
|
359
|
+
"AND name='quota_window_change_log'"
|
|
360
|
+
).fetchone()
|
|
361
|
+
if table is None:
|
|
362
|
+
return None
|
|
363
|
+
seq_row = conn.execute(
|
|
364
|
+
"SELECT seq FROM sqlite_sequence "
|
|
365
|
+
"WHERE name='quota_window_change_log'"
|
|
366
|
+
).fetchone()
|
|
367
|
+
revision = _read_codex_window_attribution_revision(conn)
|
|
368
|
+
except sqlite3.Error:
|
|
369
|
+
return None
|
|
370
|
+
try:
|
|
371
|
+
high_water = 0 if seq_row is None else int(seq_row[0])
|
|
372
|
+
except (TypeError, ValueError):
|
|
373
|
+
return None
|
|
374
|
+
return (high_water, "" if revision is None else revision)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _codex_projection_certificate_digest(conn: sqlite3.Connection) -> str:
|
|
378
|
+
"""SHA-256 of the certificate's stored bytes, ``""`` when absent.
|
|
379
|
+
|
|
380
|
+
Raises ``sqlite3.Error``; the identity's handler decides what that means.
|
|
381
|
+
"""
|
|
382
|
+
row = conn.execute(
|
|
383
|
+
"SELECT value FROM cache_meta WHERE key=?",
|
|
384
|
+
(_CODEX_PROJECTION_CERTIFICATE_KEY,),
|
|
385
|
+
).fetchone()
|
|
386
|
+
if row is None or row[0] is None:
|
|
387
|
+
return ""
|
|
388
|
+
return hashlib.sha256(str(row[0]).encode("utf-8")).hexdigest()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def codex_quota_dependency_identity(
|
|
392
|
+
conn: sqlite3.Connection,
|
|
393
|
+
*,
|
|
394
|
+
opened_file: "tuple[int, int] | None",
|
|
395
|
+
) -> tuple | None:
|
|
396
|
+
"""What a Codex quota verdict depends on beyond the signature legs (#857).
|
|
397
|
+
|
|
398
|
+
The dispatch signature carries ``codex_physical_mutation_seq`` and the
|
|
399
|
+
accounting ledger, but not three inputs that can change a Codex cycle or
|
|
400
|
+
projection verdict on their own: the dedicated quota change ledger (a
|
|
401
|
+
writer that forgot the physical counter, or manual repair), the window
|
|
402
|
+
attribution revision (which also invalidates the projection certificate
|
|
403
|
+
with no row write), and the projection certificate itself (a
|
|
404
|
+
certificate-only recovery or loss). Nor does it see the database file being
|
|
405
|
+
replaced by a restored copy whose counters match.
|
|
406
|
+
|
|
407
|
+
Legs: the version tag, ``st_dev`` / ``st_ino`` of the file this connection
|
|
408
|
+
reads, the two observation legs above, and a digest of the certificate's
|
|
409
|
+
stored bytes (empty when absent). The digest covers only the certificate's
|
|
410
|
+
CONTENTS. Whether those contents are valid is decided with inputs this
|
|
411
|
+
identity does not carry: ``codex_physical_mutation_seq``, which the
|
|
412
|
+
certificate must match, is a dispatch-signature leg and part of the Codex
|
|
413
|
+
``data_version``; and the stats.db ``quota_projection_state`` it is checked
|
|
414
|
+
against is covered by ``codex_stats_digest``, also in both. The contents
|
|
415
|
+
are the one part neither of those sees, so they are this identity's leg,
|
|
416
|
+
and the key and the version together cover the rest.
|
|
417
|
+
|
|
418
|
+
File identity is ``(st_dev, st_ino)`` rather than the path string: the
|
|
419
|
+
dashboard compares an identity read on its pinned read-write connection with
|
|
420
|
+
one read at dispatch on another connection, and the path SQLite reports is
|
|
421
|
+
a spelling of the same file that two openers need not share.
|
|
422
|
+
|
|
423
|
+
**What is bound.** ``opened_file`` is REQUIRED: the
|
|
424
|
+
``codex_cache_file_identity`` of the configured database path, observed by
|
|
425
|
+
the caller BEFORE it opened ``conn``. After this function's last read, the
|
|
426
|
+
path ``PRAGMA database_list`` reports for ``conn`` — resolved once, here —
|
|
427
|
+
is stat'ed once more, and the identity exists only when that stat names the
|
|
428
|
+
same file. SQLite opens the main database file when the connection is
|
|
429
|
+
created and reads through that one descriptor for its lifetime; a stat of
|
|
430
|
+
a path, by contrast, describes whatever file holds the name at that
|
|
431
|
+
instant. So agreement between a stat taken before the open and a stat of
|
|
432
|
+
the connection's own path taken after every read proves the connection
|
|
433
|
+
read the file both name. The binding has two blind spots, either mistaken
|
|
434
|
+
for no replacement only when every other leg of the identity matches as
|
|
435
|
+
well: a file moved away and then back to that same inode inside the
|
|
436
|
+
bracket; and, on a filesystem that reuses inode numbers (APFS does not), a
|
|
437
|
+
replacement that received the number of the file it replaced once that
|
|
438
|
+
file was freed, whose identity then names the same ``(st_dev, st_ino)`` as
|
|
439
|
+
a stamp taken on its predecessor. Disagreement is ``None``: the file was
|
|
440
|
+
replaced between the open and the last read (a stat then names the NEW
|
|
441
|
+
inode beside the OLD rows), or the connection was opened on a different
|
|
442
|
+
file than the configured path names.
|
|
443
|
+
|
|
444
|
+
**There is no fallback.** ``opened_file=None`` — the pre-open stat failed,
|
|
445
|
+
whether because the name was absent (a fresh install, or a restore caught
|
|
446
|
+
mid-rename) or for any other reason — yields ``None``: nothing observed
|
|
447
|
+
before the open, so nothing establishes which file the connection opened,
|
|
448
|
+
and a bracket that began after the open could stat a replacement twice
|
|
449
|
+
around reads from the replaced file. A caller that needs an identity where
|
|
450
|
+
the file did not exist yet must observe the file its first open created
|
|
451
|
+
and open AGAIN (the dashboard's dispatch read does exactly that). A
|
|
452
|
+
replacement AFTER the bracket needs nothing more: the connection keeps
|
|
453
|
+
reading the file the identity names, and the next read, on a new
|
|
454
|
+
connection, names the new file.
|
|
455
|
+
|
|
456
|
+
Every leg is read on ONE snapshot (`one_read_snapshot`), so a commit
|
|
457
|
+
landing between two legs cannot produce an identity no state ever had.
|
|
458
|
+
Every read is O(1) — ``PRAGMA database_list`` once, one ``stat``, a catalog
|
|
459
|
+
lookup and three primary-key reads — because the dashboard pays it on
|
|
460
|
+
every idle tick. It is STABLE while nothing changes: none of the legs moves
|
|
461
|
+
on a read, a WAL checkpoint, or a ledger prune. ``None`` means "cannot
|
|
462
|
+
establish identity", which every caller must treat as "rebuild".
|
|
463
|
+
"""
|
|
464
|
+
if opened_file is None:
|
|
465
|
+
return None
|
|
466
|
+
path = codex_main_database_path(conn)
|
|
467
|
+
if not path:
|
|
468
|
+
return None
|
|
469
|
+
try:
|
|
470
|
+
with one_read_snapshot(conn):
|
|
471
|
+
legs = codex_quota_observation_legs(conn)
|
|
472
|
+
certificate = _codex_projection_certificate_digest(conn)
|
|
473
|
+
except sqlite3.Error:
|
|
474
|
+
return None
|
|
475
|
+
if legs is None:
|
|
476
|
+
return None
|
|
477
|
+
after = codex_cache_file_identity(path)
|
|
478
|
+
if after is None or after != tuple(opened_file):
|
|
479
|
+
return None
|
|
480
|
+
return (CODEX_QUOTA_DEPENDENCY_VERSION, *after, *legs, certificate)
|
|
481
|
+
|
|
482
|
+
|
|
277
483
|
# === Task 0.2 — new-entry timestamp watermark ==============================
|
|
278
484
|
|
|
279
485
|
|
|
@@ -1103,26 +1309,99 @@ class CodexAccountingCacheResult:
|
|
|
1103
1309
|
#: all. `None` means "cannot establish an identity", which every consumer
|
|
1104
1310
|
#: must treat as a cold read rather than as a hit.
|
|
1105
1311
|
population_signature: tuple | None = None
|
|
1312
|
+
#: #857: the capture-owned accounting provenance token — the never-reused
|
|
1313
|
+
#: name of the population in `entries` (`build_cached_codex_accounting`
|
|
1314
|
+
#: states when it is minted and when preserved). The dashboard stamps it on
|
|
1315
|
+
#: the generation this capture builds and admits a retained generation
|
|
1316
|
+
#: only while its stamp equals `codex_accounting_consumed_provenance()`.
|
|
1317
|
+
provenance_token: int | None = None
|
|
1106
1318
|
|
|
1107
1319
|
|
|
1108
1320
|
_CODEX_ACCOUNTING_CACHE_STATE: dict[str, object] = _ObservedSnapshotDict()
|
|
1109
1321
|
_CODEX_ACCOUNTING_MAX_DIRTY_PATHS = 300
|
|
1110
1322
|
|
|
1323
|
+
#: #857: the server-only ``clock_data`` key a Codex generation carries its
|
|
1324
|
+
#: capture's accounting provenance token under (`None` for a build that
|
|
1325
|
+
#: bypassed the qualified accounting read, which no admission gate accepts).
|
|
1326
|
+
CODEX_ACCOUNTING_PROVENANCE_KEY = "codex_accounting_provenance"
|
|
1327
|
+
#: The state-dictionary key the token of the population the dictionary holds
|
|
1328
|
+
#: lives under. Inside the dictionary on purpose: a checkpoint copies it and a
|
|
1329
|
+
#: restore puts it back together with exactly the state it names, never apart.
|
|
1330
|
+
_CODEX_ACCOUNTING_PROVENANCE_STATE_KEY = "provenance_token"
|
|
1331
|
+
#: The token allocator. Never rewound: not by a reset, not by a restore and not
|
|
1332
|
+
#: by memory eviction, so no two populations this process ever held — however
|
|
1333
|
+
#: their ``population_signature`` generations compare after a clear — share a
|
|
1334
|
+
#: token.
|
|
1335
|
+
_CODEX_ACCOUNTING_PROVENANCE_TOKENS = itertools.count(1)
|
|
1336
|
+
#: The token of the population the process accounting cache most recently
|
|
1337
|
+
#: CONSUMED — set by every successful `build_cached_codex_accounting` call.
|
|
1338
|
+
#: Deliberately OUTSIDE `_CODEX_ACCOUNTING_CACHE_STATE`: snapshot memory
|
|
1339
|
+
#: enforcement clears that dictionary after a published build, and an eviction
|
|
1340
|
+
#: consumes nothing, so it must not invalidate the generation just published.
|
|
1341
|
+
#: A reset or a checkpoint restore does not move it either. When the failed
|
|
1342
|
+
#: build's capture minted, a generation stamped with the checkpoint's token
|
|
1343
|
+
#: predates accounting this process consumed once and stays refused until a
|
|
1344
|
+
#: capture names the population again; when that capture preserved the token,
|
|
1345
|
+
#: it consumed nothing, and the checkpoint's generation correctly stays current.
|
|
1346
|
+
_CODEX_ACCOUNTING_CONSUMED_PROVENANCE: int | None = None
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def codex_accounting_consumed_provenance() -> int | None:
|
|
1350
|
+
"""The token of the accounting population this process last consumed.
|
|
1351
|
+
|
|
1352
|
+
#857. An O(1) read of a module global — no database, no state walk — so
|
|
1353
|
+
every Codex admission gate can compare a retained generation's stamp
|
|
1354
|
+
against it on every tick. ``None`` until the first qualified capture.
|
|
1355
|
+
Not owner-asserted because it mutates nothing. It is safe only because
|
|
1356
|
+
every writer is owner-asserted and every gate reads on the owner thread, so
|
|
1357
|
+
no read can land between a capture's state write and its consume call.
|
|
1358
|
+
"""
|
|
1359
|
+
return _CODEX_ACCOUNTING_CONSUMED_PROVENANCE
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
def _mint_codex_accounting_provenance() -> int:
|
|
1363
|
+
"""A token no earlier population of this process has carried (#857)."""
|
|
1364
|
+
return next(_CODEX_ACCOUNTING_PROVENANCE_TOKENS)
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
def _consume_codex_accounting_provenance(token: int) -> int:
|
|
1368
|
+
"""Record ``token`` as the last consumed population's name and return it."""
|
|
1369
|
+
global _CODEX_ACCOUNTING_CONSUMED_PROVENANCE
|
|
1370
|
+
_CODEX_ACCOUNTING_CONSUMED_PROVENANCE = token
|
|
1371
|
+
return token
|
|
1372
|
+
|
|
1111
1373
|
|
|
1112
1374
|
def reset_codex_accounting_cache_state() -> None:
|
|
1113
|
-
"""Drop #582's value-only Codex accounting cache and ledger cursor.
|
|
1375
|
+
"""Drop #582's value-only Codex accounting cache and ledger cursor.
|
|
1376
|
+
|
|
1377
|
+
#857: the consumed provenance token and its allocator are untouched. A
|
|
1378
|
+
reset consumes nothing, and the next capture goes cold and mints a token
|
|
1379
|
+
no earlier population carried.
|
|
1380
|
+
"""
|
|
1114
1381
|
_assert_owner()
|
|
1115
1382
|
_CODEX_ACCOUNTING_CACHE_STATE.clear()
|
|
1116
1383
|
|
|
1117
1384
|
|
|
1118
1385
|
def checkpoint_codex_accounting_cache_state() -> dict[str, object]:
|
|
1119
|
-
"""Copy the value-only state so a failed source build can roll back.
|
|
1386
|
+
"""Copy the value-only state so a failed source build can roll back.
|
|
1387
|
+
|
|
1388
|
+
The copy carries the population's provenance token (#857), so a restore
|
|
1389
|
+
puts back exactly the state that token names.
|
|
1390
|
+
"""
|
|
1120
1391
|
_assert_owner()
|
|
1121
1392
|
return dict(_CODEX_ACCOUNTING_CACHE_STATE)
|
|
1122
1393
|
|
|
1123
1394
|
|
|
1124
1395
|
def restore_codex_accounting_cache_state(state: dict[str, object]) -> None:
|
|
1125
|
-
"""Restore a checkpoint after downstream source construction fails.
|
|
1396
|
+
"""Restore a checkpoint after downstream source construction fails.
|
|
1397
|
+
|
|
1398
|
+
#857: the state's own token comes back with it, but the CONSUMED token does
|
|
1399
|
+
not move back and the allocator is never rewound. If the failed build's
|
|
1400
|
+
capture minted, a generation stamped with the checkpoint's token predates
|
|
1401
|
+
accounting that build consumed and stays refused until a capture names the
|
|
1402
|
+
population again; if it preserved the token, it consumed nothing and that
|
|
1403
|
+
generation correctly stays current.
|
|
1404
|
+
"""
|
|
1126
1405
|
_assert_owner()
|
|
1127
1406
|
_CODEX_ACCOUNTING_CACHE_STATE.clear()
|
|
1128
1407
|
_CODEX_ACCOUNTING_CACHE_STATE.update(state)
|
|
@@ -1175,18 +1454,31 @@ def _codex_window_attribution_revision(conn: sqlite3.Connection) -> str | None:
|
|
|
1175
1454
|
keyed on the ledger alone would keep serving a population built before the
|
|
1176
1455
|
operator's attribution landed. Carrying the revision turns that window into
|
|
1177
1456
|
an unnecessary miss, which is the direction this signature is written in.
|
|
1457
|
+
|
|
1458
|
+
An unreadable revision reads as ``None`` here, which the signature treats
|
|
1459
|
+
as one more value; the #857 identity reads the same query through
|
|
1460
|
+
`_read_codex_window_attribution_revision` instead, because there an
|
|
1461
|
+
unreadable leg must refuse the identity rather than become a value.
|
|
1178
1462
|
"""
|
|
1179
1463
|
try:
|
|
1180
|
-
|
|
1181
|
-
"SELECT value FROM cache_meta "
|
|
1182
|
-
"WHERE key='codex_window_attribution_revision'"
|
|
1183
|
-
).fetchone()
|
|
1464
|
+
return _read_codex_window_attribution_revision(conn)
|
|
1184
1465
|
except sqlite3.Error:
|
|
1185
1466
|
return None
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def _read_codex_window_attribution_revision(
|
|
1470
|
+
conn: sqlite3.Connection,
|
|
1471
|
+
) -> str | None:
|
|
1472
|
+
"""The ONE statement of the attribution-revision read; raises on error."""
|
|
1473
|
+
row = conn.execute(
|
|
1474
|
+
"SELECT value FROM cache_meta "
|
|
1475
|
+
"WHERE key='codex_window_attribution_revision'"
|
|
1476
|
+
).fetchone()
|
|
1186
1477
|
return None if row is None or row[0] is None else str(row[0])
|
|
1187
1478
|
|
|
1188
1479
|
|
|
1189
|
-
def
|
|
1480
|
+
def codex_main_database_path(conn: sqlite3.Connection) -> str | None:
|
|
1481
|
+
"""The path SQLite reports for ``conn``'s ``main`` database, or ``None``."""
|
|
1190
1482
|
try:
|
|
1191
1483
|
return next(
|
|
1192
1484
|
str(row[2]) for row in conn.execute("PRAGMA database_list")
|
|
@@ -1237,6 +1529,15 @@ def build_cached_codex_accounting(
|
|
|
1237
1529
|
or clock regression goes cold. When the upper bound advances, paths with
|
|
1238
1530
|
already-stored future rows crossing into the range are dirtied even though
|
|
1239
1531
|
no mutation sequence moved.
|
|
1532
|
+
|
|
1533
|
+
#857: every successful call also returns — and records as the process's
|
|
1534
|
+
CONSUMED provenance (`codex_accounting_consumed_provenance`) — the
|
|
1535
|
+
population's provenance token, established here beside the state it names.
|
|
1536
|
+
A cold reconstruction, a ledger advance (whether or not it dirties a path)
|
|
1537
|
+
and a path replacement (a revealed future row included) each MINT a token
|
|
1538
|
+
no earlier population carried; only a capture that changed nothing — same
|
|
1539
|
+
ledger head, no dirty path, an upper bound that revealed no row — PRESERVES
|
|
1540
|
+
the state's token. A failed load publishes nothing, token included.
|
|
1240
1541
|
"""
|
|
1241
1542
|
_assert_owner()
|
|
1242
1543
|
if (
|
|
@@ -1246,7 +1547,7 @@ def build_cached_codex_accounting(
|
|
|
1246
1547
|
):
|
|
1247
1548
|
raise ValueError("Codex accounting cache range must be aware and ordered")
|
|
1248
1549
|
current_seq = _codex_accounting_mutation_seq(cache_conn)
|
|
1249
|
-
database_path =
|
|
1550
|
+
database_path = codex_main_database_path(cache_conn)
|
|
1250
1551
|
attribution_revision = _codex_window_attribution_revision(cache_conn)
|
|
1251
1552
|
state = _CODEX_ACCOUNTING_CACHE_STATE
|
|
1252
1553
|
prior_cached_entries = tuple(state.get("entries", ())) if state else ()
|
|
@@ -1341,6 +1642,8 @@ def build_cached_codex_accounting(
|
|
|
1341
1642
|
}))
|
|
1342
1643
|
generation = int(state.get("generation", 0)) + 1
|
|
1343
1644
|
population_signature = signature(generation)
|
|
1645
|
+
# #857: a reconstruction is a new population, whatever it contains.
|
|
1646
|
+
provenance_token = _mint_codex_accounting_provenance()
|
|
1344
1647
|
_CODEX_ACCOUNTING_CACHE_STATE.clear()
|
|
1345
1648
|
_CODEX_ACCOUNTING_CACHE_STATE.update({
|
|
1346
1649
|
"entries": entries,
|
|
@@ -1350,21 +1653,36 @@ def build_cached_codex_accounting(
|
|
|
1350
1653
|
"extra": extra_signature,
|
|
1351
1654
|
"generation": generation,
|
|
1352
1655
|
"population_signature": population_signature,
|
|
1656
|
+
_CODEX_ACCOUNTING_PROVENANCE_STATE_KEY: provenance_token,
|
|
1353
1657
|
})
|
|
1354
1658
|
return CodexAccountingCacheResult(
|
|
1355
1659
|
entries, (), accounts, True, prior_cached_entries, entries,
|
|
1356
1660
|
population_signature=population_signature,
|
|
1661
|
+
provenance_token=_consume_codex_accounting_provenance(
|
|
1662
|
+
provenance_token),
|
|
1357
1663
|
)
|
|
1358
1664
|
|
|
1359
1665
|
prior_entries = tuple(state["entries"])
|
|
1360
1666
|
if not dirty:
|
|
1361
1667
|
population_signature = signature(int(state.get("generation", 0)))
|
|
1668
|
+
# #857: no row moved, but a ledger advance still consumed a ledger
|
|
1669
|
+
# state the population did not carry before, so it is renamed; only a
|
|
1670
|
+
# capture over the same head keeps the state's token.
|
|
1671
|
+
provenance_token = state.get(_CODEX_ACCOUNTING_PROVENANCE_STATE_KEY)
|
|
1672
|
+
if (
|
|
1673
|
+
current_seq != int(state.get("seq", -1))
|
|
1674
|
+
or not isinstance(provenance_token, int)
|
|
1675
|
+
):
|
|
1676
|
+
provenance_token = _mint_codex_accounting_provenance()
|
|
1362
1677
|
state["seq"] = current_seq
|
|
1363
1678
|
state["end"] = range_end
|
|
1364
1679
|
state["population_signature"] = population_signature
|
|
1680
|
+
state[_CODEX_ACCOUNTING_PROVENANCE_STATE_KEY] = provenance_token
|
|
1365
1681
|
return CodexAccountingCacheResult(
|
|
1366
1682
|
prior_entries, (), (), False,
|
|
1367
1683
|
population_signature=population_signature,
|
|
1684
|
+
provenance_token=_consume_codex_accounting_provenance(
|
|
1685
|
+
provenance_token),
|
|
1368
1686
|
)
|
|
1369
1687
|
|
|
1370
1688
|
dirty_paths = tuple(sorted(dirty))
|
|
@@ -1405,14 +1723,19 @@ def build_cached_codex_accounting(
|
|
|
1405
1723
|
_mark_snapshot_accelerators_dirty()
|
|
1406
1724
|
generation = int(state.get("generation", 0)) + 1
|
|
1407
1725
|
population_signature = signature(generation)
|
|
1726
|
+
# #857: a path replacement is a new population, even one whose rows
|
|
1727
|
+
# compare equal — the ledger moved or the bound revealed a row.
|
|
1728
|
+
provenance_token = _mint_codex_accounting_provenance()
|
|
1408
1729
|
state["entries"] = entries
|
|
1409
1730
|
state["seq"] = current_seq
|
|
1410
1731
|
state["end"] = range_end
|
|
1411
1732
|
state["generation"] = generation
|
|
1412
1733
|
state["population_signature"] = population_signature
|
|
1734
|
+
state[_CODEX_ACCOUNTING_PROVENANCE_STATE_KEY] = provenance_token
|
|
1413
1735
|
return CodexAccountingCacheResult(
|
|
1414
1736
|
entries, dirty_paths, dirty_accounts, False, changed_old, changed_new,
|
|
1415
1737
|
population_signature=population_signature,
|
|
1738
|
+
provenance_token=_consume_codex_accounting_provenance(provenance_token),
|
|
1416
1739
|
)
|
|
1417
1740
|
|
|
1418
1741
|
|
package/bin/cctally
CHANGED
|
@@ -351,6 +351,7 @@ UPDATE_LOCK_PATH = _cctally_core.UPDATE_LOCK_PATH
|
|
|
351
351
|
UPDATE_LOG_PATH = _cctally_core.UPDATE_LOG_PATH
|
|
352
352
|
UPDATE_LOG_ROTATED_PATH = _cctally_core.UPDATE_LOG_ROTATED_PATH
|
|
353
353
|
UPDATE_CHECK_LAST_FETCH_PATH = _cctally_core.UPDATE_CHECK_LAST_FETCH_PATH
|
|
354
|
+
INSTALL_SUCCESS_RECORD_PATH = _cctally_core.INSTALL_SUCCESS_RECORD_PATH
|
|
354
355
|
|
|
355
356
|
CLAUDE_SETTINGS_PATH = _cctally_core.CLAUDE_SETTINGS_PATH
|
|
356
357
|
|
|
@@ -1356,6 +1357,10 @@ _persist_npm_prefix_to_state = _cctally_update._persist_npm_prefix_to_state
|
|
|
1356
1357
|
_detect_install_method = _cctally_update._detect_install_method
|
|
1357
1358
|
_persist_install_method_to_state = _cctally_update._persist_install_method_to_state
|
|
1358
1359
|
_stamp_install_success_to_state = _cctally_update._stamp_install_success_to_state
|
|
1360
|
+
InstallSuccessRecord = _cctally_update.InstallSuccessRecord
|
|
1361
|
+
_write_install_success_record = _cctally_update._write_install_success_record
|
|
1362
|
+
_read_install_success_record = _cctally_update._read_install_success_record
|
|
1363
|
+
_update_lock_is_held = _cctally_update._update_lock_is_held
|
|
1359
1364
|
_self_heal_current_version = _cctally_update._self_heal_current_version
|
|
1360
1365
|
_BREW_VERSION_RE_LIST = _cctally_update._BREW_VERSION_RE_LIST
|
|
1361
1366
|
_update_user_agent = _cctally_update._update_user_agent
|
|
@@ -1388,8 +1393,17 @@ _preflight_install = _cctally_update._preflight_install
|
|
|
1388
1393
|
_build_update_steps = _cctally_update._build_update_steps
|
|
1389
1394
|
_run_streaming = _cctally_update._run_streaming
|
|
1390
1395
|
_do_update_install = _cctally_update._do_update_install
|
|
1396
|
+
_install_root_for_entrypoint = _cctally_update._install_root_for_entrypoint
|
|
1397
|
+
_read_changelog_version_at = _cctally_update._read_changelog_version_at
|
|
1398
|
+
_install_layout = _cctally_update._install_layout
|
|
1391
1399
|
_resolve_execvp_target = _cctally_update._resolve_execvp_target
|
|
1400
|
+
_execvp_target_problem = _cctally_update._execvp_target_problem
|
|
1401
|
+
AutomaticRestartInProgress = _cctally_update.AutomaticRestartInProgress
|
|
1392
1402
|
UpdateWorker = _cctally_update.UpdateWorker
|
|
1403
|
+
InstallBootState = _cctally_update.InstallBootState
|
|
1404
|
+
capture_install_boot_state = _cctally_update.capture_install_boot_state
|
|
1405
|
+
RestartCandidate = _cctally_update.RestartCandidate
|
|
1406
|
+
InstallWatch = _cctally_update.InstallWatch
|
|
1393
1407
|
_DashboardUpdateCheckThread = _cctally_update._DashboardUpdateCheckThread
|
|
1394
1408
|
cmd_update = _cctally_update.cmd_update
|
|
1395
1409
|
_args_emit_json = _cctally_update._args_emit_json
|
|
@@ -2661,14 +2675,13 @@ def _warn_budget_bad_config_once(exc: Exception) -> None:
|
|
|
2661
2675
|
# server boot in cmd_dashboard. The moved _resolve_execvp_target
|
|
2662
2676
|
# uses them to return (entrypoint, exec_argv) for os.execvp:
|
|
2663
2677
|
#
|
|
2664
|
-
# - npm:
|
|
2665
|
-
#
|
|
2666
|
-
#
|
|
2678
|
+
# - npm: the interpreter the Node shim would use (CCTALLY_PYTHON,
|
|
2679
|
+
# else python3 on PATH) on the install's bin/cctally, so a restart
|
|
2680
|
+
# adds no Node process (#868).
|
|
2667
2681
|
# - brew: entrypoint = <brew>/bin/cctally → symlink into the
|
|
2668
2682
|
# post-upgrade Python script with its rewritten shebang.
|
|
2669
2683
|
# - Fallback when shutil.which("cctally") returned None: use
|
|
2670
|
-
# sys.argv[0] directly.
|
|
2671
|
-
# degraded edge case rather than guess.
|
|
2684
|
+
# sys.argv[0] directly.
|
|
2672
2685
|
ORIGINAL_SYS_ARGV: list[str] = []
|
|
2673
2686
|
ORIGINAL_ENTRYPOINT: "str | None" = None
|
|
2674
2687
|
|