ccx-native 0.0.1 → 0.2.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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -4
  3. package/bin/ccx +55 -2
  4. package/ccx/__init__.py +167 -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 +807 -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 +269 -0
  16. package/ccx/claude/host/session.py +5244 -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 +2535 -0
  26. package/ccx/claude/usage.py +796 -0
  27. package/ccx/claude/usage_control.py +204 -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/models.py +53 -0
  35. package/ccx/cli/commands/read.py +1104 -0
  36. package/ccx/cli/commands/turns.py +1081 -0
  37. package/ccx/cli/controller.py +757 -0
  38. package/ccx/cli/main.py +302 -0
  39. package/ccx/cli/parser.py +1439 -0
  40. package/ccx/cli/skills.py +215 -0
  41. package/ccx/cli/statusline.py +160 -0
  42. package/ccx/client.py +1904 -0
  43. package/ccx/codex/__init__.py +0 -0
  44. package/ccx/codex/attach_tty.py +267 -0
  45. package/ccx/codex/client.py +738 -0
  46. package/ccx/codex/completion.py +121 -0
  47. package/ccx/codex/control.py +288 -0
  48. package/ccx/codex/daemon/__init__.py +0 -0
  49. package/ccx/codex/daemon/__main__.py +4 -0
  50. package/ccx/codex/daemon/daemon.py +1558 -0
  51. package/ccx/codex/daemon/models.py +140 -0
  52. package/ccx/codex/daemon/native.py +1590 -0
  53. package/ccx/codex/daemon/release.py +367 -0
  54. package/ccx/codex/daemon/worker.py +2424 -0
  55. package/ccx/codex/delivery.py +758 -0
  56. package/ccx/codex/delivery_recovery.py +232 -0
  57. package/ccx/codex/evidence.py +827 -0
  58. package/ccx/codex/history.py +158 -0
  59. package/ccx/codex/history_store.py +131 -0
  60. package/ccx/codex/notify_history.py +140 -0
  61. package/ccx/codex/ops.py +666 -0
  62. package/ccx/codex/paths.py +70 -0
  63. package/ccx/codex/protocol.py +805 -0
  64. package/ccx/codex/provenance.py +105 -0
  65. package/ccx/codex/read.py +29 -0
  66. package/ccx/codex/records.py +51 -0
  67. package/ccx/codex/reports.py +177 -0
  68. package/ccx/codex/runtime.py +1454 -0
  69. package/ccx/codex/settle.py +363 -0
  70. package/ccx/codex/telemetry.py +792 -0
  71. package/ccx/codex/types.py +659 -0
  72. package/ccx/codex/worker_environment.py +25 -0
  73. package/ccx/constants.py +288 -0
  74. package/ccx/controllers/__init__.py +0 -0
  75. package/ccx/controllers/controller.py +1449 -0
  76. package/ccx/controllers/live_vars.py +1706 -0
  77. package/ccx/controllers/live_vars_runner.py +216 -0
  78. package/ccx/controllers/notify.py +213 -0
  79. package/ccx/controllers/orchestrator.py +193 -0
  80. package/ccx/controllers/policy.py +57 -0
  81. package/ccx/controllers/store.py +681 -0
  82. package/ccx/errors.py +634 -0
  83. package/ccx/models.py +225 -0
  84. package/ccx/notify/__init__.py +0 -0
  85. package/ccx/notify/__main__.py +5 -0
  86. package/ccx/notify/client.py +116 -0
  87. package/ccx/notify/daemon.py +480 -0
  88. package/ccx/notify/deliver.py +304 -0
  89. package/ccx/notify/paths.py +51 -0
  90. package/ccx/notify/registry.py +649 -0
  91. package/ccx/notify/select.py +750 -0
  92. package/ccx/notify/settle.py +684 -0
  93. package/ccx/ops/__init__.py +0 -0
  94. package/ccx/ops/background.py +115 -0
  95. package/ccx/ops/cancel.py +296 -0
  96. package/ccx/ops/codex_import.py +96 -0
  97. package/ccx/ops/common.py +397 -0
  98. package/ccx/ops/compact.py +337 -0
  99. package/ccx/ops/compact_ops.py +550 -0
  100. package/ccx/ops/context_events.py +48 -0
  101. package/ccx/ops/conversation.py +1593 -0
  102. package/ccx/ops/conversation_paging.py +1339 -0
  103. package/ccx/ops/create.py +56 -0
  104. package/ccx/ops/creation.py +952 -0
  105. package/ccx/ops/creation_steps.py +575 -0
  106. package/ccx/ops/event_feed.py +710 -0
  107. package/ccx/ops/forking.py +711 -0
  108. package/ccx/ops/history.py +1041 -0
  109. package/ccx/ops/input.py +556 -0
  110. package/ccx/ops/maintenance.py +295 -0
  111. package/ccx/ops/report.py +773 -0
  112. package/ccx/ops/semantics.py +490 -0
  113. package/ccx/ops/send.py +961 -0
  114. package/ccx/ops/steer_ops.py +867 -0
  115. package/ccx/ops/terminate.py +383 -0
  116. package/ccx/ops/turn_reports.py +348 -0
  117. package/ccx/ops/wait.py +762 -0
  118. package/ccx/ops/worker_events.py +640 -0
  119. package/ccx/rpc/__init__.py +0 -0
  120. package/ccx/rpc/local_rpc.py +1602 -0
  121. package/ccx/rpc/rpc.py +559 -0
  122. package/ccx/rpc/serve.py +374 -0
  123. package/ccx/rpc/transport.py +409 -0
  124. package/ccx/rpc/view.py +125 -0
  125. package/ccx/store/__init__.py +0 -0
  126. package/ccx/store/discovery.py +170 -0
  127. package/ccx/store/identity.py +105 -0
  128. package/ccx/store/journal_io.py +79 -0
  129. package/ccx/store/liveness.py +194 -0
  130. package/ccx/store/paths.py +250 -0
  131. package/ccx/store/record_cache.py +40 -0
  132. package/ccx/store/records.py +309 -0
  133. package/ccx/store/root.py +789 -0
  134. package/ccx/store/runtime_presence.py +137 -0
  135. package/ccx/store/store.py +2332 -0
  136. package/ccx/store/worker_state.py +86 -0
  137. package/ccx/system/__init__.py +0 -0
  138. package/ccx/system/bounded.py +296 -0
  139. package/ccx/system/cwd_paths.py +25 -0
  140. package/ccx/system/engine_binary.py +59 -0
  141. package/ccx/system/executable.py +50 -0
  142. package/ccx/system/oslayer.py +667 -0
  143. package/ccx/system/pipes.py +46 -0
  144. package/ccx/system/private_json.py +54 -0
  145. package/ccx/system/sanitize.py +43 -0
  146. package/ccx/types.py +809 -0
  147. package/ccx/version.py +29 -0
  148. package/ccx/workers.py +2093 -0
  149. package/ccx.py +9 -0
  150. package/package.json +39 -7
  151. package/pyproject.toml +87 -0
  152. package/skills/ccx/SKILL.md +166 -0
  153. package/skills/ccx/references/controllers.md +244 -0
  154. package/skills/ccx/references/history.md +50 -0
  155. package/skills/ccx/references/troubleshooting.md +94 -0
  156. package/skills/ccx/references/variables.md +62 -0
@@ -0,0 +1,1339 @@
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. A row longer than a read block is collected piece by piece
656
+ and joined once, so a multi-MB row costs its size, not its size squared.
657
+ """
658
+ try:
659
+ reader = (path.open_reader() if isinstance(path, PinnedNativeSource)
660
+ else open(path, "rb"))
661
+ with reader as handle:
662
+ position = before_offset
663
+ pieces: list[bytes] = [] # the current row's tail, newest piece first
664
+ while position > 0:
665
+ size = min(block_size, position)
666
+ position -= size
667
+ handle.seek(position)
668
+ data = handle.read(size)
669
+ cut = len(data)
670
+ while (newline := data.rfind(b"\n", 0, cut)) >= 0:
671
+ pieces.append(data[newline + 1:cut])
672
+ raw = b"".join(reversed(pieces))
673
+ pieces = []
674
+ if raw:
675
+ yield position + newline + 1, raw
676
+ cut = newline
677
+ pieces.append(data[:cut])
678
+ raw = b"".join(reversed(pieces))
679
+ if raw:
680
+ yield 0, raw
681
+ except FileNotFoundError:
682
+ return
683
+
684
+
685
+ def _reverse_boundary_events(path: str | PinnedNativeSource, *, before_offset: int):
686
+ """Find ordinary-user starts newest-first, without hashing skipped rows."""
687
+ for start, raw in _reverse_lines(path, before_offset=before_offset):
688
+ raw = raw.strip()
689
+ if not raw:
690
+ continue
691
+ try:
692
+ evt = json.loads(raw)
693
+ except (ValueError, UnicodeDecodeError):
694
+ continue
695
+ if isinstance(evt, dict):
696
+ if evt.get("isSidechain") is True:
697
+ continue
698
+ evt["_ccx_start_offset"] = start
699
+ if _ordinary_user_boundary(evt):
700
+ yield evt
701
+
702
+
703
+ def claude_tail_offset(path: str | PinnedNativeSource, *, before_offset: int,
704
+ turns: int) -> int:
705
+ """Where the newest ``turns`` user turns start, reading backwards; 0 if fewer."""
706
+ seen = 0
707
+ for evt in _reverse_boundary_events(path, before_offset=before_offset):
708
+ seen += 1
709
+ if seen >= turns:
710
+ return evt["_ccx_start_offset"]
711
+ return 0
712
+
713
+
714
+ def _normalize_claude_window(
715
+ path: str,
716
+ *,
717
+ after_offset: int,
718
+ before_offset: int,
719
+ max_tool_result_bytes: int | None,
720
+ ) -> tuple[list[dict[str, Any]], conv._Counters]:
721
+ raw_stats: dict[str, int] = {"malformedLines": 0}
722
+ events = list(
723
+ _iter_transcript(
724
+ path,
725
+ raw_stats,
726
+ after_offset=after_offset,
727
+ before_offset=before_offset,
728
+ )
729
+ )
730
+ counters = conv._Counters()
731
+ counters.malformed = raw_stats.get("malformedLines", 0)
732
+ if counters.malformed:
733
+ counters.warn(
734
+ f"{counters.malformed} malformed native JSONL line(s) skipped"
735
+ )
736
+ _turns, _model, records = conv._normalize_claude_events(
737
+ events, counters, max_tool_result_bytes
738
+ )
739
+ return records, counters
740
+
741
+
742
+ def _bearing_before(
743
+ records: list[dict[str, Any]],
744
+ cut_offset: int,
745
+ ) -> list[dict[str, Any]]:
746
+ return [
747
+ record for record in records
748
+ if record.get("_ccx_page_bearing")
749
+ and isinstance(record.get("_ccx_start_offset"), int)
750
+ and record["_ccx_start_offset"] < cut_offset
751
+ ]
752
+
753
+
754
+ def _choose_claude_window(
755
+ path: str,
756
+ source: dict[str, int],
757
+ *,
758
+ cut_offset: int,
759
+ normalization_end_offset: int,
760
+ target: int,
761
+ max_tool_result_bytes: int | None,
762
+ anchor_hint: dict[str, Any] | None = None,
763
+ ) -> tuple[dict[str, Any], list[dict[str, Any]], conv._Counters]:
764
+ def normalize_from(boundary: dict[str, Any]):
765
+ records, counters = _normalize_claude_window(
766
+ path,
767
+ after_offset=boundary["offset"],
768
+ before_offset=normalization_end_offset,
769
+ max_tool_result_bytes=max_tool_result_bytes,
770
+ )
771
+ return _bearing_before(records, cut_offset), counters
772
+
773
+ # Older-page cursors carry a validated normalization anchor.
774
+ if (
775
+ isinstance(anchor_hint, dict)
776
+ and isinstance(anchor_hint.get("offset"), int)
777
+ and 0 <= anchor_hint["offset"] < cut_offset
778
+ ):
779
+ eligible, counters = normalize_from(anchor_hint)
780
+ if len(eligible) >= target or anchor_hint["offset"] == 0:
781
+ return anchor_hint, eligible, counters
782
+
783
+ # Probe the same 4, 8, 16... user starts as the old forward full scan, but
784
+ # stop reading the pinned source as soon as a window has enough records.
785
+ # If fewer than the next probe distance exist, also try the oldest start.
786
+ distance = 4
787
+ count = 0
788
+ last_probed = 0
789
+ oldest = None
790
+ for evt in _reverse_boundary_events(path, before_offset=cut_offset):
791
+ count += 1
792
+ oldest = evt
793
+ if count != distance:
794
+ continue
795
+ eligible, counters = normalize_from({"offset": evt["_ccx_start_offset"]})
796
+ if len(eligible) >= target:
797
+ return _boundary_from_event(evt), eligible, counters
798
+ last_probed = count
799
+ distance *= 2
800
+ if oldest is not None and count != last_probed:
801
+ eligible, counters = normalize_from({"offset": oldest["_ccx_start_offset"]})
802
+ if len(eligible) >= target:
803
+ return _boundary_from_event(oldest), eligible, counters
804
+
805
+ zero = {"offset": 0, "source_record_id": None, "fingerprint": None}
806
+ eligible, counters = normalize_from(zero)
807
+ return zero, eligible, counters
808
+
809
+
810
+ def _source_transition_is_append_only(
811
+ before: dict[str, int],
812
+ after: dict[str, int],
813
+ ) -> bool:
814
+ """Whether the after snapshot is compatible with an as-of-before EOF read.
815
+
816
+ Claude's native transcript is append-only in ordinary operation. Growth
817
+ beyond the snapshotted complete-line EOF is harmless because every bounded
818
+ parser is fenced at that earlier EOF. Replacement, truncation, or a same-size
819
+ rewrite invalidates the read.
820
+ """
821
+ if before["dev"] != after["dev"] or before["ino"] != after["ino"]:
822
+ return False
823
+ if after["eof"] < before["eof"] or after["size"] < before["eof"]:
824
+ return False
825
+ if after["eof"] > before["eof"]:
826
+ return True
827
+ if after["size"] > before["size"]:
828
+ # A partial trailing append need not advance the complete-line EOF yet.
829
+ return True
830
+ if after["size"] != before["size"]:
831
+ return False
832
+ return after["mtime_ns"] == before["mtime_ns"]
833
+
834
+
835
+ def _unstable_source_error(short: str, *, cursor: bool) -> Exception:
836
+ if cursor:
837
+ return CursorInvalidated(
838
+ "conversation page source changed during the read; restart paging",
839
+ short=short,
840
+ )
841
+ return SessionUnavailable(
842
+ "Claude transcript changed non-append-only while reading; retry the page",
843
+ short=short,
844
+ )
845
+
846
+
847
+ def _validate_claude_cursor(
848
+ payload: dict[str, Any],
849
+ short: str,
850
+ path: str,
851
+ source: dict[str, int],
852
+ scope: dict[str, str],
853
+ ) -> tuple[int, int]:
854
+ if any(payload.get(key) != scope[key] for key in (
855
+ "root", "generation", "session_id"
856
+ )):
857
+ raise CursorInvalidated(
858
+ "Conversation worker changed; restart paging", short=short
859
+ )
860
+ if payload.get("dev") != source["dev"] or payload.get("ino") != source["ino"]:
861
+ raise CursorInvalidated(
862
+ "conversation page source was replaced; restart paging", short=short
863
+ )
864
+ cut_offset = payload.get("cut_offset")
865
+ if (
866
+ not isinstance(cut_offset, int)
867
+ or isinstance(cut_offset, bool)
868
+ or cut_offset < 0
869
+ or cut_offset >= source["eof"]
870
+ ):
871
+ raise CursorInvalidated(
872
+ "conversation page source was truncated; restart paging", short=short
873
+ )
874
+ if not _record_matches(
875
+ path,
876
+ cut_offset,
877
+ payload.get("cut_id"),
878
+ payload.get("cut_fingerprint"),
879
+ before_offset=source["eof"],
880
+ ):
881
+ raise CursorInvalidated(
882
+ "Conversation history changed; restart paging",
883
+ short=short,
884
+ )
885
+ # The fingerprint must match even when the original row has a native UUID.
886
+ # Both proofs are carried so an edited cursor cannot silently loosen either.
887
+ cut = _event_at(path, cut_offset, before_offset=source["eof"])
888
+ if cut is None or payload.get("cut_fingerprint") != _event_fingerprint(cut):
889
+ raise CursorInvalidated("conversation page cut fingerprint changed", short=short)
890
+ # Check the original snapshot fence, not a newly appended tail. A cursor
891
+ # may remain valid after append but must not shorten semantic lookahead.
892
+ source_eof = payload.get("source_eof")
893
+ if (type(source_eof) is not int or not cut_offset < source_eof <= source["eof"]):
894
+ raise CursorInvalidated("Conversation source changed; restart paging", short=short)
895
+ context_end_offset = payload.get("context_end_offset")
896
+ if (
897
+ not isinstance(context_end_offset, int)
898
+ or isinstance(context_end_offset, bool)
899
+ or context_end_offset < cut_offset
900
+ or context_end_offset > source_eof
901
+ or context_end_offset != _context_end_offset(
902
+ path, cut_offset, source_eof=source_eof
903
+ )
904
+ ):
905
+ raise CursorInvalidated(
906
+ "Conversation context changed; restart paging",
907
+ short=short,
908
+ )
909
+
910
+ anchor_offset = payload.get("anchor_offset")
911
+ if (type(anchor_offset) is not int or anchor_offset < 0
912
+ or anchor_offset >= cut_offset):
913
+ raise CursorInvalidated("conversation page anchor is no longer valid", short=short)
914
+ # Byte zero may be a synthetic fallback or a real ordinary-user anchor.
915
+ anchor = _validate_anchor(path, anchor_offset, before_offset=source_eof)
916
+ if anchor_offset == 0 and (anchor is None or anchor["offset"] != 0):
917
+ if payload.get("anchor_id") is not None or payload.get("anchor_fingerprint") is not None:
918
+ raise CursorInvalidated("conversation page zero anchor changed", short=short)
919
+ else:
920
+ if (
921
+ anchor is None
922
+ or anchor["offset"] != anchor_offset
923
+ or _cursor_id(anchor.get("source_record_id")) != payload.get("anchor_id")
924
+ or anchor.get("fingerprint") != payload.get("anchor_fingerprint")
925
+ ):
926
+ raise CursorInvalidated(
927
+ "conversation page normalization anchor is no longer valid; restart paging",
928
+ short=short,
929
+ )
930
+ return cut_offset, context_end_offset
931
+
932
+
933
+ def _cursor_id(value: Any) -> str | None:
934
+ if not isinstance(value, str) or not value:
935
+ return None
936
+ if len(value.encode("utf-8")) > _PROTECTED_STRING_MAX_BYTES:
937
+ return None
938
+ return value
939
+
940
+
941
+ def _claude_older_cursor(
942
+ scope: dict[str, str],
943
+ short: str,
944
+ path: str,
945
+ source: dict[str, int],
946
+ anchor: dict[str, Any],
947
+ eligible: list[dict[str, Any]],
948
+ first: dict[str, Any] | None,
949
+ ) -> str | None:
950
+ if first is None:
951
+ return None
952
+ first_offset = first["_ccx_start_offset"]
953
+ has_older_here = any(
954
+ isinstance(record.get("_ccx_start_offset"), int)
955
+ and record["_ccx_start_offset"] < first_offset
956
+ for record in eligible
957
+ )
958
+ has_older_before_anchor = anchor["offset"] > 0
959
+ if first_offset <= 0 or not (
960
+ has_older_here or has_older_before_anchor
961
+ ):
962
+ return None
963
+ if first_offset == anchor["offset"]:
964
+ # The first retained record can be the normalization anchor itself.
965
+ # An older cursor must point to a strictly earlier anchor, or its next
966
+ # read fails validation even when the original transcript is unchanged.
967
+ # Rebase on the preceding ordinary user, using the same pinned source;
968
+ # if none exists, the byte-zero fallback covers the earlier prelude.
969
+ previous = next(_reverse_boundary_events(path, before_offset=first_offset), None)
970
+ anchor = (_boundary_from_event(previous) if previous is not None else
971
+ {"offset": 0, "source_record_id": None, "fingerprint": None})
972
+ context_end_offset = _context_end_offset(
973
+ path, first_offset, source_eof=source["eof"]
974
+ )
975
+ return _encode_cursor({
976
+ "engine": "claude",
977
+ "worker_id": scope["worker_id"],
978
+ "root": scope["root"],
979
+ "generation": scope["generation"],
980
+ "session_id": scope["session_id"],
981
+ "dev": source["dev"],
982
+ "ino": source["ino"],
983
+ "source_eof": source["eof"],
984
+ "anchor_offset": anchor["offset"],
985
+ "anchor_id": _cursor_id(anchor.get("source_record_id")),
986
+ "anchor_fingerprint": anchor.get("fingerprint"),
987
+ "cut_offset": first_offset,
988
+ "context_end_offset": context_end_offset,
989
+ "cut_id": _cursor_id(first.get("source_record_id")),
990
+ "cut_fingerprint": first.get("_ccx_fingerprint"),
991
+ })
992
+
993
+
994
+ def _read_claude_page_once(
995
+ path: str,
996
+ worker: str,
997
+ source: dict[str, int],
998
+ scope: dict[str, str],
999
+ *,
1000
+ cursor: str | None,
1001
+ source_record_target: int,
1002
+ max_tool_result_bytes: int | None,
1003
+ timeout: float,
1004
+ ) -> dict[str, Any]:
1005
+ short = scope["worker_id"][:8]
1006
+ cut_offset = source["eof"]
1007
+ normalization_end_offset = source["eof"]
1008
+ anchor_hint = None
1009
+ if cursor is not None:
1010
+ payload = _decode_cursor(
1011
+ cursor, engine="claude", worker_id=scope["worker_id"]
1012
+ )
1013
+ cut_offset, normalization_end_offset = _validate_claude_cursor(
1014
+ payload, short, path, source, scope
1015
+ )
1016
+ anchor_hint = {
1017
+ "offset": payload.get("anchor_offset"),
1018
+ "source_record_id": payload.get("anchor_id"),
1019
+ "fingerprint": payload.get("anchor_fingerprint"),
1020
+ }
1021
+
1022
+ anchor, eligible, counters = _choose_claude_window(
1023
+ path,
1024
+ source,
1025
+ cut_offset=cut_offset,
1026
+ normalization_end_offset=normalization_end_offset,
1027
+ target=source_record_target,
1028
+ max_tool_result_bytes=max_tool_result_bytes,
1029
+ anchor_hint=anchor_hint,
1030
+ )
1031
+ selected = eligible[-source_record_target:]
1032
+ public_pairs: list[tuple[dict[str, Any], dict[str, Any]]] = []
1033
+ for record in selected:
1034
+ public = _public_record(record, counters)
1035
+ if public is not None:
1036
+ public_pairs.append((record, public))
1037
+
1038
+ keep = len(public_pairs)
1039
+ response_budget_limited = False
1040
+ while True:
1041
+ retained = public_pairs[-keep:] if keep else []
1042
+ public_records = [public for _record, public in retained]
1043
+ budget_dropped_public_records = keep < len(public_pairs)
1044
+ if not selected:
1045
+ first = None
1046
+ elif not public_pairs or not budget_dropped_public_records:
1047
+ # Empty/non-public source records still advance the native cursor.
1048
+ # Keep the first selected source cut unless the byte budget actually
1049
+ # removed public records from the front of this page.
1050
+ first = selected[0]
1051
+ else:
1052
+ first = retained[0][0] if retained else selected[0]
1053
+ older_cursor = _claude_older_cursor(
1054
+ scope,
1055
+ short,
1056
+ path,
1057
+ source,
1058
+ anchor,
1059
+ eligible,
1060
+ first,
1061
+ )
1062
+ result = _page_result(
1063
+ worker,
1064
+ public_records,
1065
+ older_cursor,
1066
+ counters,
1067
+ requested_source_record_target=source_record_target,
1068
+ response_budget_limited=response_budget_limited,
1069
+ )
1070
+ if _encoded_size(result) <= PAGE_RESULT_MAX_BYTES:
1071
+ return result
1072
+ response_budget_limited = True
1073
+ if keep <= 1:
1074
+ if public_records:
1075
+ public_records = [_last_resort_record_stub(public_records[0], counters)]
1076
+ result = _page_result(
1077
+ worker,
1078
+ public_records,
1079
+ older_cursor,
1080
+ counters,
1081
+ requested_source_record_target=source_record_target,
1082
+ response_budget_limited=True,
1083
+ )
1084
+ return result
1085
+ keep = max(1, keep // 2)
1086
+
1087
+
1088
+ def read_claude_page(
1089
+ pinned_path: str | PinnedNativeSource,
1090
+ worker: str,
1091
+ *,
1092
+ root: str | os.PathLike[str],
1093
+ generation: str,
1094
+ session_id: str,
1095
+ source_stat: os.stat_result,
1096
+ end_offset: int,
1097
+ cursor: str | None,
1098
+ source_record_target: int = _DEFAULT_TARGET,
1099
+ max_tool_result_bytes: int | None = 16_384,
1100
+ timeout: float = 30,
1101
+ ) -> dict[str, Any]:
1102
+ """One certified original-source Claude page, using the opener's held fd."""
1103
+ _validate_options(source_record_target, max_tool_result_bytes, timeout)
1104
+ scope = {
1105
+ "root": hashlib.sha256(os.fsencode(root)).hexdigest(),
1106
+ "generation": generation,
1107
+ "worker_id": worker.partition(":")[2],
1108
+ "session_id": session_id,
1109
+ }
1110
+ source = {
1111
+ "dev": int(source_stat.st_dev), "ino": int(source_stat.st_ino),
1112
+ "size": int(source_stat.st_size), "eof": end_offset,
1113
+ "mtime_ns": int(source_stat.st_mtime_ns),
1114
+ }
1115
+ page = _read_claude_page_once(
1116
+ pinned_path, worker, source, scope, cursor=cursor,
1117
+ source_record_target=source_record_target,
1118
+ max_tool_result_bytes=max_tool_result_bytes, timeout=timeout,
1119
+ )
1120
+ # The native opener still holds this same inode. Never reopen or restat the
1121
+ # original pathname, inspect another source, or retry against a new file.
1122
+ fd = (pinned_path.fd if isinstance(pinned_path, PinnedNativeSource)
1123
+ else int(pinned_path.rsplit("/", 1)[-1]))
1124
+ after_stat = os.fstat(fd)
1125
+ after = {
1126
+ "dev": int(after_stat.st_dev), "ino": int(after_stat.st_ino),
1127
+ "size": int(after_stat.st_size),
1128
+ "eof": _complete_line_end(fd, after_stat.st_size),
1129
+ "mtime_ns": int(after_stat.st_mtime_ns),
1130
+ }
1131
+ if not _source_transition_is_append_only(source, after):
1132
+ raise _unstable_source_error(scope["worker_id"][:8], cursor=cursor is not None)
1133
+ return page
1134
+
1135
+
1136
+ def _codex_timestamp(evidence: dict[str, Any] | None) -> int | None:
1137
+ if not isinstance(evidence, dict):
1138
+ return None
1139
+ completed = evidence.get("completedAtMs")
1140
+ if isinstance(completed, int) and not isinstance(completed, bool):
1141
+ return completed
1142
+ started = evidence.get("startedAtMs")
1143
+ if isinstance(started, int) and not isinstance(started, bool):
1144
+ return started
1145
+ return None
1146
+
1147
+
1148
+ def _codex_duration(evidence: dict[str, Any] | None) -> int | None:
1149
+ if not isinstance(evidence, dict):
1150
+ return None
1151
+ started = evidence.get("startedAtMs")
1152
+ completed = evidence.get("completedAtMs")
1153
+ if (
1154
+ isinstance(started, int)
1155
+ and not isinstance(started, bool)
1156
+ and isinstance(completed, int)
1157
+ and not isinstance(completed, bool)
1158
+ and completed >= started
1159
+ ):
1160
+ return completed - started
1161
+ return None
1162
+
1163
+
1164
+ def read_codex_page(
1165
+ rec: dict[str, Any],
1166
+ worker: str,
1167
+ *,
1168
+ root: str | os.PathLike[str],
1169
+ generation: str,
1170
+ worker_id: str,
1171
+ thread_id: str,
1172
+ expected_source: str,
1173
+ evidence_index: dict[str, Any],
1174
+ foreign_evidence: bool,
1175
+ cursor: str | None,
1176
+ source_record_target: int = _DEFAULT_TARGET,
1177
+ max_tool_result_bytes: int | None = 16_384,
1178
+ timeout: float = 30,
1179
+ ) -> dict[str, Any]:
1180
+ _validate_options(source_record_target, max_tool_result_bytes, timeout)
1181
+ short = worker_id[:8] # Diagnostic label only; never owner/source authority.
1182
+ scope = {
1183
+ "root": hashlib.sha256(os.fsencode(root)).hexdigest(),
1184
+ "generation": generation,
1185
+ "worker_id": worker_id,
1186
+ "thread_id": thread_id,
1187
+ }
1188
+ provider_cursor = None
1189
+ if cursor is not None:
1190
+ payload = _decode_cursor(cursor, engine="codex", worker_id=worker_id)
1191
+ if (payload.get("root") != scope["root"]
1192
+ or payload.get("generation") != generation
1193
+ or payload.get("thread_id") != thread_id):
1194
+ raise CursorInvalidated(
1195
+ "Codex conversation changed; restart paging", short=short,
1196
+ )
1197
+ provider_cursor = payload.get("provider_cursor")
1198
+ if not isinstance(provider_cursor, str) or not provider_cursor:
1199
+ raise CursorInvalidated(
1200
+ "Codex cursor expired; restart paging", short=short,
1201
+ )
1202
+
1203
+ # Only a source-attested explicit empty-history record is a zero-RPC EOF;
1204
+ # an older cursor can never originate from that thread. Decode it first so
1205
+ # wrong-owner and legacy tokens keep their normal typed refusals.
1206
+ if rec.get("historyKnownEmpty") is True:
1207
+ if cursor is not None:
1208
+ raise CursorInvalidated(
1209
+ "Codex conversation has no earlier messages", short=short,
1210
+ )
1211
+ counters = conv._Counters()
1212
+ if foreign_evidence:
1213
+ counters.warn("excluded native evidence from a foreign Codex thread")
1214
+ return _page_result(
1215
+ worker, [], None, counters,
1216
+ requested_source_record_target=source_record_target,
1217
+ response_budget_limited=False,
1218
+ )
1219
+
1220
+ evidence_items = evidence_index.get("items") or {}
1221
+ evidence_clients = evidence_index.get("clients") or {}
1222
+ observed_model = rec.get("resolvedModel")
1223
+ fetch_target = source_record_target
1224
+ response_budget_limited = False
1225
+ deadline = time.monotonic() + float(timeout)
1226
+
1227
+ while True:
1228
+ remaining = max(0.0, deadline - time.monotonic())
1229
+ if remaining <= 0:
1230
+ raise OperationTimeout(
1231
+ "Codex conversation page deadline expired", short=short
1232
+ )
1233
+ page = codex_runtime.list_thread_items(
1234
+ thread_id,
1235
+ worker_id=worker_id,
1236
+ expected_source=expected_source,
1237
+ cwd=rec.get("cwd"),
1238
+ cursor=provider_cursor,
1239
+ limit=fetch_target,
1240
+ sort_direction="desc",
1241
+ timeout=remaining,
1242
+ )
1243
+ native_items = page.get("data") if isinstance(page, dict) else None
1244
+ if not isinstance(native_items, list):
1245
+ raise SessionUnavailable(
1246
+ "Codex thread item page returned malformed data", short=short
1247
+ )
1248
+
1249
+ counters = conv._Counters()
1250
+ if foreign_evidence:
1251
+ counters.warn("excluded native evidence from a foreign Codex thread")
1252
+ records: list[dict[str, Any]] = []
1253
+ for wrapper in native_items:
1254
+ if not isinstance(wrapper, dict):
1255
+ counters.omitted += 1
1256
+ continue
1257
+ turn_id = wrapper.get("turnId")
1258
+ entry = wrapper.get("item")
1259
+ if not isinstance(entry, dict):
1260
+ counters.omitted += 1
1261
+ continue
1262
+ children = conv._codex_item(
1263
+ entry, counters, max_tool_result_bytes, observed_model
1264
+ )
1265
+ turn_evidence = evidence_items.get(turn_id) or {}
1266
+ turn_clients = evidence_clients.get(turn_id) or {}
1267
+ item_evidence = conv._evidence_entry(
1268
+ entry, turn_evidence, turn_clients
1269
+ )
1270
+ conv._apply_timing(children, item_evidence)
1271
+ public = _public_record({
1272
+ "turn_id": turn_id,
1273
+ "source_record_id": entry.get("id"),
1274
+ "timestamp_ms": _codex_timestamp(item_evidence),
1275
+ "duration_ms": _codex_duration(item_evidence),
1276
+ "usage": None,
1277
+ "items": children,
1278
+ }, counters)
1279
+ if public is not None:
1280
+ records.append(public)
1281
+
1282
+ # Native desc is transport-efficient; public page order is chronological.
1283
+ records.reverse()
1284
+ next_cursor = page.get("nextCursor")
1285
+ if next_cursor is not None and (
1286
+ not isinstance(next_cursor, str) or not next_cursor
1287
+ ):
1288
+ raise ProviderProtocolError(
1289
+ "Codex history cursor is invalid; restart paging", short=short,
1290
+ )
1291
+ older_cursor = (
1292
+ _encode_cursor({
1293
+ **scope, "engine": "codex", "provider_cursor": next_cursor,
1294
+ })
1295
+ if next_cursor is not None else None
1296
+ )
1297
+ result = _page_result(
1298
+ worker,
1299
+ records,
1300
+ older_cursor,
1301
+ counters,
1302
+ requested_source_record_target=source_record_target,
1303
+ response_budget_limited=response_budget_limited,
1304
+ )
1305
+ if _encoded_size(result) <= PAGE_RESULT_MAX_BYTES:
1306
+ return result
1307
+ response_budget_limited = True
1308
+ if fetch_target <= 1:
1309
+ if len(native_items) > fetch_target:
1310
+ # A provider that ignores limit=1 gives us no cursor boundary
1311
+ # immediately after only the newest record. Returning its
1312
+ # nextCursor after dropping siblings would skip history. Refuse
1313
+ # this provider-contract violation rather than invent a cursor.
1314
+ raise ProviderProtocolError(
1315
+ "Codex thread item page exceeded requested limit at final "
1316
+ "conversation read limit",
1317
+ short=short,
1318
+ )
1319
+ if records:
1320
+ records = [_last_resort_record_stub(records[-1], counters)]
1321
+ return _page_result(
1322
+ worker,
1323
+ records,
1324
+ older_cursor,
1325
+ counters,
1326
+ requested_source_record_target=source_record_target,
1327
+ response_budget_limited=True,
1328
+ )
1329
+
1330
+ # Refetch from the same incoming provider cursor with a smaller native
1331
+ # limit. Base the next request on what the provider actually returned,
1332
+ # not merely what we asked for, and spend one shared caller deadline.
1333
+ previous_target = fetch_target
1334
+ fetch_target = max(1, min(fetch_target, len(native_items)) // 2)
1335
+ if fetch_target >= previous_target:
1336
+ fetch_target = max(1, previous_target // 2)
1337
+
1338
+
1339
+ __all__ = ["read_claude_page", "read_codex_page"]