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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Clone-first file and tree copies (stdlib only; #940 D2, spec §6.7).
|
|
2
|
+
|
|
3
|
+
On APFS, ``fclonefileat(2)`` gives a copy that shares the source's blocks
|
|
4
|
+
until either side is written, so a copy of a large test scaffold writes no
|
|
5
|
+
data. This module takes that clone wherever it can and a real copy wherever
|
|
6
|
+
it cannot, and otherwise behaves exactly as the stdlib copier it stands in
|
|
7
|
+
for:
|
|
8
|
+
|
|
9
|
+
- :func:`copy2` is ``shutil.copy2`` for the same arguments: a directory
|
|
10
|
+
destination keeps the source's basename, ``follow_symlinks=False`` copies a
|
|
11
|
+
link as a link, the same file raises ``shutil.SameFileError``, and an
|
|
12
|
+
existing destination is overwritten in place (same inode), which a clone
|
|
13
|
+
cannot do, so that case is a real copy. Metadata is ``shutil.copystat``'s,
|
|
14
|
+
applied after the clone as after a copy.
|
|
15
|
+
- :func:`copytree` is ``shutil.copytree`` with :func:`copy2` as its file
|
|
16
|
+
copier, so symlink policy, ignore lists, dangling-link errors and
|
|
17
|
+
destination collisions are the stdlib's own code.
|
|
18
|
+
|
|
19
|
+
A clone is attempted only on Darwin. An unsupported filesystem, a
|
|
20
|
+
cross-volume destination, a missing ``fclonefileat`` and any non-Darwin
|
|
21
|
+
platform take the real copy; any other error (EIO, ENOSPC, EACCES, …)
|
|
22
|
+
propagates and is never retried as a copy. A clone is never a hardlink: the
|
|
23
|
+
copy is a new inode with a link count of one.
|
|
24
|
+
|
|
25
|
+
The optional ``counts`` mapping is incremented under ``"clone"`` or
|
|
26
|
+
``"copy"`` per file, so a caller or a measurement can see which path ran.
|
|
27
|
+
This module is public and imports nothing outside the standard library.
|
|
28
|
+
|
|
29
|
+
It binds ``fclonefileat`` itself rather than importing the binding in
|
|
30
|
+
``bin/_lib_remote_staging.py`` (#940 D-2 F17): that module is private, so it
|
|
31
|
+
is absent from the public tree that this module and its public callers ship
|
|
32
|
+
in, and a public module may not import it. The staging module may import
|
|
33
|
+
from this one instead (spec §6.7).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import ctypes
|
|
39
|
+
import errno
|
|
40
|
+
import os
|
|
41
|
+
import shutil
|
|
42
|
+
import stat
|
|
43
|
+
import sys
|
|
44
|
+
|
|
45
|
+
# Errors that mean "this filesystem or this pair of volumes cannot clone",
|
|
46
|
+
# never "the data could not be written". EEXIST is a destination created
|
|
47
|
+
# after the existence check: `shutil.copy2` overwrites it, and so do we.
|
|
48
|
+
_FALLBACK_ERRNOS = frozenset(
|
|
49
|
+
code for code in (
|
|
50
|
+
getattr(errno, "ENOTSUP", None), getattr(errno, "EOPNOTSUPP", None),
|
|
51
|
+
errno.ENOSYS, errno.EXDEV, errno.EINVAL, errno.EPERM, errno.EEXIST)
|
|
52
|
+
if code is not None)
|
|
53
|
+
|
|
54
|
+
_AT_FDCWD = -2 # Darwin's AT_FDCWD
|
|
55
|
+
_CLONE_NOOWNERCOPY = 0x0002 # leave ownership to the caller, as a copy does
|
|
56
|
+
|
|
57
|
+
_LIBC_CLONE = [] # the resolved fclonefileat, once looked up
|
|
58
|
+
# The platform this module decides on; a test sets it here instead of
|
|
59
|
+
# patching the interpreter-wide ``sys.platform`` (#940 D-2 F16).
|
|
60
|
+
_PLATFORM = sys.platform
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _fclonefileat():
|
|
64
|
+
"""The C ``fclonefileat`` on Darwin, else None."""
|
|
65
|
+
if _PLATFORM != "darwin":
|
|
66
|
+
return None
|
|
67
|
+
if not _LIBC_CLONE:
|
|
68
|
+
libc = ctypes.CDLL(None, use_errno=True)
|
|
69
|
+
clone = getattr(libc, "fclonefileat", None)
|
|
70
|
+
if clone is not None:
|
|
71
|
+
clone.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32)
|
|
72
|
+
clone.restype = ctypes.c_int
|
|
73
|
+
_LIBC_CLONE.append(clone)
|
|
74
|
+
return _LIBC_CLONE[0]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _clone_fd(source_fd: int, destination: str) -> None:
|
|
78
|
+
"""Clone the open regular file `source_fd` to the new path `destination`."""
|
|
79
|
+
clone = _fclonefileat()
|
|
80
|
+
if clone is None:
|
|
81
|
+
raise OSError(errno.ENOTSUP, "fclonefileat is unavailable", destination)
|
|
82
|
+
if clone(source_fd, _AT_FDCWD, os.fsencode(os.path.abspath(destination)),
|
|
83
|
+
_CLONE_NOOWNERCOPY) != 0:
|
|
84
|
+
code = ctypes.get_errno()
|
|
85
|
+
raise OSError(code, os.strerror(code), destination)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _count(counts, method: str) -> None:
|
|
89
|
+
if counts is not None:
|
|
90
|
+
counts[method] = counts.get(method, 0) + 1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def copy2(src, dst, *, follow_symlinks: bool = True, counts=None):
|
|
94
|
+
"""``shutil.copy2(src, dst)``, by clone where the filesystem allows it.
|
|
95
|
+
|
|
96
|
+
Returns the destination path, as ``shutil.copy2`` does.
|
|
97
|
+
"""
|
|
98
|
+
if os.path.isdir(dst):
|
|
99
|
+
dst = os.path.join(dst, os.path.basename(src))
|
|
100
|
+
if (not follow_symlinks and os.path.islink(src)) or os.path.lexists(dst):
|
|
101
|
+
# A link copied as a link, or a destination to overwrite in place.
|
|
102
|
+
result = shutil.copy2(src, dst, follow_symlinks=follow_symlinks)
|
|
103
|
+
_count(counts, "copy")
|
|
104
|
+
return result
|
|
105
|
+
try:
|
|
106
|
+
fd = os.open(src, os.O_RDONLY | getattr(os, "O_CLOEXEC", 0))
|
|
107
|
+
except OSError:
|
|
108
|
+
# The stdlib's own error for this source (missing, unreadable, …).
|
|
109
|
+
return shutil.copy2(src, dst, follow_symlinks=follow_symlinks)
|
|
110
|
+
try:
|
|
111
|
+
if not stat.S_ISREG(os.fstat(fd).st_mode):
|
|
112
|
+
# A FIFO, a device or a directory: the stdlib decides.
|
|
113
|
+
os.close(fd)
|
|
114
|
+
fd = -1
|
|
115
|
+
return shutil.copy2(src, dst, follow_symlinks=follow_symlinks)
|
|
116
|
+
try:
|
|
117
|
+
_clone_fd(fd, dst)
|
|
118
|
+
except OSError as exc:
|
|
119
|
+
if exc.errno not in _FALLBACK_ERRNOS:
|
|
120
|
+
raise
|
|
121
|
+
result = shutil.copy2(src, dst, follow_symlinks=follow_symlinks)
|
|
122
|
+
_count(counts, "copy")
|
|
123
|
+
return result
|
|
124
|
+
shutil.copystat(src, dst, follow_symlinks=follow_symlinks)
|
|
125
|
+
_count(counts, "clone")
|
|
126
|
+
return dst
|
|
127
|
+
finally:
|
|
128
|
+
if fd >= 0:
|
|
129
|
+
os.close(fd)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def copytree(src, dst, symlinks: bool = False, ignore=None,
|
|
133
|
+
ignore_dangling_symlinks: bool = False, dirs_exist_ok: bool = False,
|
|
134
|
+
*, counts=None):
|
|
135
|
+
"""``shutil.copytree`` with :func:`copy2` as its file copier."""
|
|
136
|
+
def copier(source, destination):
|
|
137
|
+
return copy2(source, destination, counts=counts)
|
|
138
|
+
|
|
139
|
+
return shutil.copytree(src, dst, symlinks=symlinks, ignore=ignore,
|
|
140
|
+
copy_function=copier,
|
|
141
|
+
ignore_dangling_symlinks=ignore_dangling_symlinks,
|
|
142
|
+
dirs_exist_ok=dirs_exist_ok)
|
|
@@ -2167,6 +2167,38 @@ def _is_late_turn_anchor(
|
|
|
2167
2167
|
)
|
|
2168
2168
|
|
|
2169
2169
|
|
|
2170
|
+
def in_physical_order(rows: Iterable[Any], *, timestamp, position) -> list:
|
|
2171
|
+
"""Rows read in ``(timestamp_utc, id)`` index order, returned in physical
|
|
2172
|
+
order: ``(timestamp_utc, source_path, line_offset)``.
|
|
2173
|
+
|
|
2174
|
+
#901 Amendment 19 T1 (spec I1). ``idx_codex_conv_msgs_conversation`` is
|
|
2175
|
+
``(conversation_key, timestamp_utc, id)``, so a read ordered by the
|
|
2176
|
+
physical position past the timestamp left SQLite a "LAST 2 TERMS" sorter
|
|
2177
|
+
over every run of rows tied on one timestamp, and one conversation's tie
|
|
2178
|
+
run (a whole conversation, when its rows share or lack a timestamp) is not
|
|
2179
|
+
a bound. The caller now reads in index order and this orders each run in
|
|
2180
|
+
Python: the same rows the caller already holds, no temp structure.
|
|
2181
|
+
|
|
2182
|
+
Exact: equal timestamps (NULL included) are contiguous in index order and
|
|
2183
|
+
compare equal here as they do in an ORDER BY, and ``UNIQUE(source_path,
|
|
2184
|
+
line_offset)`` makes ``position`` a total order inside a run.
|
|
2185
|
+
"""
|
|
2186
|
+
ordered: list = []
|
|
2187
|
+
run: list = []
|
|
2188
|
+
run_timestamp = None
|
|
2189
|
+
for row in rows:
|
|
2190
|
+
stamp = timestamp(row)
|
|
2191
|
+
if run and stamp != run_timestamp:
|
|
2192
|
+
run.sort(key=position)
|
|
2193
|
+
ordered.extend(run)
|
|
2194
|
+
run = []
|
|
2195
|
+
run_timestamp = stamp
|
|
2196
|
+
run.append(row)
|
|
2197
|
+
run.sort(key=position)
|
|
2198
|
+
ordered.extend(run)
|
|
2199
|
+
return ordered
|
|
2200
|
+
|
|
2201
|
+
|
|
2170
2202
|
def codex_event_is_late_turn_anchor(event: Any) -> bool:
|
|
2171
2203
|
"""Public predicate shared by delta persistence and turn inference."""
|
|
2172
2204
|
try:
|
|
@@ -27,11 +27,13 @@ import json
|
|
|
27
27
|
import os
|
|
28
28
|
import re
|
|
29
29
|
import sqlite3
|
|
30
|
+
import stat
|
|
30
31
|
import threading
|
|
31
32
|
from collections import deque
|
|
32
33
|
from types import MappingProxyType
|
|
33
34
|
|
|
34
35
|
import _lib_codex_conversation as kern
|
|
36
|
+
import _lib_codex_rollout_container as _rc
|
|
35
37
|
import _lib_codex_landmarks as landmarks
|
|
36
38
|
import _lib_codex_segments as segkern
|
|
37
39
|
from _lib_codex_find_projection import (
|
|
@@ -353,13 +355,19 @@ def _load_conversation_index_rows(
|
|
|
353
355
|
"""
|
|
354
356
|
rows: list = []
|
|
355
357
|
detail_bytes: dict[tuple[str, int], int] = {}
|
|
358
|
+
# #901 Amendment 19 T1: index order, each timestamp tie run then put in
|
|
359
|
+
# physical order in Python (``kern.in_physical_order``), not a sorter.
|
|
356
360
|
for (source_path, line_offset, timestamp_utc, turn_id, call_id, kind,
|
|
357
361
|
event_type, record_family, model, content_digest, content_len,
|
|
358
|
-
detail_json, detail_len) in
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
362
|
+
detail_json, detail_len) in kern.in_physical_order(
|
|
363
|
+
conn.execute(
|
|
364
|
+
"SELECT " + _NARROW_ROW_COLS + " FROM codex_conversation_messages "
|
|
365
|
+
"WHERE conversation_key = ? "
|
|
366
|
+
"ORDER BY timestamp_utc, id",
|
|
367
|
+
(conversation_key,),
|
|
368
|
+
),
|
|
369
|
+
timestamp=lambda row: row[2],
|
|
370
|
+
position=lambda row: (row[0], row[1]),
|
|
363
371
|
):
|
|
364
372
|
rows.append(kern.CodexNormalizedRow(
|
|
365
373
|
conversation_key=conversation_key, source_root_key="",
|
|
@@ -1857,6 +1865,29 @@ def codex_conversation_exists(conn: sqlite3.Connection, conversation_key: str) -
|
|
|
1857
1865
|
return row is not None
|
|
1858
1866
|
|
|
1859
1867
|
|
|
1868
|
+
def codex_publication_pending(
|
|
1869
|
+
conn: sqlite3.Connection, conversation_key: str,
|
|
1870
|
+
) -> bool:
|
|
1871
|
+
"""A barrier-population conversation with no publication yet (#968 5.11).
|
|
1872
|
+
|
|
1873
|
+
Such a conversation stays in the existing pending-state handling until it
|
|
1874
|
+
has an eligible publication: it answers ``normalization_pending``, never
|
|
1875
|
+
``not_found``. Any store error answers False (the caller's ``not_found``).
|
|
1876
|
+
"""
|
|
1877
|
+
try:
|
|
1878
|
+
if conn.execute(
|
|
1879
|
+
"SELECT 1 FROM cache_db.codex_barrier_population "
|
|
1880
|
+
"WHERE conversation_key=?", (conversation_key,)).fetchone() is None:
|
|
1881
|
+
return False
|
|
1882
|
+
row = conn.execute(
|
|
1883
|
+
"SELECT published_revision, legacy_uncertified "
|
|
1884
|
+
"FROM main.codex_conversation_publications WHERE conversation_key=?",
|
|
1885
|
+
(conversation_key,)).fetchone()
|
|
1886
|
+
except sqlite3.Error:
|
|
1887
|
+
return False
|
|
1888
|
+
return row is None or (int(row[0] or 0) == 0 and not row[1])
|
|
1889
|
+
|
|
1890
|
+
|
|
1860
1891
|
def codex_conversation_source_paths(
|
|
1861
1892
|
conn: sqlite3.Connection, conversation_key: str
|
|
1862
1893
|
) -> list[str]:
|
|
@@ -2339,6 +2370,9 @@ def get_codex_conversation(
|
|
|
2339
2370
|
# ── Phase A: the narrow index pass ───────────────────────────────────────
|
|
2340
2371
|
rows, detail_bytes = _load_conversation_index_rows(conn, conversation_key)
|
|
2341
2372
|
if not rows:
|
|
2373
|
+
if codex_publication_pending(conn, conversation_key):
|
|
2374
|
+
return {"status": "normalization_pending",
|
|
2375
|
+
"conversation_key": conversation_key, "items": [], "children": []}
|
|
2342
2376
|
return {"status": "not_found", "conversation_key": conversation_key}
|
|
2343
2377
|
undecodable_keys = undecodable_codex_conversation_keys(conn)
|
|
2344
2378
|
kept, _suppressed = kern.pair_mirrors(rows)
|
|
@@ -3029,6 +3063,9 @@ def _outline_envelope(
|
|
|
3029
3063
|
# The outline is not the route F3 bounds, and it stays turn-granular.
|
|
3030
3064
|
rows = _load_conversation_rows(conn, conversation_key)
|
|
3031
3065
|
if not rows:
|
|
3066
|
+
if codex_publication_pending(conn, conversation_key):
|
|
3067
|
+
return {"status": "normalization_pending",
|
|
3068
|
+
"conversation_key": conversation_key, "turns": [], "files": [], "children": []}
|
|
3032
3069
|
return {"status": "not_found", "conversation_key": conversation_key}
|
|
3033
3070
|
detail_bytes = _detail_bytes_of(rows)
|
|
3034
3071
|
kept, _suppressed = kern.pair_mirrors(rows)
|
|
@@ -3415,20 +3452,26 @@ def _stored_browse_page_queries(
|
|
|
3415
3452
|
"(COALESCE(r.last_activity_utc, '') = ? AND r.conversation_key < ?))")
|
|
3416
3453
|
page_params.extend((cursor_last, cursor_last, cursor_key))
|
|
3417
3454
|
|
|
3455
|
+
# Keep native/parent enrichment inside the page statement. A LEFT JOIN
|
|
3456
|
+
# against the compound last-good view materializes retained history;
|
|
3457
|
+
# correlated probes push these predicates into its indexed branches.
|
|
3458
|
+
# The scalar parent lookup follows the detail reader's same-root lookup.
|
|
3459
|
+
parent_key_sql = (
|
|
3460
|
+
"(SELECT pt.conversation_key FROM codex_conversation_threads pt "
|
|
3461
|
+
"WHERE pt.source_root_key = r.source_root_key "
|
|
3462
|
+
"AND pt.native_thread_id = r.parent_thread_id "
|
|
3463
|
+
"AND pt.conversation_key != r.conversation_key LIMIT 1)"
|
|
3464
|
+
)
|
|
3418
3465
|
sql = (
|
|
3419
3466
|
"SELECT r.conversation_key, r.item_count, r.started_utc, "
|
|
3420
3467
|
"r.last_activity_utc, r.project_key, r.project_label, r.models_json, "
|
|
3421
|
-
"r.title, r.parent_thread_id, r.source_root_key,
|
|
3422
|
-
"
|
|
3468
|
+
"r.title, r.parent_thread_id, r.source_root_key, "
|
|
3469
|
+
"(SELECT t.native_thread_id FROM codex_conversation_threads t "
|
|
3470
|
+
"WHERE t.conversation_key = r.conversation_key), "
|
|
3471
|
+
f"{parent_key_sql}, pr.title, pr.project_label, r.parent_thread_id "
|
|
3423
3472
|
"FROM codex_conversation_rollups r "
|
|
3424
|
-
"LEFT JOIN codex_conversation_threads t "
|
|
3425
|
-
"ON t.conversation_key = r.conversation_key "
|
|
3426
|
-
"LEFT JOIN codex_conversation_threads pt "
|
|
3427
|
-
"ON pt.source_root_key = r.source_root_key "
|
|
3428
|
-
"AND pt.native_thread_id = r.parent_thread_id "
|
|
3429
|
-
"AND pt.conversation_key != r.conversation_key "
|
|
3430
3473
|
"LEFT JOIN codex_conversation_rollups pr "
|
|
3431
|
-
"ON pr.conversation_key =
|
|
3474
|
+
f"ON pr.conversation_key = {parent_key_sql} "
|
|
3432
3475
|
f"WHERE {' AND '.join(page_where)} "
|
|
3433
3476
|
"ORDER BY COALESCE(r.last_activity_utc, '') DESC, r.conversation_key DESC"
|
|
3434
3477
|
)
|
|
@@ -3872,34 +3915,165 @@ def _project_find_row(row, *, payload: dict | None = None, block: dict | None =
|
|
|
3872
3915
|
return project_plain((RenderLeaf("t0", text),))
|
|
3873
3916
|
|
|
3874
3917
|
|
|
3918
|
+
#: Every ``codex_find_projection`` column in one fixed order: the builder's row
|
|
3919
|
+
#: tuples, the stored-row read and the write statements below all use it.
|
|
3920
|
+
#: ``(message_id, surface)`` (positions 0 and 5) is the table's primary key;
|
|
3921
|
+
#: everything else is an attribute the difference compares (#901 §5.3b).
|
|
3922
|
+
_FIND_PROJECTION_COLUMNS = (
|
|
3923
|
+
"message_id,conversation_key,item_key,block_key,container_block_key,"
|
|
3924
|
+
"surface,render_order,projected_text,leaves_json,disclosure_json,"
|
|
3925
|
+
"projection_version"
|
|
3926
|
+
)
|
|
3927
|
+
_FIND_PROJECTION_KEY = (0, 5)
|
|
3928
|
+
_FIND_PROJECTION_ORDER = 6
|
|
3929
|
+
|
|
3930
|
+
_FIND_PROJECTION_INSERT_SQL = (
|
|
3931
|
+
"INSERT INTO codex_find_projection "
|
|
3932
|
+
"(" + _FIND_PROJECTION_COLUMNS + ") "
|
|
3933
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?)"
|
|
3934
|
+
)
|
|
3935
|
+
#: An attribute change that keeps the row's place: the ordering index
|
|
3936
|
+
#: ``(conversation_key, render_order, message_id, surface)`` is untouched.
|
|
3937
|
+
_FIND_PROJECTION_UPDATE_IN_PLACE_SQL = (
|
|
3938
|
+
"UPDATE codex_find_projection SET item_key=?,block_key=?,"
|
|
3939
|
+
"container_block_key=?,projected_text=?,leaves_json=?,disclosure_json=?,"
|
|
3940
|
+
"projection_version=? WHERE message_id=? AND surface=?"
|
|
3941
|
+
)
|
|
3942
|
+
#: A moved row (its dense ``render_order`` shifted): the index entry moves too.
|
|
3943
|
+
_FIND_PROJECTION_UPDATE_MOVED_SQL = (
|
|
3944
|
+
"UPDATE codex_find_projection SET conversation_key=?,item_key=?,"
|
|
3945
|
+
"block_key=?,container_block_key=?,render_order=?,projected_text=?,"
|
|
3946
|
+
"leaves_json=?,disclosure_json=?,projection_version=? "
|
|
3947
|
+
"WHERE message_id=? AND surface=?"
|
|
3948
|
+
)
|
|
3949
|
+
_FIND_PROJECTION_DELETE_SQL = (
|
|
3950
|
+
"DELETE FROM codex_find_projection WHERE message_id=? AND surface=?"
|
|
3951
|
+
)
|
|
3952
|
+
_FIND_PROJECTION_GENERATION_SQL = (
|
|
3953
|
+
"INSERT INTO cache_meta(key,value) VALUES"
|
|
3954
|
+
"('codex_find_projection_generation','1') "
|
|
3955
|
+
"ON CONFLICT(key) DO UPDATE SET value=CAST(value AS INTEGER)+1"
|
|
3956
|
+
)
|
|
3957
|
+
|
|
3958
|
+
|
|
3959
|
+
def _write_codex_find_projection_difference(
|
|
3960
|
+
conn: sqlite3.Connection,
|
|
3961
|
+
conversation_key: str,
|
|
3962
|
+
built: list[tuple],
|
|
3963
|
+
) -> int:
|
|
3964
|
+
"""Make the stored projection of one conversation equal ``built``.
|
|
3965
|
+
|
|
3966
|
+
``built`` is the complete canonical projection in the builder's order. Both
|
|
3967
|
+
sides are keyed by the primary key ``(message_id, surface)`` and compared
|
|
3968
|
+
on every other column; only the difference is written. Returns the number
|
|
3969
|
+
of rows deleted, inserted or updated (0 for an unchanged conversation).
|
|
3970
|
+
"""
|
|
3971
|
+
built_by_key = {
|
|
3972
|
+
(row[_FIND_PROJECTION_KEY[0]], row[_FIND_PROJECTION_KEY[1]]): row
|
|
3973
|
+
for row in built
|
|
3974
|
+
}
|
|
3975
|
+
if len(built_by_key) != len(built):
|
|
3976
|
+
# The whole-rewrite writer raised on a duplicate key at INSERT time;
|
|
3977
|
+
# a difference must not silently keep only one of the two rows.
|
|
3978
|
+
raise sqlite3.IntegrityError(
|
|
3979
|
+
"UNIQUE constraint failed: codex_find_projection.message_id, "
|
|
3980
|
+
"codex_find_projection.surface"
|
|
3981
|
+
)
|
|
3982
|
+
stored = {
|
|
3983
|
+
(row[_FIND_PROJECTION_KEY[0]], row[_FIND_PROJECTION_KEY[1]]): row
|
|
3984
|
+
for row in conn.execute(
|
|
3985
|
+
"SELECT " + _FIND_PROJECTION_COLUMNS + " FROM codex_find_projection "
|
|
3986
|
+
"WHERE conversation_key=?",
|
|
3987
|
+
(conversation_key,),
|
|
3988
|
+
)
|
|
3989
|
+
}
|
|
3990
|
+
deletes = sorted(key for key in stored if key not in built_by_key)
|
|
3991
|
+
inserts: list[tuple] = []
|
|
3992
|
+
in_place: list[tuple] = []
|
|
3993
|
+
moved: list[tuple] = []
|
|
3994
|
+
for key, row in built_by_key.items():
|
|
3995
|
+
old = stored.get(key)
|
|
3996
|
+
if old is None:
|
|
3997
|
+
inserts.append(row)
|
|
3998
|
+
continue
|
|
3999
|
+
if tuple(old) == row:
|
|
4000
|
+
continue
|
|
4001
|
+
message_id, conversation, item, block, container, surface, order, *rest = row
|
|
4002
|
+
if old[1] == conversation and old[_FIND_PROJECTION_ORDER] == order:
|
|
4003
|
+
in_place.append((item, block, container, *rest, message_id, surface))
|
|
4004
|
+
else:
|
|
4005
|
+
moved.append(
|
|
4006
|
+
(conversation, item, block, container, order, *rest,
|
|
4007
|
+
message_id, surface)
|
|
4008
|
+
)
|
|
4009
|
+
if deletes:
|
|
4010
|
+
conn.executemany(_FIND_PROJECTION_DELETE_SQL, deletes)
|
|
4011
|
+
if in_place:
|
|
4012
|
+
conn.executemany(_FIND_PROJECTION_UPDATE_IN_PLACE_SQL, in_place)
|
|
4013
|
+
if moved:
|
|
4014
|
+
conn.executemany(_FIND_PROJECTION_UPDATE_MOVED_SQL, moved)
|
|
4015
|
+
if inserts:
|
|
4016
|
+
conn.executemany(_FIND_PROJECTION_INSERT_SQL, inserts)
|
|
4017
|
+
return len(deletes) + len(inserts) + len(in_place) + len(moved)
|
|
4018
|
+
|
|
4019
|
+
|
|
4020
|
+
def _load_conversation_rows(
|
|
4021
|
+
conn: sqlite3.Connection, conversation_key: str,
|
|
4022
|
+
) -> list:
|
|
4023
|
+
"""A conversation's WIDE normalized rows in physical order:
|
|
4024
|
+
``(timestamp_utc, source_path, line_offset)``.
|
|
4025
|
+
|
|
4026
|
+
#901 Amendment 19 T1: read in ``(timestamp_utc, id)`` index order and each
|
|
4027
|
+
timestamp tie run put in physical order by ``kern.in_physical_order``; the
|
|
4028
|
+
old ``ORDER BY`` left a partial sorter over every tie run, and one
|
|
4029
|
+
conversation is not a bound."""
|
|
4030
|
+
return kern.in_physical_order(
|
|
4031
|
+
(
|
|
4032
|
+
kern.CodexNormalizedRow(*row)
|
|
4033
|
+
for row in conn.execute(
|
|
4034
|
+
"SELECT " + _ROW_COLS + " FROM codex_conversation_messages "
|
|
4035
|
+
"WHERE conversation_key=? "
|
|
4036
|
+
"ORDER BY timestamp_utc,id",
|
|
4037
|
+
(conversation_key,),
|
|
4038
|
+
)
|
|
4039
|
+
),
|
|
4040
|
+
timestamp=lambda row: row.timestamp_utc,
|
|
4041
|
+
position=lambda row: (row.source_path, row.line_offset),
|
|
4042
|
+
)
|
|
4043
|
+
|
|
4044
|
+
|
|
3875
4045
|
def materialize_codex_find_projection(
|
|
3876
4046
|
conn: sqlite3.Connection,
|
|
3877
4047
|
conversation_keys,
|
|
4048
|
+
*,
|
|
4049
|
+
cascaded: bool = False,
|
|
3878
4050
|
) -> None:
|
|
3879
|
-
"""
|
|
4051
|
+
"""Bring #482 projection rows of the affected conversations up to date.
|
|
3880
4052
|
|
|
3881
4053
|
The existing item/block builder is the only authority for native folds.
|
|
3882
4054
|
Every searchable physical row keeps its own block key; a folded output or
|
|
3883
4055
|
completion separately records the visual call block that owns it.
|
|
4056
|
+
|
|
4057
|
+
#901 §5.3b (W6): each conversation's complete canonical projection is still
|
|
4058
|
+
built in memory, exactly as before, but only its difference against the
|
|
4059
|
+
stored rows is written, so an unchanged conversation writes no row.
|
|
4060
|
+
``codex_find_projection_generation`` advances exactly once per call that
|
|
4061
|
+
changed a row, or when the caller says ``cascaded=True``: it deleted
|
|
4062
|
+
messages of these conversations in the same transaction, so the message
|
|
4063
|
+
trigger already cascaded their projection rows away and the builder may
|
|
4064
|
+
find nothing left to change. A true no-op leaves the generation alone.
|
|
4065
|
+
Callers make one call per transaction, so this is once per transaction.
|
|
3884
4066
|
"""
|
|
3885
4067
|
keys = sorted({key for key in conversation_keys if key})
|
|
3886
4068
|
if not keys:
|
|
3887
4069
|
return
|
|
4070
|
+
changed = 0
|
|
3888
4071
|
for conversation_key in keys:
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
(conversation_key,),
|
|
3892
|
-
)
|
|
3893
|
-
rows = [
|
|
3894
|
-
kern.CodexNormalizedRow(*row)
|
|
3895
|
-
for row in conn.execute(
|
|
3896
|
-
"SELECT " + _ROW_COLS + " FROM codex_conversation_messages "
|
|
3897
|
-
"WHERE conversation_key=? "
|
|
3898
|
-
"ORDER BY timestamp_utc,source_path,line_offset",
|
|
3899
|
-
(conversation_key,),
|
|
3900
|
-
)
|
|
3901
|
-
]
|
|
4072
|
+
built: list[tuple] = []
|
|
4073
|
+
rows = _load_conversation_rows(conn, conversation_key)
|
|
3902
4074
|
if not rows:
|
|
4075
|
+
changed += _write_codex_find_projection_difference(
|
|
4076
|
+
conn, conversation_key, built)
|
|
3903
4077
|
continue
|
|
3904
4078
|
kept, _suppressed = kern.pair_mirrors(rows)
|
|
3905
4079
|
items = kern.canonical_items(kept)
|
|
@@ -3941,12 +4115,9 @@ def materialize_codex_find_projection(
|
|
|
3941
4115
|
if row.kind in {"reasoning", "meta"} or surface != "body"
|
|
3942
4116
|
else []
|
|
3943
4117
|
)
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
"container_block_key,surface,render_order,projected_text,"
|
|
3948
|
-
"leaves_json,disclosure_json,projection_version) "
|
|
3949
|
-
"VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
|
4118
|
+
# The builder collects complete rows; the difference below writes
|
|
4119
|
+
# them (column order: `_FIND_PROJECTION_COLUMNS`).
|
|
4120
|
+
built.append(
|
|
3950
4121
|
(
|
|
3951
4122
|
message_id,
|
|
3952
4123
|
conversation_key,
|
|
@@ -4032,11 +4203,11 @@ def materialize_codex_find_projection(
|
|
|
4032
4203
|
container = completion_owner.get(physical_key, physical_key)
|
|
4033
4204
|
store(row, container_block_key=container)
|
|
4034
4205
|
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4206
|
+
changed += _write_codex_find_projection_difference(
|
|
4207
|
+
conn, conversation_key, built)
|
|
4208
|
+
|
|
4209
|
+
if changed or cascaded:
|
|
4210
|
+
conn.execute(_FIND_PROJECTION_GENERATION_SQL)
|
|
4040
4211
|
|
|
4041
4212
|
|
|
4042
4213
|
def _fts_query(query: str, column: str | None) -> str:
|
|
@@ -4828,6 +4999,8 @@ def find_in_codex_conversation(
|
|
|
4828
4999
|
return {**base, "status": "normalization_pending"}
|
|
4829
5000
|
rows = _load_conversation_rows(conn, conversation_key)
|
|
4830
5001
|
if not rows:
|
|
5002
|
+
if codex_publication_pending(conn, conversation_key):
|
|
5003
|
+
return {**base, "status": "normalization_pending"}
|
|
4831
5004
|
return {"status": "not_found", "conversation_key": conversation_key}
|
|
4832
5005
|
q = (query or "").strip()
|
|
4833
5006
|
if not q:
|
|
@@ -4887,6 +5060,9 @@ def codex_conversation_prompts(conn: sqlite3.Connection, conversation_key: str)
|
|
|
4887
5060
|
"conversation_key": conversation_key, "prompts": []}
|
|
4888
5061
|
rows = _load_conversation_rows(conn, conversation_key)
|
|
4889
5062
|
if not rows:
|
|
5063
|
+
if codex_publication_pending(conn, conversation_key):
|
|
5064
|
+
return {"status": "normalization_pending",
|
|
5065
|
+
"conversation_key": conversation_key, "prompts": []}
|
|
4890
5066
|
return {"status": "not_found", "conversation_key": conversation_key}
|
|
4891
5067
|
kept, _suppressed = kern.pair_mirrors(rows)
|
|
4892
5068
|
items = kern.canonical_items(kept)
|
|
@@ -4927,7 +5103,40 @@ def _within_root(source_path: str | None, root_path: str | None) -> bool:
|
|
|
4927
5103
|
return False
|
|
4928
5104
|
|
|
4929
5105
|
|
|
4930
|
-
|
|
5106
|
+
class CodexContainerReadError(RuntimeError):
|
|
5107
|
+
"""A present rollout failed to read; the query envelope reports HTTP 500."""
|
|
5108
|
+
|
|
5109
|
+
def __init__(self, category: str, logical: str) -> None:
|
|
5110
|
+
super().__init__(f"Codex rollout could not be read ({category})")
|
|
5111
|
+
self.category = category
|
|
5112
|
+
self.logical = logical
|
|
5113
|
+
|
|
5114
|
+
|
|
5115
|
+
class _CodexPayloadContainmentError(RuntimeError):
|
|
5116
|
+
"""Internal 404 signal: the selected physical container escaped its root."""
|
|
5117
|
+
|
|
5118
|
+
|
|
5119
|
+
def _select_codex_payload_container(logical):
|
|
5120
|
+
# The discovery leaf deliberately treats stat failures as absent. A payload
|
|
5121
|
+
# request must distinguish a missing container (410) from inaccessible data
|
|
5122
|
+
# (500), including an inaccessible plain file beside a readable zstd twin.
|
|
5123
|
+
for candidate in (logical, _rc.zstd_twin(logical)):
|
|
5124
|
+
try:
|
|
5125
|
+
info = os.stat(candidate)
|
|
5126
|
+
except (FileNotFoundError, NotADirectoryError):
|
|
5127
|
+
continue
|
|
5128
|
+
except OSError as exc:
|
|
5129
|
+
raise CodexContainerReadError("read-error", str(logical)) from exc
|
|
5130
|
+
if not stat.S_ISREG(info.st_mode):
|
|
5131
|
+
raise CodexContainerReadError("read-error", str(logical))
|
|
5132
|
+
selected = _rc.select_container(logical)
|
|
5133
|
+
if selected is not None:
|
|
5134
|
+
return selected
|
|
5135
|
+
# It disappeared between the strict stat and selection; try the twin.
|
|
5136
|
+
return None
|
|
5137
|
+
|
|
5138
|
+
|
|
5139
|
+
def _reread_codex_full_content(conn: sqlite3.Connection, row, *, streams: "dict | None" = None):
|
|
4931
5140
|
"""Re-read the physical line at ``(row.source_path, row.line_offset)``, validate
|
|
4932
5141
|
it against the stored ``codex_conversation_events.payload_json`` for that exact
|
|
4933
5142
|
position (§3.4 structural gone-check — the canonical FULL record, not
|
|
@@ -4939,19 +5148,49 @@ def _reread_codex_full_content(conn: sqlite3.Connection, row):
|
|
|
4939
5148
|
The full pre-cap content is re-derived through the SAME ``_extract`` the
|
|
4940
5149
|
normalizer uses — which is how payload serves content beyond the normalized
|
|
4941
5150
|
``CODEX_TEXT_CAP``. Truncation/``truncated`` is against ``_FULL_PAYLOAD_CEILING``
|
|
4942
|
-
(1,000,000 Python characters), the same ceiling the Claude payload path uses.
|
|
5151
|
+
(1,000,000 Python characters), the same ceiling the Claude payload path uses.
|
|
5152
|
+
|
|
5153
|
+
#968: select plain first, else zstd, and seek in decoded offsets. Streams are
|
|
5154
|
+
request-local and reusable; no decoded copies persist. Present unreadable
|
|
5155
|
+
containers raise CodexContainerReadError, never a false gone result."""
|
|
4943
5156
|
stored = conn.execute(
|
|
4944
5157
|
"SELECT payload_json FROM codex_conversation_events "
|
|
4945
5158
|
"WHERE source_path = ? AND line_offset = ?",
|
|
4946
5159
|
(row.source_path, row.line_offset)).fetchone()
|
|
4947
5160
|
if stored is None:
|
|
4948
5161
|
return None
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
5162
|
+
for attempt in range(2):
|
|
5163
|
+
selected = _select_codex_payload_container(row.source_path)
|
|
5164
|
+
if selected is None:
|
|
5165
|
+
return None
|
|
5166
|
+
# Validate exactly the descriptor we open, including after a compression
|
|
5167
|
+
# race. The caller has already checked the logical path and known root.
|
|
5168
|
+
root_path = _codex_source_root_path(conn, row.source_root_key)
|
|
5169
|
+
if not _within_root(str(selected.path), root_path):
|
|
5170
|
+
raise _CodexPayloadContainmentError()
|
|
5171
|
+
try:
|
|
5172
|
+
fh = None if streams is None else streams.get(str(selected.path))
|
|
5173
|
+
if fh is None:
|
|
5174
|
+
fh = _rc.open_decoded(selected)
|
|
5175
|
+
if streams is not None:
|
|
5176
|
+
streams[str(selected.path)] = fh
|
|
5177
|
+
try:
|
|
5178
|
+
fh.seek(row.line_offset)
|
|
5179
|
+
line = fh.readline()
|
|
5180
|
+
finally:
|
|
5181
|
+
if streams is None:
|
|
5182
|
+
fh.close()
|
|
5183
|
+
break
|
|
5184
|
+
except OSError as exc:
|
|
5185
|
+
# ContainerError subclasses OSError; preserve its approved leaf
|
|
5186
|
+
# category instead of flattening decoder/integrity errors.
|
|
5187
|
+
replacement = _select_codex_payload_container(row.source_path)
|
|
5188
|
+
if replacement is None:
|
|
5189
|
+
return None
|
|
5190
|
+
if replacement.path != selected.path and attempt == 0:
|
|
5191
|
+
continue # plain vanished during Codex's compression window
|
|
5192
|
+
category = exc.category if isinstance(exc, _rc.ContainerError) else "read-error"
|
|
5193
|
+
raise CodexContainerReadError(category, str(row.source_path)) from exc
|
|
4955
5194
|
try:
|
|
4956
5195
|
obj = json.loads(line.decode("utf-8"))
|
|
4957
5196
|
except (UnicodeDecodeError, ValueError, TypeError):
|
|
@@ -5038,7 +5277,9 @@ def read_codex_payload(
|
|
|
5038
5277
|
full bounded ``card``, and ``truncated`` reflects ``_FULL_PAYLOAD_CEILING``.
|
|
5039
5278
|
``gone`` (→ HTTP 410) means the physical
|
|
5040
5279
|
record moved/mutated; ``not_found`` (→ 404) means no such block, no output
|
|
5041
|
-
partner, or a containment miss (a read is never attempted outside the root).
|
|
5280
|
+
partner, or a containment miss (a read is never attempted outside the root).
|
|
5281
|
+
A present container that cannot be read raises CodexContainerReadError through
|
|
5282
|
+
the existing query-error envelope, including plain-file read failures."""
|
|
5042
5283
|
miss = {"status": "not_found", "block_key": block_key, "which": which}
|
|
5043
5284
|
if which not in ("call", "output", "event"):
|
|
5044
5285
|
return miss
|
|
@@ -5053,7 +5294,14 @@ def read_codex_payload(
|
|
|
5053
5294
|
root_path = _codex_source_root_path(conn, target.source_root_key)
|
|
5054
5295
|
if not _within_root(target.source_path, root_path):
|
|
5055
5296
|
return miss
|
|
5056
|
-
|
|
5297
|
+
streams: dict = {}
|
|
5298
|
+
try:
|
|
5299
|
+
outcome = _reread_codex_full_content(conn, target, streams=streams)
|
|
5300
|
+
except _CodexPayloadContainmentError:
|
|
5301
|
+
return miss
|
|
5302
|
+
finally:
|
|
5303
|
+
for stream in streams.values():
|
|
5304
|
+
stream.close()
|
|
5057
5305
|
if outcome is None:
|
|
5058
5306
|
return {"status": "gone", "block_key": block_key, "which": which}
|
|
5059
5307
|
content, truncated, card = outcome
|