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.
Files changed (97) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +4 -6
  3. package/bin/_cctally_account.py +32 -8
  4. package/bin/_cctally_cache.py +6035 -722
  5. package/bin/_cctally_completion.py +1287 -0
  6. package/bin/_cctally_config.py +37 -0
  7. package/bin/_cctally_core.py +400 -2
  8. package/bin/_cctally_dashboard.py +98 -20
  9. package/bin/_cctally_dashboard_conversation.py +292 -23
  10. package/bin/_cctally_dashboard_envelope.py +16 -3
  11. package/bin/_cctally_dashboard_perf.py +128 -9
  12. package/bin/_cctally_dashboard_sources.py +285 -213
  13. package/bin/_cctally_db.py +1177 -23
  14. package/bin/_cctally_diagnosis_sources.py +22 -4
  15. package/bin/_cctally_doctor.py +572 -45
  16. package/bin/_cctally_five_hour.py +84 -33
  17. package/bin/_cctally_forecast.py +108 -15
  18. package/bin/_cctally_journal.py +3950 -116
  19. package/bin/_cctally_journal_repair.py +101 -12
  20. package/bin/_cctally_milestone_history.py +82 -9
  21. package/bin/_cctally_parser.py +42 -0
  22. package/bin/_cctally_percent_breakdown.py +3 -2
  23. package/bin/_cctally_project.py +8 -2
  24. package/bin/_cctally_quarantine.py +652 -0
  25. package/bin/_cctally_quota.py +227 -37
  26. package/bin/_cctally_quota_model.py +360 -83
  27. package/bin/_cctally_record.py +691 -149
  28. package/bin/_cctally_rederive.py +320 -41
  29. package/bin/_cctally_refresh.py +84 -13
  30. package/bin/_cctally_source_analytics.py +83 -17
  31. package/bin/_cctally_statusline.py +6154 -517
  32. package/bin/_cctally_store.py +367 -53
  33. package/bin/_cctally_transcript.py +8 -7
  34. package/bin/_cctally_tui.py +61 -56
  35. package/bin/_cctally_weekrefs.py +128 -21
  36. package/bin/_lib_account_binding.py +152 -0
  37. package/bin/_lib_account_protocol.py +386 -0
  38. package/bin/_lib_aggregators.py +4 -0
  39. package/bin/_lib_cache_coverage.py +13 -1
  40. package/bin/_lib_cache_report.py +35 -6
  41. package/bin/_lib_cache_writer_lock.py +19 -3
  42. package/bin/_lib_clone_copy.py +142 -0
  43. package/bin/_lib_codex_conversation.py +32 -0
  44. package/bin/_lib_codex_conversation_query.py +297 -49
  45. package/bin/_lib_codex_conversation_watch.py +59 -14
  46. package/bin/_lib_codex_drain_admission.py +214 -0
  47. package/bin/_lib_codex_metadata.py +18 -1
  48. package/bin/_lib_codex_publication_barrier.py +277 -0
  49. package/bin/_lib_codex_rollout_container.py +502 -0
  50. package/bin/_lib_conversation_dispatch.py +3 -0
  51. package/bin/_lib_conversation_query.py +106 -59
  52. package/bin/_lib_conversation_retention.py +1605 -640
  53. package/bin/_lib_cost_provenance.py +53 -2
  54. package/bin/_lib_detector_access_registry.py +3817 -0
  55. package/bin/_lib_doctor.py +714 -69
  56. package/bin/_lib_five_hour.py +126 -0
  57. package/bin/_lib_fmt.py +11 -0
  58. package/bin/_lib_ingest_frontier.py +414 -20
  59. package/bin/_lib_journal.py +141 -10
  60. package/bin/_lib_jsonl.py +5 -4
  61. package/bin/_lib_legacy_spool.py +126 -0
  62. package/bin/_lib_legacy_writer_evidence.py +136 -0
  63. package/bin/_lib_merge_gate.py +231 -86
  64. package/bin/_lib_meter_rate_change.py +10 -0
  65. package/bin/_lib_outcome_quarantine.py +690 -0
  66. package/bin/_lib_perf.py +27 -1
  67. package/bin/_lib_pricing.py +199 -9
  68. package/bin/_lib_pricing_check.py +20 -4
  69. package/bin/_lib_quarantine.py +154 -0
  70. package/bin/_lib_quota_calibration.py +53 -8
  71. package/bin/_lib_quota_copy.py +2 -1
  72. package/bin/_lib_quota_model.py +288 -46
  73. package/bin/_lib_reclaim_planner.py +792 -0
  74. package/bin/_lib_rederive.py +319 -82
  75. package/bin/_lib_segment_summary.py +35 -2
  76. package/bin/_lib_snapshot_cache.py +21 -0
  77. package/bin/_lib_sqlite_close.py +81 -0
  78. package/bin/_lib_statusline_candidates.py +292 -23
  79. package/bin/_lib_subscription_weeks.py +100 -17
  80. package/bin/_lib_tick_stats.py +217 -17
  81. package/bin/_lib_wal_checkpoint.py +1077 -0
  82. package/bin/_lib_write_budget.py +483 -0
  83. package/bin/_lib_write_io.py +703 -0
  84. package/bin/cctally +31 -1
  85. package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
  86. package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
  87. package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
  88. package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
  89. package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
  90. package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
  91. package/dashboard/static/assets/index-BKITk1CT.css +1 -0
  92. package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
  93. package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
  94. package/dashboard/static/dashboard.html +2 -2
  95. package/package.json +19 -1
  96. package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
  97. 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 conn.execute(
359
- "SELECT " + _NARROW_ROW_COLS + " FROM codex_conversation_messages "
360
- "WHERE conversation_key = ? "
361
- "ORDER BY timestamp_utc, source_path, line_offset",
362
- (conversation_key,),
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, t.native_thread_id, "
3422
- "pt.conversation_key, pr.title, pr.project_label, pt.native_thread_id "
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 = pt.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
- """Replace #482 projection rows for the affected conversations.
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
- conn.execute(
3890
- "DELETE FROM codex_find_projection WHERE conversation_key=?",
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
- conn.execute(
3945
- "INSERT INTO codex_find_projection "
3946
- "(message_id,conversation_key,item_key,block_key,"
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
- conn.execute(
4036
- "INSERT INTO cache_meta(key,value) VALUES"
4037
- "('codex_find_projection_generation','1') "
4038
- "ON CONFLICT(key) DO UPDATE SET value=CAST(value AS INTEGER)+1"
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
- def _reread_codex_full_content(conn: sqlite3.Connection, row):
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
- try:
4950
- with open(row.source_path, "rb") as fh:
4951
- fh.seek(row.line_offset)
4952
- line = fh.readline()
4953
- except OSError:
4954
- return None
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
- outcome = _reread_codex_full_content(conn, target)
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