ccx-native 0.0.1 → 0.1.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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -4
  3. package/bin/ccx +55 -2
  4. package/ccx/__init__.py +163 -0
  5. package/ccx/__main__.py +28 -0
  6. package/ccx/claude/__init__.py +0 -0
  7. package/ccx/claude/analytics.py +466 -0
  8. package/ccx/claude/attach.py +762 -0
  9. package/ccx/claude/context.py +465 -0
  10. package/ccx/claude/delivery.py +806 -0
  11. package/ccx/claude/external.py +253 -0
  12. package/ccx/claude/host/__init__.py +0 -0
  13. package/ccx/claude/host/events.py +297 -0
  14. package/ccx/claude/host/hooks.py +438 -0
  15. package/ccx/claude/host/pty.py +250 -0
  16. package/ccx/claude/host/session.py +5184 -0
  17. package/ccx/claude/host/subagents.py +169 -0
  18. package/ccx/claude/host/successor.py +267 -0
  19. package/ccx/claude/managed_turns.py +240 -0
  20. package/ccx/claude/oauth.py +176 -0
  21. package/ccx/claude/settings.py +339 -0
  22. package/ccx/claude/spawn.py +557 -0
  23. package/ccx/claude/tasks.py +72 -0
  24. package/ccx/claude/telemetry.py +282 -0
  25. package/ccx/claude/transcript.py +2281 -0
  26. package/ccx/claude/usage.py +837 -0
  27. package/ccx/claude/usage_control.py +152 -0
  28. package/ccx/cli/__init__.py +0 -0
  29. package/ccx/cli/codex.py +1009 -0
  30. package/ccx/cli/commands/__init__.py +0 -0
  31. package/ccx/cli/commands/_common.py +407 -0
  32. package/ccx/cli/commands/control.py +161 -0
  33. package/ccx/cli/commands/create.py +669 -0
  34. package/ccx/cli/commands/read.py +1020 -0
  35. package/ccx/cli/commands/turns.py +1081 -0
  36. package/ccx/cli/controller.py +757 -0
  37. package/ccx/cli/main.py +301 -0
  38. package/ccx/cli/parser.py +1324 -0
  39. package/ccx/cli/skills.py +215 -0
  40. package/ccx/cli/statusline.py +160 -0
  41. package/ccx/client.py +1900 -0
  42. package/ccx/codex/__init__.py +0 -0
  43. package/ccx/codex/attach_tty.py +267 -0
  44. package/ccx/codex/client.py +732 -0
  45. package/ccx/codex/completion.py +121 -0
  46. package/ccx/codex/control.py +288 -0
  47. package/ccx/codex/daemon/__init__.py +0 -0
  48. package/ccx/codex/daemon/__main__.py +4 -0
  49. package/ccx/codex/daemon/daemon.py +1556 -0
  50. package/ccx/codex/daemon/models.py +140 -0
  51. package/ccx/codex/daemon/native.py +1590 -0
  52. package/ccx/codex/daemon/release.py +367 -0
  53. package/ccx/codex/daemon/worker.py +2396 -0
  54. package/ccx/codex/delivery.py +758 -0
  55. package/ccx/codex/delivery_recovery.py +232 -0
  56. package/ccx/codex/evidence.py +827 -0
  57. package/ccx/codex/history.py +158 -0
  58. package/ccx/codex/notify_history.py +140 -0
  59. package/ccx/codex/ops.py +666 -0
  60. package/ccx/codex/paths.py +70 -0
  61. package/ccx/codex/protocol.py +805 -0
  62. package/ccx/codex/provenance.py +105 -0
  63. package/ccx/codex/read.py +29 -0
  64. package/ccx/codex/records.py +51 -0
  65. package/ccx/codex/reports.py +177 -0
  66. package/ccx/codex/runtime.py +1412 -0
  67. package/ccx/codex/settle.py +363 -0
  68. package/ccx/codex/telemetry.py +792 -0
  69. package/ccx/codex/types.py +659 -0
  70. package/ccx/codex/worker_environment.py +25 -0
  71. package/ccx/constants.py +288 -0
  72. package/ccx/controllers/__init__.py +0 -0
  73. package/ccx/controllers/controller.py +1449 -0
  74. package/ccx/controllers/live_vars.py +1706 -0
  75. package/ccx/controllers/live_vars_runner.py +216 -0
  76. package/ccx/controllers/notify.py +213 -0
  77. package/ccx/controllers/orchestrator.py +193 -0
  78. package/ccx/controllers/policy.py +57 -0
  79. package/ccx/controllers/store.py +681 -0
  80. package/ccx/errors.py +634 -0
  81. package/ccx/notify/__init__.py +0 -0
  82. package/ccx/notify/__main__.py +5 -0
  83. package/ccx/notify/client.py +116 -0
  84. package/ccx/notify/daemon.py +480 -0
  85. package/ccx/notify/deliver.py +304 -0
  86. package/ccx/notify/paths.py +51 -0
  87. package/ccx/notify/registry.py +649 -0
  88. package/ccx/notify/select.py +750 -0
  89. package/ccx/notify/settle.py +684 -0
  90. package/ccx/ops/__init__.py +0 -0
  91. package/ccx/ops/background.py +115 -0
  92. package/ccx/ops/cancel.py +289 -0
  93. package/ccx/ops/codex_import.py +96 -0
  94. package/ccx/ops/common.py +397 -0
  95. package/ccx/ops/compact.py +337 -0
  96. package/ccx/ops/compact_ops.py +550 -0
  97. package/ccx/ops/context_events.py +48 -0
  98. package/ccx/ops/conversation.py +1540 -0
  99. package/ccx/ops/conversation_paging.py +1338 -0
  100. package/ccx/ops/create.py +56 -0
  101. package/ccx/ops/creation.py +952 -0
  102. package/ccx/ops/creation_steps.py +579 -0
  103. package/ccx/ops/event_feed.py +710 -0
  104. package/ccx/ops/forking.py +711 -0
  105. package/ccx/ops/input.py +556 -0
  106. package/ccx/ops/maintenance.py +295 -0
  107. package/ccx/ops/report.py +770 -0
  108. package/ccx/ops/semantics.py +490 -0
  109. package/ccx/ops/send.py +961 -0
  110. package/ccx/ops/steer_ops.py +867 -0
  111. package/ccx/ops/terminate.py +383 -0
  112. package/ccx/ops/turn_reports.py +306 -0
  113. package/ccx/ops/wait.py +762 -0
  114. package/ccx/ops/worker_events.py +636 -0
  115. package/ccx/rpc/__init__.py +0 -0
  116. package/ccx/rpc/local_rpc.py +1602 -0
  117. package/ccx/rpc/rpc.py +559 -0
  118. package/ccx/rpc/serve.py +374 -0
  119. package/ccx/rpc/transport.py +409 -0
  120. package/ccx/rpc/view.py +125 -0
  121. package/ccx/store/__init__.py +0 -0
  122. package/ccx/store/discovery.py +170 -0
  123. package/ccx/store/identity.py +105 -0
  124. package/ccx/store/journal_io.py +79 -0
  125. package/ccx/store/liveness.py +194 -0
  126. package/ccx/store/paths.py +254 -0
  127. package/ccx/store/record_cache.py +40 -0
  128. package/ccx/store/records.py +309 -0
  129. package/ccx/store/root.py +804 -0
  130. package/ccx/store/runtime_presence.py +137 -0
  131. package/ccx/store/store.py +2332 -0
  132. package/ccx/store/worker_state.py +86 -0
  133. package/ccx/system/__init__.py +0 -0
  134. package/ccx/system/bounded.py +296 -0
  135. package/ccx/system/cwd_paths.py +25 -0
  136. package/ccx/system/engine_binary.py +59 -0
  137. package/ccx/system/executable.py +50 -0
  138. package/ccx/system/oslayer.py +667 -0
  139. package/ccx/system/pipes.py +46 -0
  140. package/ccx/system/sanitize.py +43 -0
  141. package/ccx/types.py +705 -0
  142. package/ccx/version.py +29 -0
  143. package/ccx/workers.py +2081 -0
  144. package/ccx.py +9 -0
  145. package/package.json +39 -7
  146. package/pyproject.toml +87 -0
  147. package/skills/ccx/SKILL.md +155 -0
  148. package/skills/ccx/references/controllers.md +244 -0
  149. package/skills/ccx/references/troubleshooting.md +94 -0
  150. package/skills/ccx/references/variables.md +62 -0
@@ -0,0 +1,1338 @@
1
+ """Engine-neutral bounded normalized conversation paging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import binascii
7
+ import copy
8
+ import hashlib
9
+ import json
10
+ import os
11
+ import time
12
+ from typing import Any
13
+
14
+ from ccx.codex import runtime as codex_runtime
15
+ from ccx.ops import conversation as conv
16
+ from ccx.system.bounded import BoundedValue, bound_value, omit_value
17
+ from ccx.errors import (
18
+ CursorInvalidated,
19
+ InvalidRequest,
20
+ OperationTimeout,
21
+ ProviderProtocolError,
22
+ SessionUnavailable,
23
+ )
24
+ from ccx.system.sanitize import safe_native
25
+ from ccx.claude.transcript import (
26
+ PinnedNativeSource,
27
+ _complete_line_end,
28
+ _has_queued_command_marker,
29
+ _is_local_command_output,
30
+ _iter_transcript,
31
+ )
32
+
33
+ _CURSOR_PREFIX = "ccxcp2."
34
+ _CURSOR_VERSION = 3
35
+ _DEFAULT_TARGET = 40
36
+ _MAX_TARGET = 100
37
+
38
+ # V1 has a 4 MiB process-response ceiling. Keep the canonical conversation-page
39
+ # result a full MiB below it so the protocol envelope/request id can never push a
40
+ # valid page over the transport guard. This is a CCX policy, not a caller knob.
41
+ PAGE_RESULT_MAX_BYTES = 3 * 1024 * 1024
42
+ _PAGE_FIELD_MAX_BYTES = 64 * 1024
43
+ _PAGE_ITEM_MAX_BYTES = 256 * 1024
44
+ _PAGE_SINGLE_RECORD_MAX_BYTES = PAGE_RESULT_MAX_BYTES - 256 * 1024
45
+ _PROTECTED_STRING_MAX_BYTES = 4 * 1024
46
+ _PAGE_IDENTITY_STUB_MAX_ITEMS = 64
47
+ _PROTECTED_ITEM_FIELDS = frozenset({
48
+ "kind", "id", "nativeType", "name", "status", "callId",
49
+ "clientId", "origin", "model", "phase", "unpaired",
50
+ })
51
+ _PROTECTED_SUMMARY_FIELDS = frozenset({"verb", "target", "description"})
52
+
53
+
54
+ def _encode_cursor(payload: dict[str, Any]) -> str:
55
+ raw = json.dumps(
56
+ {"v": _CURSOR_VERSION, **payload},
57
+ sort_keys=True,
58
+ separators=(",", ":"),
59
+ ensure_ascii=False,
60
+ ).encode("utf-8")
61
+ return _CURSOR_PREFIX + base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
62
+
63
+
64
+ def _decode_cursor(cursor: str, *, engine: str, worker_id: str) -> dict[str, Any]:
65
+ if not isinstance(cursor, str) or not cursor.startswith(_CURSOR_PREFIX):
66
+ raise InvalidRequest("invalid conversation page cursor")
67
+ token = cursor[len(_CURSOR_PREFIX):]
68
+ try:
69
+ raw = base64.urlsafe_b64decode(token + "=" * (-len(token) % 4))
70
+ payload = json.loads(raw)
71
+ except (ValueError, UnicodeDecodeError, binascii.Error) as exc:
72
+ raise InvalidRequest("invalid conversation page cursor") from exc
73
+ if not isinstance(payload, dict) or payload.get("v") != _CURSOR_VERSION:
74
+ raise CursorInvalidated(
75
+ "conversation page cursor version is no longer valid", short=worker_id[:8]
76
+ )
77
+ if payload.get("engine") != engine or payload.get("worker_id") != worker_id:
78
+ raise InvalidRequest("conversation page cursor belongs to another worker")
79
+ return payload
80
+
81
+
82
+ def _validate_options(
83
+ source_record_target: int,
84
+ max_tool_result_bytes: int | None,
85
+ timeout: float,
86
+ ) -> None:
87
+ if (
88
+ isinstance(source_record_target, bool)
89
+ or not isinstance(source_record_target, int)
90
+ or not 1 <= source_record_target <= _MAX_TARGET
91
+ ):
92
+ raise InvalidRequest(
93
+ f"source_record_target must be between 1 and {_MAX_TARGET}"
94
+ )
95
+ conv._requested_label(max_tool_result_bytes)
96
+ if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or timeout <= 0:
97
+ raise InvalidRequest("conversation page timeout must be greater than zero")
98
+
99
+
100
+ def _encoded_size(value: Any) -> int:
101
+ return len(json.dumps(
102
+ value, ensure_ascii=False, allow_nan=False, separators=(",", ":"), sort_keys=True
103
+ ).encode("utf-8"))
104
+
105
+
106
+ def _nested_path(prefix: str, relative: str) -> str:
107
+ if not relative or relative == "$":
108
+ return prefix
109
+ if relative.startswith("["):
110
+ return f"{prefix}{relative}"
111
+ return f"{prefix}.{relative}"
112
+
113
+
114
+ def _truncation(
115
+ path: str,
116
+ bounded: BoundedValue,
117
+ *,
118
+ source: str = "ccx-page",
119
+ force_omitted: bool = False,
120
+ original_byte_count: int | None = None,
121
+ returned_byte_count: int | None = None,
122
+ sha256: str | None = None,
123
+ ) -> dict[str, Any]:
124
+ omitted = force_omitted or bounded.omitted
125
+ value = {
126
+ "path": path,
127
+ "source": source,
128
+ "original_byte_count": (
129
+ bounded.original_byte_count if original_byte_count is None else original_byte_count
130
+ ),
131
+ "returned_byte_count": (
132
+ (0 if omitted else bounded.returned_byte_count)
133
+ if returned_byte_count is None
134
+ else returned_byte_count
135
+ ),
136
+ "sha256": bounded.sha256 if sha256 is None else sha256,
137
+ }
138
+ if omitted:
139
+ value["omitted"] = True
140
+ if bounded.changed_paths:
141
+ value["paths"] = [
142
+ _nested_path(path, relative) for relative in bounded.changed_paths
143
+ ]
144
+ if bounded.changed_path_count:
145
+ value["changed_path_count"] = bounded.changed_path_count
146
+ if bounded.omitted_count:
147
+ value["omitted_path_count"] = bounded.omitted_count
148
+ return value
149
+
150
+
151
+ def _existing_tool_result_truncation(result: dict[str, Any]) -> dict[str, Any] | None:
152
+ if result.get("truncated") is not True:
153
+ return None
154
+ original = result.get("originalByteCount")
155
+ returned = result.get("returnedByteCount")
156
+ digest = result.get("contentSha256")
157
+ if (
158
+ not isinstance(original, int)
159
+ or isinstance(original, bool)
160
+ or not isinstance(returned, int)
161
+ or isinstance(returned, bool)
162
+ or not isinstance(digest, str)
163
+ or not digest
164
+ ):
165
+ return None
166
+ return {
167
+ "path": "result.content",
168
+ "source": "max_tool_result_bytes",
169
+ "original_byte_count": original,
170
+ "returned_byte_count": returned,
171
+ "sha256": digest,
172
+ }
173
+
174
+
175
+ def _project_protected(
176
+ key: str, entry: Any, truncations: list[dict[str, Any]]
177
+ ) -> Any:
178
+ bounded = bound_value(entry, _PROTECTED_STRING_MAX_BYTES)
179
+ if bounded.truncated:
180
+ truncations.append(_truncation(key, bounded))
181
+ return bounded.value
182
+
183
+
184
+ def _project_summary(entry: Any, truncations: list[dict[str, Any]]) -> Any:
185
+ if not isinstance(entry, dict):
186
+ return _project_protected("summary", entry, truncations)
187
+ value: dict[str, Any] = {}
188
+ for key, field in entry.items():
189
+ if field is None:
190
+ continue
191
+ limit = (
192
+ _PROTECTED_STRING_MAX_BYTES
193
+ if key in _PROTECTED_SUMMARY_FIELDS
194
+ else _PAGE_FIELD_MAX_BYTES
195
+ )
196
+ bounded = bound_value(field, limit)
197
+ if not bounded.omitted:
198
+ value[key] = bounded.value
199
+ if bounded.truncated:
200
+ truncations.append(_truncation(f"summary.{key}", bounded))
201
+ return value
202
+
203
+
204
+ def _project_item_once(item: dict[str, Any], field_limit: int) -> dict[str, Any]:
205
+ value: dict[str, Any] = {}
206
+ truncations: list[dict[str, Any]] = []
207
+ for key, entry in item.items():
208
+ if key in ("ordinal", "startedAtMs", "completedAtMs", "timingSource"):
209
+ continue
210
+ if entry is None:
211
+ continue
212
+ if key in _PROTECTED_ITEM_FIELDS:
213
+ value[key] = _project_protected(key, entry, truncations)
214
+ continue
215
+ if key == "summary":
216
+ value[key] = _project_summary(entry, truncations)
217
+ continue
218
+ if key == "result" and isinstance(entry, dict):
219
+ projected = copy.deepcopy(entry)
220
+ existing = _existing_tool_result_truncation(projected)
221
+ if existing is not None:
222
+ truncations.append(existing)
223
+ if projected.get("content") is not None:
224
+ bounded = bound_value(projected["content"], field_limit)
225
+ if bounded.omitted:
226
+ projected.pop("content", None)
227
+ else:
228
+ projected["content"] = bounded.value
229
+ if bounded.truncated:
230
+ if existing is not None:
231
+ truncations.append({
232
+ "path": "result.content",
233
+ "source": "ccx-page",
234
+ "original_byte_count": existing["original_byte_count"],
235
+ "returned_byte_count": (
236
+ 0 if bounded.omitted else bounded.returned_byte_count
237
+ ),
238
+ "sha256": existing["sha256"],
239
+ **({"omitted": True} if bounded.omitted else {}),
240
+ })
241
+ projected["originalByteCount"] = existing["original_byte_count"]
242
+ projected["contentSha256"] = existing["sha256"]
243
+ else:
244
+ truncations.append(_truncation("result.content", bounded))
245
+ projected["originalByteCount"] = bounded.original_byte_count
246
+ projected["contentSha256"] = bounded.sha256
247
+ projected["truncated"] = True
248
+ projected["returnedByteCount"] = (
249
+ 0 if bounded.omitted else bounded.returned_byte_count
250
+ )
251
+ value[key] = projected
252
+ continue
253
+ bounded = bound_value(entry, field_limit)
254
+ if not bounded.omitted:
255
+ value[key] = bounded.value
256
+ if bounded.truncated:
257
+ truncations.append(_truncation(key, bounded))
258
+ if truncations:
259
+ value["truncations"] = truncations
260
+ return value
261
+
262
+
263
+ def _minimal_public_item(item: dict[str, Any]) -> dict[str, Any]:
264
+ value: dict[str, Any] = {}
265
+ truncations: list[dict[str, Any]] = []
266
+ for key, entry in item.items():
267
+ if key in _PROTECTED_ITEM_FIELDS and entry is not None:
268
+ value[key] = _project_protected(key, entry, truncations)
269
+ if item.get("summary") is not None:
270
+ value["summary"] = _project_summary(item["summary"], truncations)
271
+
272
+ for key, entry in item.items():
273
+ if (
274
+ key in _PROTECTED_ITEM_FIELDS
275
+ or key == "summary"
276
+ or key in ("ordinal", "startedAtMs", "completedAtMs", "timingSource")
277
+ or entry is None
278
+ ):
279
+ continue
280
+ if key == "result" and isinstance(entry, dict):
281
+ existing = _existing_tool_result_truncation(entry)
282
+ if existing is not None:
283
+ truncations.append(existing)
284
+ content = entry.get("content")
285
+ if content is not None:
286
+ if existing is not None:
287
+ truncations.append({
288
+ "path": "result.content",
289
+ "source": "ccx-page",
290
+ "original_byte_count": existing["original_byte_count"],
291
+ "returned_byte_count": 0,
292
+ "sha256": existing["sha256"],
293
+ "omitted": True,
294
+ })
295
+ else:
296
+ truncations.append(_truncation(
297
+ "result.content", omit_value(content), force_omitted=True
298
+ ))
299
+ else:
300
+ truncations.append(_truncation(
301
+ "result", omit_value(entry), force_omitted=True
302
+ ))
303
+ continue
304
+ truncations.append(_truncation(
305
+ key, omit_value(entry), force_omitted=True
306
+ ))
307
+ if truncations:
308
+ value["truncations"] = truncations
309
+ return value
310
+
311
+
312
+ def _public_item(
313
+ item: dict[str, Any], *, item_budget: int = _PAGE_ITEM_MAX_BYTES
314
+ ) -> dict[str, Any]:
315
+ variable = sum(
316
+ 1
317
+ for key, entry in item.items()
318
+ if key not in _PROTECTED_ITEM_FIELDS
319
+ and key != "summary"
320
+ and key not in ("ordinal", "startedAtMs", "completedAtMs", "timingSource")
321
+ and entry is not None
322
+ )
323
+ field_limit = min(
324
+ _PAGE_FIELD_MAX_BYTES,
325
+ max(256, item_budget // max(1, variable + 1)),
326
+ )
327
+ while True:
328
+ value = _project_item_once(item, field_limit)
329
+ if _encoded_size(value) <= item_budget:
330
+ return value
331
+ if field_limit <= 256:
332
+ break
333
+ field_limit = max(256, field_limit // 2)
334
+ return _minimal_public_item(item)
335
+
336
+
337
+ def _project_record_identity(
338
+ path: str, value: Any, truncations: list[dict[str, Any]]
339
+ ) -> Any:
340
+ if not isinstance(value, str):
341
+ return copy.deepcopy(value)
342
+ bounded = bound_value(value, _PROTECTED_STRING_MAX_BYTES)
343
+ if bounded.truncated:
344
+ truncations.append(_truncation(path, bounded))
345
+ return bounded.value
346
+
347
+
348
+ def _record_value(
349
+ *,
350
+ turn_id: Any,
351
+ source_record_id: Any,
352
+ timestamp_ms: Any,
353
+ duration_ms: Any,
354
+ items: list[dict[str, Any]],
355
+ usage: dict[str, Any] | None,
356
+ record_truncations: list[dict[str, Any]],
357
+ ) -> dict[str, Any]:
358
+ value = {
359
+ "turn_id": turn_id,
360
+ "source_record_id": source_record_id,
361
+ "timestamp_ms": timestamp_ms,
362
+ "duration_ms": duration_ms,
363
+ "usage": usage,
364
+ "items": items,
365
+ }
366
+ if record_truncations:
367
+ value["truncations"] = record_truncations
368
+ return value
369
+
370
+
371
+ def _public_record(
372
+ record: dict[str, Any], counters: conv._Counters | None = None
373
+ ) -> dict[str, Any] | None:
374
+ raw_items = [item for item in (record.get("items") or []) if isinstance(item, dict)]
375
+ raw_usage = record.get("usage")
376
+ if not raw_items and not raw_usage:
377
+ return None
378
+ counters = counters or conv._Counters()
379
+
380
+ record_truncations: list[dict[str, Any]] = []
381
+ turn_id = _project_record_identity(
382
+ "turn_id", record.get("turn_id"), record_truncations
383
+ )
384
+ source_record_id = _project_record_identity(
385
+ "source_record_id", record.get("source_record_id"), record_truncations
386
+ )
387
+ timestamp_ms = record.get("timestamp_ms")
388
+ duration_ms = record.get("duration_ms")
389
+ usage = copy.deepcopy(raw_usage) if isinstance(raw_usage, dict) else None
390
+ if isinstance(raw_usage, dict):
391
+ bounded_usage = bound_value(raw_usage, _PAGE_FIELD_MAX_BYTES)
392
+ usage = None if bounded_usage.omitted else bounded_usage.value
393
+ if bounded_usage.truncated:
394
+ record_truncations.append(_truncation("usage", bounded_usage))
395
+
396
+ fair_item_budget = max(
397
+ 2048, (_PAGE_SINGLE_RECORD_MAX_BYTES - 32 * 1024) // max(1, len(raw_items))
398
+ )
399
+ # Start with a richer per-item preview than the strict fair share, then let
400
+ # the record-level loop shrink it only when the aggregate record actually
401
+ # needs that reduction. This keeps the shrink loop meaningful while bounding
402
+ # it to at most a small number of passes for high-cardinality records.
403
+ item_budget = min(_PAGE_ITEM_MAX_BYTES, fair_item_budget * 2)
404
+ while True:
405
+ items = [_public_item(item, item_budget=item_budget) for item in raw_items]
406
+ value = _record_value(
407
+ turn_id=turn_id, source_record_id=source_record_id,
408
+ timestamp_ms=timestamp_ms, duration_ms=duration_ms,
409
+ items=items, usage=usage, record_truncations=list(record_truncations),
410
+ )
411
+ if _encoded_size(value) <= _PAGE_SINGLE_RECORD_MAX_BYTES:
412
+ return value
413
+ if item_budget <= 2048:
414
+ break
415
+ item_budget = max(2048, item_budget // 2)
416
+
417
+ # Pathological child cardinality/protected metadata must not make one source
418
+ # record a permanent cursor barrier. Keep a bounded source-order prefix of
419
+ # identity-only stubs and explicitly count every omitted normalized item.
420
+ stub_count = min(len(raw_items), _PAGE_IDENTITY_STUB_MAX_ITEMS)
421
+ while True:
422
+ stubs = [_minimal_public_item(item) for item in raw_items[:stub_count]]
423
+ value = _record_value(
424
+ turn_id=turn_id, source_record_id=source_record_id,
425
+ timestamp_ms=timestamp_ms, duration_ms=duration_ms,
426
+ items=stubs, usage=usage, record_truncations=list(record_truncations),
427
+ )
428
+ if _encoded_size(value) <= _PAGE_SINGLE_RECORD_MAX_BYTES or stub_count == 0:
429
+ break
430
+ stub_count //= 2
431
+
432
+ omitted = len(raw_items) - stub_count
433
+ if omitted:
434
+ counters.omitted += omitted
435
+ counters.warn(
436
+ f"{omitted} normalized item(s) omitted from one conversation source "
437
+ "record to fit the bounded page"
438
+ )
439
+ return value
440
+
441
+
442
+ def _truncated_field_count(records: list[dict[str, Any]]) -> int:
443
+ return sum(
444
+ len(record.get("truncations") or [])
445
+ + sum(
446
+ len(item.get("truncations") or [])
447
+ for item in (record.get("items") or [])
448
+ if isinstance(item, dict)
449
+ )
450
+ for record in records
451
+ )
452
+
453
+
454
+ def _bounded_warnings(counters: conv._Counters) -> tuple[list[str], int]:
455
+ warnings: list[str] = []
456
+ truncated = 0
457
+ for warning in counters.warning_list():
458
+ bounded = bound_value(str(warning), _PROTECTED_STRING_MAX_BYTES)
459
+ warnings.append(str(bounded.value))
460
+ if bounded.truncated:
461
+ truncated += 1
462
+ return warnings, truncated
463
+
464
+
465
+ def _last_resort_record_stub(
466
+ record: dict[str, Any], counters: conv._Counters
467
+ ) -> dict[str, Any]:
468
+ value = copy.deepcopy(record)
469
+ omitted = len(value.get("items") or [])
470
+ value["items"] = []
471
+ if omitted:
472
+ counters.omitted += omitted
473
+ counters.warn(
474
+ f"{omitted} projected item(s) omitted from one conversation source "
475
+ "record at the final page-budget fence"
476
+ )
477
+ return value
478
+
479
+
480
+ def _page_completeness(
481
+ counters: conv._Counters,
482
+ *,
483
+ requested_source_record_target: int,
484
+ returned_source_record_count: int,
485
+ response_budget_limited: bool,
486
+ truncated_field_count: int,
487
+ ) -> dict[str, Any]:
488
+ if counters.malformed or counters.omitted:
489
+ status = "partial"
490
+ elif counters.truncated or truncated_field_count or response_budget_limited:
491
+ status = "bounded"
492
+ else:
493
+ status = "complete"
494
+ warnings, warning_truncated_count = _bounded_warnings(counters)
495
+ return {
496
+ "status": status,
497
+ "truncated_item_count": counters.truncated,
498
+ "truncated_field_count": truncated_field_count,
499
+ "omitted_item_count": counters.omitted,
500
+ "malformed_line_count": counters.malformed,
501
+ "requested_source_record_target": requested_source_record_target,
502
+ "returned_source_record_count": returned_source_record_count,
503
+ "response_budget_limited": response_budget_limited,
504
+ "warning_truncated_count": warning_truncated_count,
505
+ "warnings": warnings,
506
+ }
507
+
508
+
509
+ def _page_result(
510
+ worker: str,
511
+ records: list[dict[str, Any]],
512
+ older_cursor: str | None,
513
+ counters: conv._Counters,
514
+ *,
515
+ requested_source_record_target: int,
516
+ response_budget_limited: bool,
517
+ ) -> dict[str, Any]:
518
+ return {
519
+ "worker": worker,
520
+ "records": records,
521
+ "older_cursor": older_cursor,
522
+ "completeness": _page_completeness(
523
+ counters,
524
+ requested_source_record_target=requested_source_record_target,
525
+ returned_source_record_count=len(records),
526
+ response_budget_limited=response_budget_limited,
527
+ truncated_field_count=_truncated_field_count(records),
528
+ ),
529
+ }
530
+
531
+
532
+ def _event_fingerprint(evt: dict[str, Any]) -> str:
533
+ return hashlib.sha256(
534
+ json.dumps(
535
+ safe_native(evt),
536
+ sort_keys=True,
537
+ separators=(",", ":"),
538
+ ensure_ascii=False,
539
+ default=str,
540
+ ).encode("utf-8")
541
+ ).hexdigest()
542
+
543
+
544
+ def _ordinary_user_boundary(evt: dict[str, Any]) -> bool:
545
+ if evt.get("type") != "user" or evt.get("isMeta") or evt.get("isCompactSummary"):
546
+ return False
547
+ if _has_queued_command_marker(evt):
548
+ return False
549
+ content = (evt.get("message") or {}).get("content")
550
+ if isinstance(content, str):
551
+ if conv._command_from_text(content) is not None:
552
+ return False
553
+ return bool(content.strip()) and not _is_local_command_output(content)
554
+ if not isinstance(content, list):
555
+ return False
556
+ for block in content:
557
+ if not isinstance(block, dict):
558
+ continue
559
+ kind = block.get("type")
560
+ if kind == "tool_result":
561
+ continue
562
+ if kind == "text":
563
+ if (block.get("text") or "").strip():
564
+ return True
565
+ continue
566
+ if kind is not None:
567
+ return True
568
+ return False
569
+
570
+
571
+ def _boundary_from_event(evt: dict[str, Any]) -> dict[str, Any] | None:
572
+ offset = evt.get("_ccx_start_offset")
573
+ if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
574
+ return None
575
+ return {
576
+ "offset": offset,
577
+ "source_record_id": evt.get("uuid"),
578
+ "fingerprint": _event_fingerprint(evt),
579
+ }
580
+
581
+
582
+ def _validate_anchor(
583
+ path: str,
584
+ candidate_offset: int,
585
+ *,
586
+ before_offset: int,
587
+ ) -> dict[str, Any] | None:
588
+ for evt in _iter_transcript(
589
+ path, after_offset=candidate_offset, before_offset=before_offset
590
+ ):
591
+ if not conv._claude_page_bearing(evt):
592
+ continue
593
+ if not _ordinary_user_boundary(evt):
594
+ return None
595
+ return _boundary_from_event(evt)
596
+ return None
597
+
598
+
599
+ def _event_at(path: str, offset: int, *, before_offset: int) -> dict[str, Any] | None:
600
+ for evt in _iter_transcript(path, after_offset=offset, before_offset=before_offset):
601
+ if evt.get("_ccx_start_offset") != offset:
602
+ return None
603
+ return evt
604
+ return None
605
+
606
+
607
+ def _context_end_offset(
608
+ path: str,
609
+ cut_offset: int,
610
+ *,
611
+ source_eof: int,
612
+ ) -> int:
613
+ """Upper normalization fence for the history immediately before a page cut.
614
+
615
+ A real user prompt starts a fresh semantic group. To render records just
616
+ before a cut, look ahead only until the next such prompt; that is enough to
617
+ pair tool results and assistant fragments from the same turn without
618
+ reparsing every newer turn through current EOF.
619
+ """
620
+ for evt in _iter_transcript(
621
+ path, after_offset=cut_offset, before_offset=source_eof
622
+ ):
623
+ start = evt.get("_ccx_start_offset")
624
+ if not isinstance(start, int) or start < cut_offset:
625
+ continue
626
+ if _ordinary_user_boundary(evt):
627
+ return start
628
+ return source_eof
629
+
630
+
631
+ def _record_matches(
632
+ path: str,
633
+ offset: int,
634
+ source_record_id: Any,
635
+ fingerprint: Any,
636
+ *,
637
+ before_offset: int,
638
+ ) -> bool:
639
+ evt = _event_at(path, offset, before_offset=before_offset)
640
+ if evt is None:
641
+ return False
642
+ actual_id = evt.get("uuid")
643
+ if isinstance(source_record_id, str) and source_record_id:
644
+ return actual_id == source_record_id
645
+ return isinstance(fingerprint, str) and _event_fingerprint(evt) == fingerprint
646
+
647
+
648
+ def _reverse_lines(
649
+ path: str | PinnedNativeSource, *, before_offset: int,
650
+ block_size: int = 64 * 1024,
651
+ ):
652
+ """Yield (start offset, raw line) backwards from a certified source fence.
653
+
654
+ The pinned reader uses pread, not the mutable native pathname or shared fd
655
+ seek position. The buffer also handles JSONL rows larger than a read block.
656
+ """
657
+ try:
658
+ reader = (path.open_reader() if isinstance(path, PinnedNativeSource)
659
+ else open(path, "rb"))
660
+ with reader as handle:
661
+ position = before_offset
662
+ suffix = b""
663
+ while position > 0:
664
+ size = min(block_size, position)
665
+ position -= size
666
+ handle.seek(position)
667
+ data = handle.read(size) + suffix
668
+ parts = data.split(b"\n")
669
+ if position > 0:
670
+ suffix = parts[0]
671
+ parts = parts[1:]
672
+ else:
673
+ suffix = b""
674
+ end = position + len(data)
675
+ for raw in reversed(parts):
676
+ start = end - len(raw)
677
+ if raw:
678
+ yield start, raw
679
+ end = start - 1
680
+ except FileNotFoundError:
681
+ return
682
+
683
+
684
+ def _reverse_boundary_events(path: str | PinnedNativeSource, *, before_offset: int):
685
+ """Find ordinary-user starts newest-first, without hashing skipped rows."""
686
+ for start, raw in _reverse_lines(path, before_offset=before_offset):
687
+ raw = raw.strip()
688
+ if not raw:
689
+ continue
690
+ try:
691
+ evt = json.loads(raw)
692
+ except (ValueError, UnicodeDecodeError):
693
+ continue
694
+ if isinstance(evt, dict):
695
+ if evt.get("isSidechain") is True:
696
+ continue
697
+ evt["_ccx_start_offset"] = start
698
+ if _ordinary_user_boundary(evt):
699
+ yield evt
700
+
701
+
702
+ def claude_tail_offset(path: str | PinnedNativeSource, *, before_offset: int,
703
+ turns: int) -> int:
704
+ """Where the newest ``turns`` user turns start, reading backwards; 0 if fewer."""
705
+ seen = 0
706
+ for evt in _reverse_boundary_events(path, before_offset=before_offset):
707
+ seen += 1
708
+ if seen >= turns:
709
+ return evt["_ccx_start_offset"]
710
+ return 0
711
+
712
+
713
+ def _normalize_claude_window(
714
+ path: str,
715
+ *,
716
+ after_offset: int,
717
+ before_offset: int,
718
+ max_tool_result_bytes: int | None,
719
+ ) -> tuple[list[dict[str, Any]], conv._Counters]:
720
+ raw_stats: dict[str, int] = {"malformedLines": 0}
721
+ events = list(
722
+ _iter_transcript(
723
+ path,
724
+ raw_stats,
725
+ after_offset=after_offset,
726
+ before_offset=before_offset,
727
+ )
728
+ )
729
+ counters = conv._Counters()
730
+ counters.malformed = raw_stats.get("malformedLines", 0)
731
+ if counters.malformed:
732
+ counters.warn(
733
+ f"{counters.malformed} malformed native JSONL line(s) skipped"
734
+ )
735
+ _turns, _model, records = conv._normalize_claude_events(
736
+ events, counters, max_tool_result_bytes
737
+ )
738
+ return records, counters
739
+
740
+
741
+ def _bearing_before(
742
+ records: list[dict[str, Any]],
743
+ cut_offset: int,
744
+ ) -> list[dict[str, Any]]:
745
+ return [
746
+ record for record in records
747
+ if record.get("_ccx_page_bearing")
748
+ and isinstance(record.get("_ccx_start_offset"), int)
749
+ and record["_ccx_start_offset"] < cut_offset
750
+ ]
751
+
752
+
753
+ def _choose_claude_window(
754
+ path: str,
755
+ source: dict[str, int],
756
+ *,
757
+ cut_offset: int,
758
+ normalization_end_offset: int,
759
+ target: int,
760
+ max_tool_result_bytes: int | None,
761
+ anchor_hint: dict[str, Any] | None = None,
762
+ ) -> tuple[dict[str, Any], list[dict[str, Any]], conv._Counters]:
763
+ def normalize_from(boundary: dict[str, Any]):
764
+ records, counters = _normalize_claude_window(
765
+ path,
766
+ after_offset=boundary["offset"],
767
+ before_offset=normalization_end_offset,
768
+ max_tool_result_bytes=max_tool_result_bytes,
769
+ )
770
+ return _bearing_before(records, cut_offset), counters
771
+
772
+ # Older-page cursors carry a validated normalization anchor.
773
+ if (
774
+ isinstance(anchor_hint, dict)
775
+ and isinstance(anchor_hint.get("offset"), int)
776
+ and 0 <= anchor_hint["offset"] < cut_offset
777
+ ):
778
+ eligible, counters = normalize_from(anchor_hint)
779
+ if len(eligible) >= target or anchor_hint["offset"] == 0:
780
+ return anchor_hint, eligible, counters
781
+
782
+ # Probe the same 4, 8, 16... user starts as the old forward full scan, but
783
+ # stop reading the pinned source as soon as a window has enough records.
784
+ # If fewer than the next probe distance exist, also try the oldest start.
785
+ distance = 4
786
+ count = 0
787
+ last_probed = 0
788
+ oldest = None
789
+ for evt in _reverse_boundary_events(path, before_offset=cut_offset):
790
+ count += 1
791
+ oldest = evt
792
+ if count != distance:
793
+ continue
794
+ eligible, counters = normalize_from({"offset": evt["_ccx_start_offset"]})
795
+ if len(eligible) >= target:
796
+ return _boundary_from_event(evt), eligible, counters
797
+ last_probed = count
798
+ distance *= 2
799
+ if oldest is not None and count != last_probed:
800
+ eligible, counters = normalize_from({"offset": oldest["_ccx_start_offset"]})
801
+ if len(eligible) >= target:
802
+ return _boundary_from_event(oldest), eligible, counters
803
+
804
+ zero = {"offset": 0, "source_record_id": None, "fingerprint": None}
805
+ eligible, counters = normalize_from(zero)
806
+ return zero, eligible, counters
807
+
808
+
809
+ def _source_transition_is_append_only(
810
+ before: dict[str, int],
811
+ after: dict[str, int],
812
+ ) -> bool:
813
+ """Whether the after snapshot is compatible with an as-of-before EOF read.
814
+
815
+ Claude's native transcript is append-only in ordinary operation. Growth
816
+ beyond the snapshotted complete-line EOF is harmless because every bounded
817
+ parser is fenced at that earlier EOF. Replacement, truncation, or a same-size
818
+ rewrite invalidates the read.
819
+ """
820
+ if before["dev"] != after["dev"] or before["ino"] != after["ino"]:
821
+ return False
822
+ if after["eof"] < before["eof"] or after["size"] < before["eof"]:
823
+ return False
824
+ if after["eof"] > before["eof"]:
825
+ return True
826
+ if after["size"] > before["size"]:
827
+ # A partial trailing append need not advance the complete-line EOF yet.
828
+ return True
829
+ if after["size"] != before["size"]:
830
+ return False
831
+ return after["mtime_ns"] == before["mtime_ns"]
832
+
833
+
834
+ def _unstable_source_error(short: str, *, cursor: bool) -> Exception:
835
+ if cursor:
836
+ return CursorInvalidated(
837
+ "conversation page source changed during the read; restart paging",
838
+ short=short,
839
+ )
840
+ return SessionUnavailable(
841
+ "Claude transcript changed non-append-only while reading; retry the page",
842
+ short=short,
843
+ )
844
+
845
+
846
+ def _validate_claude_cursor(
847
+ payload: dict[str, Any],
848
+ short: str,
849
+ path: str,
850
+ source: dict[str, int],
851
+ scope: dict[str, str],
852
+ ) -> tuple[int, int]:
853
+ if any(payload.get(key) != scope[key] for key in (
854
+ "root", "generation", "session_id"
855
+ )):
856
+ raise CursorInvalidated(
857
+ "Conversation worker changed; restart paging", short=short
858
+ )
859
+ if payload.get("dev") != source["dev"] or payload.get("ino") != source["ino"]:
860
+ raise CursorInvalidated(
861
+ "conversation page source was replaced; restart paging", short=short
862
+ )
863
+ cut_offset = payload.get("cut_offset")
864
+ if (
865
+ not isinstance(cut_offset, int)
866
+ or isinstance(cut_offset, bool)
867
+ or cut_offset < 0
868
+ or cut_offset >= source["eof"]
869
+ ):
870
+ raise CursorInvalidated(
871
+ "conversation page source was truncated; restart paging", short=short
872
+ )
873
+ if not _record_matches(
874
+ path,
875
+ cut_offset,
876
+ payload.get("cut_id"),
877
+ payload.get("cut_fingerprint"),
878
+ before_offset=source["eof"],
879
+ ):
880
+ raise CursorInvalidated(
881
+ "Conversation history changed; restart paging",
882
+ short=short,
883
+ )
884
+ # The fingerprint must match even when the original row has a native UUID.
885
+ # Both proofs are carried so an edited cursor cannot silently loosen either.
886
+ cut = _event_at(path, cut_offset, before_offset=source["eof"])
887
+ if cut is None or payload.get("cut_fingerprint") != _event_fingerprint(cut):
888
+ raise CursorInvalidated("conversation page cut fingerprint changed", short=short)
889
+ # Check the original snapshot fence, not a newly appended tail. A cursor
890
+ # may remain valid after append but must not shorten semantic lookahead.
891
+ source_eof = payload.get("source_eof")
892
+ if (type(source_eof) is not int or not cut_offset < source_eof <= source["eof"]):
893
+ raise CursorInvalidated("Conversation source changed; restart paging", short=short)
894
+ context_end_offset = payload.get("context_end_offset")
895
+ if (
896
+ not isinstance(context_end_offset, int)
897
+ or isinstance(context_end_offset, bool)
898
+ or context_end_offset < cut_offset
899
+ or context_end_offset > source_eof
900
+ or context_end_offset != _context_end_offset(
901
+ path, cut_offset, source_eof=source_eof
902
+ )
903
+ ):
904
+ raise CursorInvalidated(
905
+ "Conversation context changed; restart paging",
906
+ short=short,
907
+ )
908
+
909
+ anchor_offset = payload.get("anchor_offset")
910
+ if (type(anchor_offset) is not int or anchor_offset < 0
911
+ or anchor_offset >= cut_offset):
912
+ raise CursorInvalidated("conversation page anchor is no longer valid", short=short)
913
+ # Byte zero may be a synthetic fallback or a real ordinary-user anchor.
914
+ anchor = _validate_anchor(path, anchor_offset, before_offset=source_eof)
915
+ if anchor_offset == 0 and (anchor is None or anchor["offset"] != 0):
916
+ if payload.get("anchor_id") is not None or payload.get("anchor_fingerprint") is not None:
917
+ raise CursorInvalidated("conversation page zero anchor changed", short=short)
918
+ else:
919
+ if (
920
+ anchor is None
921
+ or anchor["offset"] != anchor_offset
922
+ or _cursor_id(anchor.get("source_record_id")) != payload.get("anchor_id")
923
+ or anchor.get("fingerprint") != payload.get("anchor_fingerprint")
924
+ ):
925
+ raise CursorInvalidated(
926
+ "conversation page normalization anchor is no longer valid; restart paging",
927
+ short=short,
928
+ )
929
+ return cut_offset, context_end_offset
930
+
931
+
932
+ def _cursor_id(value: Any) -> str | None:
933
+ if not isinstance(value, str) or not value:
934
+ return None
935
+ if len(value.encode("utf-8")) > _PROTECTED_STRING_MAX_BYTES:
936
+ return None
937
+ return value
938
+
939
+
940
+ def _claude_older_cursor(
941
+ scope: dict[str, str],
942
+ short: str,
943
+ path: str,
944
+ source: dict[str, int],
945
+ anchor: dict[str, Any],
946
+ eligible: list[dict[str, Any]],
947
+ first: dict[str, Any] | None,
948
+ ) -> str | None:
949
+ if first is None:
950
+ return None
951
+ first_offset = first["_ccx_start_offset"]
952
+ has_older_here = any(
953
+ isinstance(record.get("_ccx_start_offset"), int)
954
+ and record["_ccx_start_offset"] < first_offset
955
+ for record in eligible
956
+ )
957
+ has_older_before_anchor = anchor["offset"] > 0
958
+ if first_offset <= 0 or not (
959
+ has_older_here or has_older_before_anchor
960
+ ):
961
+ return None
962
+ if first_offset == anchor["offset"]:
963
+ # The first retained record can be the normalization anchor itself.
964
+ # An older cursor must point to a strictly earlier anchor, or its next
965
+ # read fails validation even when the original transcript is unchanged.
966
+ # Rebase on the preceding ordinary user, using the same pinned source;
967
+ # if none exists, the byte-zero fallback covers the earlier prelude.
968
+ previous = next(_reverse_boundary_events(path, before_offset=first_offset), None)
969
+ anchor = (_boundary_from_event(previous) if previous is not None else
970
+ {"offset": 0, "source_record_id": None, "fingerprint": None})
971
+ context_end_offset = _context_end_offset(
972
+ path, first_offset, source_eof=source["eof"]
973
+ )
974
+ return _encode_cursor({
975
+ "engine": "claude",
976
+ "worker_id": scope["worker_id"],
977
+ "root": scope["root"],
978
+ "generation": scope["generation"],
979
+ "session_id": scope["session_id"],
980
+ "dev": source["dev"],
981
+ "ino": source["ino"],
982
+ "source_eof": source["eof"],
983
+ "anchor_offset": anchor["offset"],
984
+ "anchor_id": _cursor_id(anchor.get("source_record_id")),
985
+ "anchor_fingerprint": anchor.get("fingerprint"),
986
+ "cut_offset": first_offset,
987
+ "context_end_offset": context_end_offset,
988
+ "cut_id": _cursor_id(first.get("source_record_id")),
989
+ "cut_fingerprint": first.get("_ccx_fingerprint"),
990
+ })
991
+
992
+
993
+ def _read_claude_page_once(
994
+ path: str,
995
+ worker: str,
996
+ source: dict[str, int],
997
+ scope: dict[str, str],
998
+ *,
999
+ cursor: str | None,
1000
+ source_record_target: int,
1001
+ max_tool_result_bytes: int | None,
1002
+ timeout: float,
1003
+ ) -> dict[str, Any]:
1004
+ short = scope["worker_id"][:8]
1005
+ cut_offset = source["eof"]
1006
+ normalization_end_offset = source["eof"]
1007
+ anchor_hint = None
1008
+ if cursor is not None:
1009
+ payload = _decode_cursor(
1010
+ cursor, engine="claude", worker_id=scope["worker_id"]
1011
+ )
1012
+ cut_offset, normalization_end_offset = _validate_claude_cursor(
1013
+ payload, short, path, source, scope
1014
+ )
1015
+ anchor_hint = {
1016
+ "offset": payload.get("anchor_offset"),
1017
+ "source_record_id": payload.get("anchor_id"),
1018
+ "fingerprint": payload.get("anchor_fingerprint"),
1019
+ }
1020
+
1021
+ anchor, eligible, counters = _choose_claude_window(
1022
+ path,
1023
+ source,
1024
+ cut_offset=cut_offset,
1025
+ normalization_end_offset=normalization_end_offset,
1026
+ target=source_record_target,
1027
+ max_tool_result_bytes=max_tool_result_bytes,
1028
+ anchor_hint=anchor_hint,
1029
+ )
1030
+ selected = eligible[-source_record_target:]
1031
+ public_pairs: list[tuple[dict[str, Any], dict[str, Any]]] = []
1032
+ for record in selected:
1033
+ public = _public_record(record, counters)
1034
+ if public is not None:
1035
+ public_pairs.append((record, public))
1036
+
1037
+ keep = len(public_pairs)
1038
+ response_budget_limited = False
1039
+ while True:
1040
+ retained = public_pairs[-keep:] if keep else []
1041
+ public_records = [public for _record, public in retained]
1042
+ budget_dropped_public_records = keep < len(public_pairs)
1043
+ if not selected:
1044
+ first = None
1045
+ elif not public_pairs or not budget_dropped_public_records:
1046
+ # Empty/non-public source records still advance the native cursor.
1047
+ # Keep the first selected source cut unless the byte budget actually
1048
+ # removed public records from the front of this page.
1049
+ first = selected[0]
1050
+ else:
1051
+ first = retained[0][0] if retained else selected[0]
1052
+ older_cursor = _claude_older_cursor(
1053
+ scope,
1054
+ short,
1055
+ path,
1056
+ source,
1057
+ anchor,
1058
+ eligible,
1059
+ first,
1060
+ )
1061
+ result = _page_result(
1062
+ worker,
1063
+ public_records,
1064
+ older_cursor,
1065
+ counters,
1066
+ requested_source_record_target=source_record_target,
1067
+ response_budget_limited=response_budget_limited,
1068
+ )
1069
+ if _encoded_size(result) <= PAGE_RESULT_MAX_BYTES:
1070
+ return result
1071
+ response_budget_limited = True
1072
+ if keep <= 1:
1073
+ if public_records:
1074
+ public_records = [_last_resort_record_stub(public_records[0], counters)]
1075
+ result = _page_result(
1076
+ worker,
1077
+ public_records,
1078
+ older_cursor,
1079
+ counters,
1080
+ requested_source_record_target=source_record_target,
1081
+ response_budget_limited=True,
1082
+ )
1083
+ return result
1084
+ keep = max(1, keep // 2)
1085
+
1086
+
1087
+ def read_claude_page(
1088
+ pinned_path: str | PinnedNativeSource,
1089
+ worker: str,
1090
+ *,
1091
+ root: str | os.PathLike[str],
1092
+ generation: str,
1093
+ session_id: str,
1094
+ source_stat: os.stat_result,
1095
+ end_offset: int,
1096
+ cursor: str | None,
1097
+ source_record_target: int = _DEFAULT_TARGET,
1098
+ max_tool_result_bytes: int | None = 16_384,
1099
+ timeout: float = 30,
1100
+ ) -> dict[str, Any]:
1101
+ """One certified original-source Claude page, using the opener's held fd."""
1102
+ _validate_options(source_record_target, max_tool_result_bytes, timeout)
1103
+ scope = {
1104
+ "root": hashlib.sha256(os.fsencode(root)).hexdigest(),
1105
+ "generation": generation,
1106
+ "worker_id": worker.partition(":")[2],
1107
+ "session_id": session_id,
1108
+ }
1109
+ source = {
1110
+ "dev": int(source_stat.st_dev), "ino": int(source_stat.st_ino),
1111
+ "size": int(source_stat.st_size), "eof": end_offset,
1112
+ "mtime_ns": int(source_stat.st_mtime_ns),
1113
+ }
1114
+ page = _read_claude_page_once(
1115
+ pinned_path, worker, source, scope, cursor=cursor,
1116
+ source_record_target=source_record_target,
1117
+ max_tool_result_bytes=max_tool_result_bytes, timeout=timeout,
1118
+ )
1119
+ # The native opener still holds this same inode. Never reopen or restat the
1120
+ # original pathname, inspect another source, or retry against a new file.
1121
+ fd = (pinned_path.fd if isinstance(pinned_path, PinnedNativeSource)
1122
+ else int(pinned_path.rsplit("/", 1)[-1]))
1123
+ after_stat = os.fstat(fd)
1124
+ after = {
1125
+ "dev": int(after_stat.st_dev), "ino": int(after_stat.st_ino),
1126
+ "size": int(after_stat.st_size),
1127
+ "eof": _complete_line_end(fd, after_stat.st_size),
1128
+ "mtime_ns": int(after_stat.st_mtime_ns),
1129
+ }
1130
+ if not _source_transition_is_append_only(source, after):
1131
+ raise _unstable_source_error(scope["worker_id"][:8], cursor=cursor is not None)
1132
+ return page
1133
+
1134
+
1135
+ def _codex_timestamp(evidence: dict[str, Any] | None) -> int | None:
1136
+ if not isinstance(evidence, dict):
1137
+ return None
1138
+ completed = evidence.get("completedAtMs")
1139
+ if isinstance(completed, int) and not isinstance(completed, bool):
1140
+ return completed
1141
+ started = evidence.get("startedAtMs")
1142
+ if isinstance(started, int) and not isinstance(started, bool):
1143
+ return started
1144
+ return None
1145
+
1146
+
1147
+ def _codex_duration(evidence: dict[str, Any] | None) -> int | None:
1148
+ if not isinstance(evidence, dict):
1149
+ return None
1150
+ started = evidence.get("startedAtMs")
1151
+ completed = evidence.get("completedAtMs")
1152
+ if (
1153
+ isinstance(started, int)
1154
+ and not isinstance(started, bool)
1155
+ and isinstance(completed, int)
1156
+ and not isinstance(completed, bool)
1157
+ and completed >= started
1158
+ ):
1159
+ return completed - started
1160
+ return None
1161
+
1162
+
1163
+ def read_codex_page(
1164
+ rec: dict[str, Any],
1165
+ worker: str,
1166
+ *,
1167
+ root: str | os.PathLike[str],
1168
+ generation: str,
1169
+ worker_id: str,
1170
+ thread_id: str,
1171
+ expected_source: str,
1172
+ evidence_index: dict[str, Any],
1173
+ foreign_evidence: bool,
1174
+ cursor: str | None,
1175
+ source_record_target: int = _DEFAULT_TARGET,
1176
+ max_tool_result_bytes: int | None = 16_384,
1177
+ timeout: float = 30,
1178
+ ) -> dict[str, Any]:
1179
+ _validate_options(source_record_target, max_tool_result_bytes, timeout)
1180
+ short = worker_id[:8] # Diagnostic label only; never owner/source authority.
1181
+ scope = {
1182
+ "root": hashlib.sha256(os.fsencode(root)).hexdigest(),
1183
+ "generation": generation,
1184
+ "worker_id": worker_id,
1185
+ "thread_id": thread_id,
1186
+ }
1187
+ provider_cursor = None
1188
+ if cursor is not None:
1189
+ payload = _decode_cursor(cursor, engine="codex", worker_id=worker_id)
1190
+ if (payload.get("root") != scope["root"]
1191
+ or payload.get("generation") != generation
1192
+ or payload.get("thread_id") != thread_id):
1193
+ raise CursorInvalidated(
1194
+ "Codex conversation changed; restart paging", short=short,
1195
+ )
1196
+ provider_cursor = payload.get("provider_cursor")
1197
+ if not isinstance(provider_cursor, str) or not provider_cursor:
1198
+ raise CursorInvalidated(
1199
+ "Codex cursor expired; restart paging", short=short,
1200
+ )
1201
+
1202
+ # Only a source-attested explicit empty-history record is a zero-RPC EOF;
1203
+ # an older cursor can never originate from that thread. Decode it first so
1204
+ # wrong-owner and legacy tokens keep their normal typed refusals.
1205
+ if rec.get("historyKnownEmpty") is True:
1206
+ if cursor is not None:
1207
+ raise CursorInvalidated(
1208
+ "Codex conversation has no earlier messages", short=short,
1209
+ )
1210
+ counters = conv._Counters()
1211
+ if foreign_evidence:
1212
+ counters.warn("excluded native evidence from a foreign Codex thread")
1213
+ return _page_result(
1214
+ worker, [], None, counters,
1215
+ requested_source_record_target=source_record_target,
1216
+ response_budget_limited=False,
1217
+ )
1218
+
1219
+ evidence_items = evidence_index.get("items") or {}
1220
+ evidence_clients = evidence_index.get("clients") or {}
1221
+ observed_model = rec.get("resolvedModel")
1222
+ fetch_target = source_record_target
1223
+ response_budget_limited = False
1224
+ deadline = time.monotonic() + float(timeout)
1225
+
1226
+ while True:
1227
+ remaining = max(0.0, deadline - time.monotonic())
1228
+ if remaining <= 0:
1229
+ raise OperationTimeout(
1230
+ "Codex conversation page deadline expired", short=short
1231
+ )
1232
+ page = codex_runtime.list_thread_items(
1233
+ thread_id,
1234
+ worker_id=worker_id,
1235
+ expected_source=expected_source,
1236
+ cwd=rec.get("cwd"),
1237
+ cursor=provider_cursor,
1238
+ limit=fetch_target,
1239
+ sort_direction="desc",
1240
+ timeout=remaining,
1241
+ )
1242
+ native_items = page.get("data") if isinstance(page, dict) else None
1243
+ if not isinstance(native_items, list):
1244
+ raise SessionUnavailable(
1245
+ "Codex thread item page returned malformed data", short=short
1246
+ )
1247
+
1248
+ counters = conv._Counters()
1249
+ if foreign_evidence:
1250
+ counters.warn("excluded native evidence from a foreign Codex thread")
1251
+ records: list[dict[str, Any]] = []
1252
+ for wrapper in native_items:
1253
+ if not isinstance(wrapper, dict):
1254
+ counters.omitted += 1
1255
+ continue
1256
+ turn_id = wrapper.get("turnId")
1257
+ entry = wrapper.get("item")
1258
+ if not isinstance(entry, dict):
1259
+ counters.omitted += 1
1260
+ continue
1261
+ children = conv._codex_item(
1262
+ entry, counters, max_tool_result_bytes, observed_model
1263
+ )
1264
+ turn_evidence = evidence_items.get(turn_id) or {}
1265
+ turn_clients = evidence_clients.get(turn_id) or {}
1266
+ item_evidence = conv._evidence_entry(
1267
+ entry, turn_evidence, turn_clients
1268
+ )
1269
+ conv._apply_timing(children, item_evidence)
1270
+ public = _public_record({
1271
+ "turn_id": turn_id,
1272
+ "source_record_id": entry.get("id"),
1273
+ "timestamp_ms": _codex_timestamp(item_evidence),
1274
+ "duration_ms": _codex_duration(item_evidence),
1275
+ "usage": None,
1276
+ "items": children,
1277
+ }, counters)
1278
+ if public is not None:
1279
+ records.append(public)
1280
+
1281
+ # Native desc is transport-efficient; public page order is chronological.
1282
+ records.reverse()
1283
+ next_cursor = page.get("nextCursor")
1284
+ if next_cursor is not None and (
1285
+ not isinstance(next_cursor, str) or not next_cursor
1286
+ ):
1287
+ raise ProviderProtocolError(
1288
+ "Codex history cursor is invalid; restart paging", short=short,
1289
+ )
1290
+ older_cursor = (
1291
+ _encode_cursor({
1292
+ **scope, "engine": "codex", "provider_cursor": next_cursor,
1293
+ })
1294
+ if next_cursor is not None else None
1295
+ )
1296
+ result = _page_result(
1297
+ worker,
1298
+ records,
1299
+ older_cursor,
1300
+ counters,
1301
+ requested_source_record_target=source_record_target,
1302
+ response_budget_limited=response_budget_limited,
1303
+ )
1304
+ if _encoded_size(result) <= PAGE_RESULT_MAX_BYTES:
1305
+ return result
1306
+ response_budget_limited = True
1307
+ if fetch_target <= 1:
1308
+ if len(native_items) > fetch_target:
1309
+ # A provider that ignores limit=1 gives us no cursor boundary
1310
+ # immediately after only the newest record. Returning its
1311
+ # nextCursor after dropping siblings would skip history. Refuse
1312
+ # this provider-contract violation rather than invent a cursor.
1313
+ raise ProviderProtocolError(
1314
+ "Codex thread item page exceeded requested limit at final "
1315
+ "conversation read limit",
1316
+ short=short,
1317
+ )
1318
+ if records:
1319
+ records = [_last_resort_record_stub(records[-1], counters)]
1320
+ return _page_result(
1321
+ worker,
1322
+ records,
1323
+ older_cursor,
1324
+ counters,
1325
+ requested_source_record_target=source_record_target,
1326
+ response_budget_limited=True,
1327
+ )
1328
+
1329
+ # Refetch from the same incoming provider cursor with a smaller native
1330
+ # limit. Base the next request on what the provider actually returned,
1331
+ # not merely what we asked for, and spend one shared caller deadline.
1332
+ previous_target = fetch_target
1333
+ fetch_target = max(1, min(fetch_target, len(native_items)) // 2)
1334
+ if fetch_target >= previous_target:
1335
+ fetch_target = max(1, previous_target // 2)
1336
+
1337
+
1338
+ __all__ = ["read_claude_page", "read_codex_page"]