ccx-native 0.0.0-stage → 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 -2
  3. package/bin/ccx +56 -0
  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 +40 -4
  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,758 @@
1
+ """Durable caller-known delivery receipts for Codex turn starts.
2
+
3
+ A receipt is persisted before a turn can be submitted. The daemon moves the
4
+ receipt to submitted immediately before the irreversible native turn/start
5
+ call, and to accepted only after a native turn id is known. A receipt observed
6
+ in submitted is never re-submitted.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import contextlib
12
+ import hashlib
13
+ import json
14
+ import os
15
+ import time
16
+ import uuid
17
+ from pathlib import Path
18
+ from typing import Any, Iterator
19
+
20
+ from ccx.codex.provenance import new_client_message_id
21
+
22
+ SCHEMA_VERSION = 2
23
+ MAX_DELIVERY_ID = 128
24
+ STATES = frozenset({"prepared", "owned", "submitted", "accepted", "rejected"})
25
+ TERMINAL_STATES = frozenset({"accepted", "rejected"})
26
+ MODES = frozenset({"new_turn_only", "auto", "steer_only"})
27
+ ROUTES = frozenset({"new_turn", "steer"})
28
+ PLACEMENTS = frozenset({"new_turn", "current_turn"})
29
+ _CALLER_METADATA_UNSET = object()
30
+
31
+
32
+ class DeliveryReceiptError(RuntimeError):
33
+ pass
34
+
35
+
36
+ class DeliveryReceiptConflict(DeliveryReceiptError):
37
+ pass
38
+
39
+
40
+ def now_ms() -> int:
41
+ return int(time.time() * 1000)
42
+
43
+
44
+ def new_delivery_id() -> str:
45
+ return str(uuid.uuid4())
46
+
47
+
48
+ def valid_delivery_id(value: object) -> bool:
49
+ return (
50
+ isinstance(value, str)
51
+ and bool(value)
52
+ and len(value) <= MAX_DELIVERY_ID
53
+ and "\x00" not in value
54
+ )
55
+
56
+
57
+ def _store():
58
+ from ccx.constants import CCX_ROOT
59
+ from ccx.store.root import require_ccx_active
60
+ return require_ccx_active(CCX_ROOT)
61
+
62
+
63
+ def _owner(store, target: str):
64
+ owner = store.resolve(target)
65
+ if owner.engine != "codex":
66
+ from ccx.store.identity import EngineMismatch
67
+ raise EngineMismatch("Delivery requires a Codex worker handle")
68
+ return owner
69
+
70
+
71
+ def _key(delivery_id: str) -> str:
72
+ return hashlib.sha256(delivery_id.encode("utf-8")).hexdigest()
73
+
74
+
75
+ def receipt_path(target: str, delivery_id: str) -> Path:
76
+ """Projection for diagnostics only; never opened by actual receipt I/O."""
77
+ store = _store()
78
+ owner = _owner(store, target)
79
+ return (store.pointer.generation_root / "semantic-deliveries"
80
+ / "codex" / owner.worker_id / (_key(delivery_id) + ".json"))
81
+
82
+
83
+ def _prompt_identity(text: str) -> tuple[str, int]:
84
+ raw = text.encode("utf-8")
85
+ return hashlib.sha256(raw).hexdigest(), len(raw)
86
+
87
+
88
+ @contextlib.contextmanager
89
+ def receipt_lock(short: str, delivery_id: str) -> Iterator[None]:
90
+ """Original Codex per-delivery lock on a stable full-UUID generation inode."""
91
+ from ccx.store.root import _flock_at
92
+ if not valid_delivery_id(delivery_id):
93
+ raise DeliveryReceiptError("invalid delivery id")
94
+ store = _store()
95
+ owner = _owner(store, short)
96
+ with store.lease():
97
+ with store._gen("semantic-deliveries", "codex",
98
+ owner.worker_id, create=True) as fd:
99
+ with _flock_at(fd, _key(delivery_id) + ".lock",
100
+ exclusive=True, create=True, timeout=.5):
101
+ yield
102
+
103
+
104
+ def _validate(value: Any, *, short: str, delivery_id: str) -> dict[str, Any]:
105
+ if not isinstance(value, dict):
106
+ raise DeliveryReceiptError("Codex send receipt has invalid shape")
107
+ if value.get("schemaVersion") != SCHEMA_VERSION:
108
+ raise DeliveryReceiptError("unsupported Codex send receipt version")
109
+ store = _store()
110
+ owner = _owner(store, short)
111
+ if (value.get("workerId") != owner.worker_id
112
+ or value.get("engine") != "codex"
113
+ or value.get("generation") != store.generation
114
+ or value.get("deliveryId") != delivery_id):
115
+ raise DeliveryReceiptError("Codex delivery belongs to another worker; check its ID")
116
+ if value.get("state") not in STATES:
117
+ raise DeliveryReceiptError("Codex send receipt invalid state")
118
+ return value
119
+
120
+
121
+ def read_receipt(short: str, delivery_id: str) -> dict[str, Any] | None:
122
+ from ccx.store.root import _read_json
123
+ if not valid_delivery_id(delivery_id):
124
+ raise DeliveryReceiptError("invalid delivery id")
125
+ store = _store()
126
+ owner = _owner(store, short)
127
+ with store.lease():
128
+ try:
129
+ with store._gen("semantic-deliveries", "codex",
130
+ owner.worker_id) as fd:
131
+ value = _read_json(fd, _key(delivery_id) + ".json")
132
+ except FileNotFoundError:
133
+ value = None
134
+ return _validate(value, short=short, delivery_id=delivery_id) if value is not None else None
135
+
136
+
137
+ def _write(short: str, delivery_id: str, value: dict[str, Any]) -> None:
138
+ from ccx.store.root import _write_json
139
+ store = _store()
140
+ owner = _owner(store, short)
141
+ if "workerShort" in value:
142
+ raise DeliveryReceiptError("legacy workerShort authority forbidden")
143
+ value["updatedAtMs"] = now_ms()
144
+ value["workerId"] = owner.worker_id
145
+ value["engine"] = "codex"
146
+ value["generation"] = store.generation
147
+ with store.lease():
148
+ with store._gen("semantic-deliveries", "codex",
149
+ owner.worker_id, create=True) as fd:
150
+ _write_json(fd, _key(delivery_id) + ".json", value)
151
+
152
+
153
+ def requested_mode(entry: dict[str, Any]) -> str:
154
+ """Old receipts predate modes and are strict new-turn deliveries."""
155
+ value = entry.get("requestedMode")
156
+ return value if value in MODES else "new_turn_only"
157
+
158
+
159
+ def intent_spec(entry: dict[str, Any]) -> dict[str, Any]:
160
+ """Immutable pre-route native identity: auto/steer choose their client id later."""
161
+ mode = requested_mode(entry)
162
+ return {
163
+ "deliveryId": entry["deliveryId"],
164
+ "promptSha256": entry["promptSha256"],
165
+ "promptByteCount": entry["promptByteCount"],
166
+ "requestedMode": mode,
167
+ "callerMetadata": entry.get("callerMetadata"),
168
+ "cap": entry.get("cap"),
169
+ "clientUserMessageId": (
170
+ entry["clientUserMessageId"] if mode == "new_turn_only" else None
171
+ ),
172
+ }
173
+
174
+
175
+ def route(entry: dict[str, Any]) -> str | None:
176
+ value = entry.get("route")
177
+ if value in ROUTES:
178
+ return value
179
+ # A submitted/accepted old receipt could only have opened a new turn.
180
+ if entry.get("state") in {"submitted", "accepted"}:
181
+ return "new_turn"
182
+ return None
183
+
184
+
185
+ def placement(entry: dict[str, Any]) -> str | None:
186
+ value = entry.get("placement")
187
+ if value in PLACEMENTS:
188
+ return value
189
+ if entry.get("state") == "accepted":
190
+ return "current_turn" if route(entry) == "steer" else "new_turn"
191
+ return None
192
+
193
+
194
+ def turn_ref_for(turn_id: str | None) -> str | None:
195
+ if not isinstance(turn_id, str) or not turn_id:
196
+ return None
197
+ return f"ccx-turn:codex:v1:{turn_id}"
198
+
199
+
200
+ def _assert_semantics(
201
+ entry: dict[str, Any],
202
+ text: str,
203
+ mode: str,
204
+ *,
205
+ caller_metadata: Any = _CALLER_METADATA_UNSET,
206
+ cap: dict | None = None,
207
+ ) -> None:
208
+ digest, byte_count = _prompt_identity(text)
209
+ metadata_conflict = (
210
+ caller_metadata is not _CALLER_METADATA_UNSET
211
+ and entry.get("callerMetadata") != caller_metadata
212
+ )
213
+ if (
214
+ entry.get("promptSha256") != digest
215
+ or entry.get("promptByteCount") != byte_count
216
+ or requested_mode(entry) != mode
217
+ or metadata_conflict
218
+ or entry.get("cap") != cap
219
+ ):
220
+ raise DeliveryReceiptConflict(
221
+ "delivery_id is already owned by different prompt content, mode, "
222
+ "caller metadata, or cap"
223
+ )
224
+
225
+
226
+ def prepare(
227
+ short: str,
228
+ delivery_id: str,
229
+ text: str,
230
+ *,
231
+ mode: str = "new_turn_only",
232
+ caller_metadata: Any = _CALLER_METADATA_UNSET,
233
+ cap: dict | None = None,
234
+ ) -> tuple[dict[str, Any], bool]:
235
+ if not valid_delivery_id(delivery_id):
236
+ raise DeliveryReceiptError(
237
+ "delivery_id must be a non-empty string of at most 128 characters"
238
+ )
239
+ if not isinstance(text, str) or not text.strip():
240
+ raise DeliveryReceiptError("prompt text is required")
241
+ if mode not in MODES:
242
+ raise DeliveryReceiptError(f"unsupported delivery mode: {mode!r}")
243
+ with receipt_lock(short, delivery_id):
244
+ prior = read_receipt(short, delivery_id)
245
+ if prior is not None:
246
+ _assert_semantics(
247
+ prior,
248
+ text,
249
+ mode,
250
+ caller_metadata=caller_metadata,
251
+ cap=cap,
252
+ )
253
+ return prior, False
254
+ digest, byte_count = _prompt_identity(text)
255
+ created = now_ms()
256
+ value = {
257
+ "schemaVersion": SCHEMA_VERSION,
258
+ "workerId": _owner(_store(), short).worker_id,
259
+ "engine": "codex",
260
+ "generation": _store().generation,
261
+ "deliveryId": delivery_id,
262
+ "promptSha256": digest,
263
+ "promptByteCount": byte_count,
264
+ "requestedMode": mode,
265
+ "cap": cap,
266
+ "callerMetadata": (
267
+ None
268
+ if caller_metadata is _CALLER_METADATA_UNSET
269
+ else caller_metadata
270
+ ),
271
+ # Preserve the landed strict-send contract: new_turn_only already
272
+ # knows its only possible provenance kind at prepare time. Auto must
273
+ # defer until codexd freezes start versus steer.
274
+ "clientUserMessageId": (
275
+ new_client_message_id(_owner(_store(), short).worker_id, "start")
276
+ if mode == "new_turn_only"
277
+ else None
278
+ ),
279
+ "state": "prepared",
280
+ "route": None,
281
+ "placement": None,
282
+ "targetTurnId": None,
283
+ "turnRef": None,
284
+ "ownedAtMs": None,
285
+ "createdAtMs": created,
286
+ "updatedAtMs": created,
287
+ "submittedAtMs": None,
288
+ "acceptedAtMs": None,
289
+ "rejectedAtMs": None,
290
+ "turnId": None,
291
+ "rejectionCode": None,
292
+ "rejectionMessage": None,
293
+ "lastRefusalCode": None,
294
+ "lastRefusalMessage": None,
295
+ "lastRefusalState": None,
296
+ "lastRefusalAtMs": None,
297
+ }
298
+ _write(short, delivery_id, value)
299
+ return dict(value), True
300
+
301
+
302
+ def require_matching(
303
+ short: str,
304
+ delivery_id: str,
305
+ text: str,
306
+ *,
307
+ mode: str = "new_turn_only",
308
+ caller_metadata: Any = _CALLER_METADATA_UNSET,
309
+ cap: dict | None = None,
310
+ ) -> dict[str, Any]:
311
+ value = read_receipt(short, delivery_id)
312
+ if value is None:
313
+ raise DeliveryReceiptError(
314
+ "Codex send receipt is missing; prepare the delivery before submission"
315
+ )
316
+ _assert_semantics(
317
+ value,
318
+ text,
319
+ mode,
320
+ caller_metadata=caller_metadata,
321
+ cap=cap,
322
+ )
323
+ return value
324
+
325
+
326
+ def _transition(
327
+ short: str,
328
+ delivery_id: str,
329
+ allowed: set[str],
330
+ state: str,
331
+ **patch: Any,
332
+ ) -> dict[str, Any]:
333
+ with receipt_lock(short, delivery_id):
334
+ value = read_receipt(short, delivery_id)
335
+ if value is None:
336
+ raise DeliveryReceiptError(f"unknown Codex delivery: {delivery_id}")
337
+ current = str(value.get("state"))
338
+ if current == state:
339
+ return value
340
+ if current not in allowed:
341
+ raise DeliveryReceiptError(
342
+ f"invalid Codex delivery transition {current!r} -> {state!r}"
343
+ )
344
+ updated = {**value, **patch, "state": state}
345
+ _write(short, delivery_id, updated)
346
+ return updated
347
+
348
+
349
+ def mark_owned(
350
+ short: str,
351
+ delivery_id: str,
352
+ *,
353
+ route_kind: str,
354
+ target_turn_id: str | None = None,
355
+ ) -> dict[str, Any]:
356
+ if route_kind not in ROUTES:
357
+ raise DeliveryReceiptError(f"invalid delivery route: {route_kind!r}")
358
+ if route_kind == "steer" and not target_turn_id:
359
+ raise DeliveryReceiptError("steer route requires target turn id")
360
+ with receipt_lock(short, delivery_id):
361
+ value = read_receipt(short, delivery_id)
362
+ if value is None:
363
+ raise DeliveryReceiptError(f"unknown Codex delivery: {delivery_id}")
364
+ current = value.get("state")
365
+ if current in {"owned", "submitted", "accepted"}:
366
+ if route(value) != route_kind:
367
+ raise DeliveryReceiptConflict("owned delivery route mismatch")
368
+ if route_kind == "steer" and value.get("targetTurnId") != target_turn_id:
369
+ raise DeliveryReceiptConflict("owned delivery target-turn mismatch")
370
+ return value
371
+ if current != "prepared":
372
+ return value
373
+ existing_client = value.get("clientUserMessageId")
374
+ client_id = (
375
+ existing_client
376
+ if isinstance(existing_client, str) and existing_client
377
+ else new_client_message_id(
378
+ _owner(_store(), short).worker_id,
379
+ "steer" if route_kind == "steer" else "start"
380
+ )
381
+ )
382
+ updated = {
383
+ **value,
384
+ "state": "owned",
385
+ "route": route_kind,
386
+ "targetTurnId": target_turn_id,
387
+ "clientUserMessageId": client_id,
388
+ "ownedAtMs": now_ms(),
389
+ "lastRefusalCode": None,
390
+ "lastRefusalMessage": None,
391
+ "lastRefusalState": None,
392
+ "lastRefusalAtMs": None,
393
+ }
394
+ _write(short, delivery_id, updated)
395
+ return updated
396
+
397
+
398
+ def rollback_retryable(
399
+ short: str,
400
+ delivery_id: str,
401
+ *,
402
+ code: str,
403
+ message: str,
404
+ state: str | None = None,
405
+ ) -> dict[str, Any]:
406
+ """Return an explicitly non-mutated owned/submitted delivery to prepared."""
407
+ with receipt_lock(short, delivery_id):
408
+ value = read_receipt(short, delivery_id)
409
+ if value is None:
410
+ raise DeliveryReceiptError(f"unknown Codex delivery: {delivery_id}")
411
+ if value.get("state") not in {"owned", "submitted"}:
412
+ return value
413
+ updated = _prepared_again(value, code=code, message=message, state=state)
414
+ _write(short, delivery_id, updated)
415
+ return updated
416
+
417
+
418
+ def _prepared_again(
419
+ value: dict[str, Any], *, code: str, message: str, state: str | None
420
+ ) -> dict[str, Any]:
421
+ return {
422
+ **value,
423
+ "state": "prepared",
424
+ "route": None,
425
+ "placement": None,
426
+ "targetTurnId": None,
427
+ "turnRef": None,
428
+ "clientUserMessageId": None,
429
+ "ownedAtMs": None,
430
+ "submittedAtMs": None,
431
+ "submittedRuntimeId": None,
432
+ "lastRefusalCode": code,
433
+ "lastRefusalMessage": message,
434
+ "lastRefusalState": state,
435
+ "lastRefusalAtMs": now_ms(),
436
+ }
437
+
438
+
439
+ def mark_submitted(
440
+ short: str, delivery_id: str, *, runtime_id: str | None = None
441
+ ) -> dict[str, Any]:
442
+ """Freeze the delivery immediately before its native request is written.
443
+
444
+ ``runtime_id`` names the codexd lifetime (and so the one app-server
445
+ connection) that may still complete the request. It is diagnostic
446
+ provenance only: a vanished runtime never proves the request did not land.
447
+ """
448
+ with receipt_lock(short, delivery_id):
449
+ value = read_receipt(short, delivery_id)
450
+ if value is None:
451
+ raise DeliveryReceiptError(f"unknown Codex delivery: {delivery_id}")
452
+ current = value.get("state")
453
+ if current == "submitted":
454
+ return value
455
+ if current == "prepared":
456
+ if requested_mode(value) != "new_turn_only":
457
+ raise DeliveryReceiptError(
458
+ "Message has no destination; wait for a worker turn"
459
+ )
460
+ client_id = value.get("clientUserMessageId")
461
+ if not isinstance(client_id, str) or not client_id:
462
+ client_id = new_client_message_id(_owner(_store(), short).worker_id, "start")
463
+ value = {
464
+ **value,
465
+ "state": "owned",
466
+ "route": "new_turn",
467
+ "targetTurnId": None,
468
+ "clientUserMessageId": client_id,
469
+ "ownedAtMs": now_ms(),
470
+ }
471
+ if value.get("state") != "owned":
472
+ raise DeliveryReceiptError(
473
+ f"invalid Codex delivery transition {value.get('state')!r} -> 'submitted'"
474
+ )
475
+ updated = {
476
+ **value,
477
+ "state": "submitted",
478
+ "submittedAtMs": now_ms(),
479
+ "submittedRuntimeId": runtime_id,
480
+ "lastRefusalCode": None,
481
+ "lastRefusalMessage": None,
482
+ "lastRefusalState": None,
483
+ "lastRefusalAtMs": None,
484
+ }
485
+ _write(short, delivery_id, updated)
486
+ return updated
487
+
488
+
489
+ def mark_retryable_refusal(
490
+ short: str,
491
+ delivery_id: str,
492
+ *,
493
+ code: str,
494
+ message: str,
495
+ state: str | None = None,
496
+ ) -> dict[str, Any]:
497
+ """Persist one definitive pre-submission refusal without burning the id."""
498
+ with receipt_lock(short, delivery_id):
499
+ value = read_receipt(short, delivery_id)
500
+ if value is None:
501
+ raise DeliveryReceiptError(f"unknown Codex delivery: {delivery_id}")
502
+ if value.get("state") != "prepared":
503
+ return value
504
+ updated = {
505
+ **value,
506
+ "lastRefusalCode": code,
507
+ "lastRefusalMessage": message,
508
+ "lastRefusalState": state,
509
+ "lastRefusalAtMs": now_ms(),
510
+ }
511
+ _write(short, delivery_id, updated)
512
+ return updated
513
+
514
+
515
+ def mark_accepted(
516
+ short: str, delivery_id: str, *, turn_id: str
517
+ ) -> dict[str, Any]:
518
+ if not isinstance(turn_id, str) or not turn_id:
519
+ raise DeliveryReceiptError("Accepted Codex message has no turn ID; inspect the delivery")
520
+ with receipt_lock(short, delivery_id):
521
+ value = read_receipt(short, delivery_id)
522
+ if value is None:
523
+ raise DeliveryReceiptError(f"unknown Codex delivery: {delivery_id}")
524
+ if value.get("state") == "accepted":
525
+ if value.get("turnId") != turn_id:
526
+ raise DeliveryReceiptConflict("accepted delivery turn mismatch")
527
+ return value
528
+ if value.get("state") != "submitted":
529
+ raise DeliveryReceiptError(
530
+ f"cannot accept Codex delivery from state={value.get('state')!r}"
531
+ )
532
+ actual_route = route(value) or "new_turn"
533
+ updated = {
534
+ **value,
535
+ "state": "accepted",
536
+ "acceptedAtMs": now_ms(),
537
+ "turnId": turn_id,
538
+ "placement": "current_turn" if actual_route == "steer" else "new_turn",
539
+ "turnRef": turn_ref_for(turn_id),
540
+ "rejectionCode": None,
541
+ "rejectionMessage": None,
542
+ }
543
+ _write(short, delivery_id, updated)
544
+ return updated
545
+
546
+
547
+ def mark_settled(
548
+ short: str, delivery_id: str, *, turn_id: str,
549
+ original_positive_completion: dict[str, Any],
550
+ ) -> dict[str, Any]:
551
+ """Immutably attach original native turn/completed evidence to accepted ID."""
552
+ from ccx.store.root import _HASH
553
+ if (not isinstance(original_positive_completion, dict)
554
+ or original_positive_completion.get("turnId") != turn_id
555
+ or original_positive_completion.get("status") != "completed"
556
+ or not isinstance(original_positive_completion.get("text"), str)
557
+ or not isinstance(original_positive_completion.get("nativeEvidenceSha256"), str)
558
+ or not _HASH.fullmatch(original_positive_completion["nativeEvidenceSha256"])):
559
+ raise DeliveryReceiptError("Codex turn completion is unconfirmed; inspect the delivery")
560
+ with receipt_lock(short, delivery_id):
561
+ value = read_receipt(short, delivery_id)
562
+ if (value is None or value.get("state") != "accepted"
563
+ or value.get("turnId") != turn_id):
564
+ raise DeliveryReceiptError("Codex turn was not accepted; inspect the delivery")
565
+ prior = value.get("settlement")
566
+ if prior is not None:
567
+ # Live item/completed and source-fenced thread/read can render a
568
+ # different answer (and therefore a different evidence digest) for
569
+ # the very same completed native turn. The first proof is immutable;
570
+ # only a different original identity is a conflicting observation.
571
+ identity = ("workerId", "engine", "nativeSource", "threadId",
572
+ "turnId", "deliveryId", "status")
573
+ if (not isinstance(prior, dict) or any(
574
+ prior.get(field) != original_positive_completion.get(field)
575
+ for field in identity
576
+ )):
577
+ raise DeliveryReceiptConflict("Codex completion record changed; inspect the worker")
578
+ return value
579
+ result = dict(value)
580
+ result["settlement"] = dict(original_positive_completion)
581
+ result["settledAtMs"] = now_ms()
582
+ _write(short, delivery_id, result)
583
+ return result
584
+
585
+
586
+ def mark_rejected(
587
+ short: str,
588
+ delivery_id: str,
589
+ *,
590
+ code: str,
591
+ message: str,
592
+ ) -> dict[str, Any]:
593
+ return _transition(
594
+ short,
595
+ delivery_id,
596
+ {"prepared", "submitted"},
597
+ "rejected",
598
+ rejectedAtMs=now_ms(),
599
+ rejectionCode=code,
600
+ rejectionMessage=message,
601
+ )
602
+
603
+
604
+ def _receipt_rows(short: str, *, strict: bool) -> Iterator[dict[str, Any]]:
605
+ """Reuse original Codex integrity distinction: status tolerates damage,
606
+ native-turn ownership decisions must see and refuse unrelated corruption.
607
+ """
608
+ from ccx.store.root import _read_json, StorageIntegrityError
609
+ store = _store()
610
+ owner = _owner(store, short)
611
+ with store.lease():
612
+ try:
613
+ with store._gen("semantic-deliveries", "codex",
614
+ owner.worker_id) as fd:
615
+ with os.scandir(fd) as entries:
616
+ names = sorted(entry.name for entry in entries
617
+ if entry.name.endswith(".json"))
618
+ if len(names) > 100000:
619
+ raise DeliveryReceiptError("Too many Codex delivery records; narrow the request")
620
+ rows = []
621
+ for name in names:
622
+ try:
623
+ rows.append(_read_json(fd, name))
624
+ except StorageIntegrityError as exc:
625
+ if strict:
626
+ raise DeliveryReceiptError(
627
+ "Cannot read Codex turn record "
628
+ + name
629
+ ) from exc
630
+ except FileNotFoundError:
631
+ rows = []
632
+ for value in rows:
633
+ try:
634
+ if not isinstance(value, dict):
635
+ raise DeliveryReceiptError("Codex semantic receipt invalid shape")
636
+ delivery_id = value.get("deliveryId")
637
+ if not valid_delivery_id(delivery_id):
638
+ raise DeliveryReceiptError("Codex delivery ID is invalid; use the saved delivery ID")
639
+ yield _validate(value, short=short, delivery_id=delivery_id)
640
+ except DeliveryReceiptError:
641
+ if strict:
642
+ raise
643
+
644
+
645
+ def iter_receipts(short: str) -> Iterator[dict[str, Any]]:
646
+ """Original best-effort status listing; no native ownership decisions."""
647
+ yield from _receipt_rows(short, strict=False)
648
+
649
+
650
+ def receipt_for_turn(short: str, turn_id: str) -> dict[str, Any] | None:
651
+ """Resolve one native turn to its semantic delivery without hiding damage.
652
+
653
+ General receipt enumeration is intentionally best-effort so status/reporting
654
+ can survive an unrelated corrupt sidecar. Native-turn reconciliation is an
655
+ identity decision, though: silently skipping corruption there could erase
656
+ delivery provenance or conceal a second receipt claiming the same turn.
657
+ """
658
+ matches = [
659
+ value for value in _receipt_rows(short, strict=True)
660
+ if value.get("turnId") == turn_id
661
+ ]
662
+ # Steered deliveries legitimately share the turn they joined, so only the
663
+ # delivery that opened the turn identifies it; two openers is damage.
664
+ openers = [value for value in matches if route(value) != "steer"]
665
+ if len(openers) > 1:
666
+ raise DeliveryReceiptError(
667
+ f"multiple Codex deliveries claim to have opened native turn {turn_id}"
668
+ )
669
+ if openers:
670
+ return openers[0]
671
+ # Opened by input without a receipt (a raw send or a parent notification):
672
+ # the first delivery steered into it is the earliest semantic evidence.
673
+ matches.sort(key=lambda value: (value.get("acceptedAtMs") or 0,
674
+ str(value.get("deliveryId"))))
675
+ return matches[0] if matches else None
676
+
677
+
678
+ def public_receipt(value: dict[str, Any] | None) -> dict[str, Any] | None:
679
+ if value is None:
680
+ return None
681
+ state = value.get("state")
682
+ mode = requested_mode(value)
683
+ actual_route = route(value)
684
+ actual_placement = placement(value)
685
+ turn_id = value.get("turnId")
686
+ if state in {"prepared", "owned"}:
687
+ action = "retry_same_id"
688
+ elif state == "submitted":
689
+ action = "wait"
690
+ else:
691
+ action = "none"
692
+ return {
693
+ "delivery_id": value.get("deliveryId"),
694
+ # Detailed durable receipt state; The public protocol normalizes its own
695
+ # top-level reconciliation vocabulary separately.
696
+ "status": state,
697
+ "requested_mode": mode,
698
+ "caller_metadata": value.get("callerMetadata"),
699
+ "cap": value.get("cap"),
700
+ "owned": state in {"owned", "submitted", "accepted"},
701
+ "recommended_action": action,
702
+ "route": actual_route,
703
+ "placement": actual_placement,
704
+ "target_turn_id": value.get("targetTurnId"),
705
+ "turn_ref": value.get("turnRef") or (
706
+ turn_ref_for(turn_id) if state == "accepted" else None
707
+ ),
708
+ "turn_id": turn_id,
709
+ "client_user_message_id": value.get("clientUserMessageId"),
710
+ "created_at_ms": value.get("createdAtMs"),
711
+ "updated_at_ms": value.get("updatedAtMs"),
712
+ "owned_at_ms": value.get("ownedAtMs"),
713
+ "submitted_at_ms": value.get("submittedAtMs"),
714
+ "accepted_at_ms": value.get("acceptedAtMs"),
715
+ "settled_at_ms": value.get("settledAtMs"),
716
+ "settlement": value.get("settlement"),
717
+ "rejected_at_ms": value.get("rejectedAtMs"),
718
+ "rejection_code": value.get("rejectionCode"),
719
+ "rejection_message": value.get("rejectionMessage"),
720
+ "last_refusal": (
721
+ {
722
+ "code": value.get("lastRefusalCode"),
723
+ "message": value.get("lastRefusalMessage"),
724
+ "state": value.get("lastRefusalState"),
725
+ "at_ms": value.get("lastRefusalAtMs"),
726
+ }
727
+ if value.get("lastRefusalCode") or value.get("lastRefusalMessage")
728
+ else None
729
+ ),
730
+ "pending": state in {"prepared", "owned", "submitted"},
731
+ }
732
+
733
+
734
+
735
+ __all__ = [
736
+ "DeliveryReceiptConflict",
737
+ "DeliveryReceiptError",
738
+ "MAX_DELIVERY_ID",
739
+ "TERMINAL_STATES",
740
+ "iter_receipts",
741
+ "mark_accepted",
742
+ "mark_owned",
743
+ "mark_rejected",
744
+ "mark_retryable_refusal",
745
+ "rollback_retryable",
746
+ "mark_submitted",
747
+ "new_delivery_id",
748
+ "prepare",
749
+ "placement",
750
+ "public_receipt",
751
+ "requested_mode",
752
+ "route",
753
+ "read_receipt",
754
+ "receipt_for_turn",
755
+ "require_matching",
756
+ "turn_ref_for",
757
+ "valid_delivery_id",
758
+ ]