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,806 @@
1
+ """Durable semantic-delivery receipts for managed Claude workers.
2
+
3
+ The bounded per-session deliveries sidecar remains the hot PTY-placement
4
+ journal. This module owns long-lived caller identity and reconciliation facts so
5
+ hot-journal eviction can never make a semantic delivery id reusable. Terminal
6
+ receipts are retained for ``RECEIPT_RETENTION_S`` after their last update (far
7
+ beyond any retry of a lost reply) and then pruned at host startup, unless the
8
+ hot journal still references them.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import contextlib
14
+ import hashlib
15
+ import json
16
+ import math
17
+ import os
18
+ import threading
19
+ import time
20
+ from pathlib import Path
21
+ from typing import Any, Iterator
22
+
23
+
24
+ SCHEMA_VERSION = 2
25
+ MAX_DELIVERY_ID = 128
26
+ MODES = frozenset({"new_turn_only", "auto", "steer_only"})
27
+ ROUTES = frozenset({"new_turn", "steer"})
28
+ PLACEMENTS = frozenset({"new_turn", "current_turn"})
29
+ STATES = frozenset({"prepared", "owned", "accepted", "rejected",
30
+ "delivered", "abandoned"})
31
+ TERMINAL_STATES = frozenset({"accepted", "rejected", "delivered", "abandoned"})
32
+ _CALLER_METADATA_UNSET = object()
33
+
34
+ HOST_LOCK_TIMEOUT_S = 2.0
35
+ CLIENT_LOCK_TIMEOUT_S = 15.0
36
+ _LOCK_POLL_S = 0.02
37
+ RECEIPT_RETENTION_S = 30 * 24 * 60 * 60
38
+
39
+ _LOCAL = threading.local()
40
+ _THREAD_LOCKS: dict[str, threading.RLock] = {}
41
+ _THREAD_LOCKS_GUARD = threading.Lock()
42
+
43
+
44
+ class ClaudeDeliveryError(RuntimeError):
45
+ """A Claude semantic receipt cannot be trusted or transitioned."""
46
+
47
+
48
+ class ClaudeDeliveryConflict(ClaudeDeliveryError):
49
+ """The caller reused a delivery id with different semantic content."""
50
+
51
+
52
+ class ClaudeDeliveryBusy(ClaudeDeliveryError):
53
+ """The receipt lock could not be acquired within the bounded wait."""
54
+
55
+
56
+ def now_ms() -> int:
57
+ return int(time.time() * 1000)
58
+
59
+
60
+ def valid_delivery_id(value: object) -> bool:
61
+ return (
62
+ isinstance(value, str)
63
+ and bool(value)
64
+ and len(value) <= MAX_DELIVERY_ID
65
+ and "\x00" not in value
66
+ )
67
+
68
+
69
+ def _store():
70
+ """Resolve one ACTIVE full-ID global owner; never initialize legacy root."""
71
+ from ccx.constants import CCX_ROOT
72
+ from ccx.store.root import require_ccx_active
73
+ return require_ccx_active(CCX_ROOT)
74
+
75
+
76
+ def _owner(store, target: str):
77
+ owner = store.resolve(target)
78
+ if owner.engine != "claude":
79
+ from ccx.store.identity import EngineMismatch
80
+ raise EngineMismatch("Delivery requires a Claude worker handle")
81
+ return owner
82
+
83
+
84
+ def _key(delivery_id: str) -> str:
85
+ return hashlib.sha256(delivery_id.encode("utf-8")).hexdigest()
86
+
87
+
88
+ def receipt_path(target: str, delivery_id: str) -> Path:
89
+ """Display-only diagnostic path; never open it for internal receipt I/O."""
90
+ store = _store()
91
+ owner = _owner(store, target)
92
+ return (store.pointer.generation_root / "semantic-deliveries"
93
+ / "claude" / owner.worker_id / (_key(delivery_id) + ".json"))
94
+
95
+
96
+ def _thread_lock(key: str) -> threading.RLock:
97
+ with _THREAD_LOCKS_GUARD:
98
+ lock = _THREAD_LOCKS.get(key)
99
+ if lock is None:
100
+ lock = _THREAD_LOCKS[key] = threading.RLock()
101
+ return lock
102
+
103
+
104
+ @contextlib.contextmanager
105
+ def receipt_lock(
106
+ short: str, delivery_id: str, *, timeout: float | None = None
107
+ ) -> Iterator[None]:
108
+ """Stable per-(generation,full UUID,semantic delivery) POSIX lock.
109
+
110
+ Every actual read/write is fd-relative and nofollow. The outer ACTIVE
111
+ lease covers the bounded receipt update but never a model turn. All
112
+ receipt-state transition functions remain the original Claude code.
113
+ """
114
+ from ccx.store.root import _flock_at, GenerationBusy
115
+ if not valid_delivery_id(delivery_id):
116
+ raise ClaudeDeliveryError("invalid semantic delivery_id")
117
+ store = _store()
118
+ owner = _owner(store, short)
119
+ budget = CLIENT_LOCK_TIMEOUT_S if timeout is None else max(0.0, float(timeout))
120
+ if budget > 3:
121
+ budget = 3.0
122
+ key = f"{store.generation}:{owner.worker_id}:{_key(delivery_id)}"
123
+ held = getattr(_LOCAL, "held", None)
124
+ if held is None:
125
+ held = _LOCAL.held = set()
126
+ if key in held:
127
+ with store.lease():
128
+ yield
129
+ return
130
+ thread_lock = _thread_lock(key)
131
+ if not thread_lock.acquire(timeout=budget):
132
+ raise ClaudeDeliveryBusy(f"delivery_id {delivery_id!r} is locally locked")
133
+ try:
134
+ with store.lease():
135
+ with store._gen("semantic-deliveries", "claude",
136
+ owner.worker_id, create=True) as fd:
137
+ try:
138
+ with _flock_at(fd, _key(delivery_id) + ".lock",
139
+ exclusive=True, create=True, timeout=budget):
140
+ held.add(key)
141
+ try:
142
+ yield
143
+ finally:
144
+ held.discard(key)
145
+ except GenerationBusy as exc:
146
+ raise ClaudeDeliveryBusy("Claude receipt lock contested") from exc
147
+ finally:
148
+ thread_lock.release()
149
+
150
+
151
+ def validate_cap(cap: dict | None) -> dict | None:
152
+ """One valid per-message Claude quota cap, never silently uncapped."""
153
+ if cap is None:
154
+ return None
155
+ if not isinstance(cap, dict):
156
+ raise ClaudeDeliveryError("cap must be an object of quota percentages")
157
+ if not cap:
158
+ return None
159
+ if any(
160
+ key not in {"5h", "7d", "opus"}
161
+ or type(value) not in (int, float)
162
+ or not math.isfinite(value)
163
+ or not 0 <= value <= 100
164
+ for key, value in cap.items()
165
+ ):
166
+ raise ClaudeDeliveryError("cap keys must be 5h/7d/opus and values finite 0..100")
167
+ return dict(cap)
168
+
169
+
170
+ def spec_digest(receipt: dict) -> str:
171
+ """Original Claude immutable identity; keep no-cap strict-send bytes.
172
+
173
+ The native producer, caller and read-only completion reconciler use the
174
+ same fields/serializer. Codex has a different message-id-bound identity.
175
+ """
176
+ from ccx.store.store import _digest
177
+ return _digest({
178
+ "deliveryId": receipt["deliveryId"],
179
+ "promptSha256": receipt["promptSha256"],
180
+ "promptByteCount": receipt["promptByteCount"],
181
+ "requestedMode": receipt["requestedMode"],
182
+ "callerMetadata": receipt.get("callerMetadata"),
183
+ "cap": receipt.get("cap"),
184
+ })
185
+
186
+
187
+ def _prompt_identity(text: str) -> tuple[str, int]:
188
+ raw = text.encode("utf-8")
189
+ return hashlib.sha256(raw).hexdigest(), len(raw)
190
+
191
+
192
+ def _validate(value: Any, *, short: str, delivery_id: str) -> dict[str, Any]:
193
+ if not isinstance(value, dict):
194
+ raise ClaudeDeliveryError("Claude delivery receipt has an invalid shape")
195
+ if value.get("schemaVersion") != SCHEMA_VERSION:
196
+ raise ClaudeDeliveryError("unsupported Claude delivery receipt version")
197
+ store = _store()
198
+ owner = _owner(store, short)
199
+ if (value.get("workerId") != owner.worker_id
200
+ or value.get("engine") != "claude"
201
+ or value.get("generation") != store.generation
202
+ or value.get("deliveryId") != delivery_id):
203
+ raise ClaudeDeliveryError("Claude delivery belongs to another worker; inspect its ID")
204
+ if value.get("state") not in STATES:
205
+ raise ClaudeDeliveryError("Claude delivery receipt has an invalid state")
206
+ if value.get("requestedMode") not in MODES:
207
+ raise ClaudeDeliveryError("Claude delivery receipt has an invalid mode")
208
+ route = value.get("route")
209
+ if route is not None and route not in ROUTES:
210
+ raise ClaudeDeliveryError("Claude delivery receipt has an invalid route")
211
+ placement = value.get("placement")
212
+ if placement is not None and placement not in PLACEMENTS:
213
+ raise ClaudeDeliveryError("Claude delivery receipt has invalid placement")
214
+ return value
215
+
216
+
217
+ def read_receipt(short: str, delivery_id: str) -> dict[str, Any] | None:
218
+ if not valid_delivery_id(delivery_id):
219
+ raise ClaudeDeliveryError("invalid delivery id")
220
+ from ccx.store.root import _read_json
221
+ store = _store()
222
+ owner = _owner(store, short)
223
+ with store.lease():
224
+ try:
225
+ with store._gen("semantic-deliveries", "claude", owner.worker_id) as fd:
226
+ value = _read_json(fd, _key(delivery_id) + ".json")
227
+ except FileNotFoundError:
228
+ value = None
229
+ return _validate(value, short=short, delivery_id=delivery_id) if value is not None else None
230
+
231
+
232
+ def _write(short: str, delivery_id: str, value: dict[str, Any]) -> None:
233
+ from ccx.store.root import _write_json
234
+ store = _store()
235
+ owner = _owner(store, short)
236
+ value["updatedAtMs"] = now_ms()
237
+ value["workerId"] = owner.worker_id
238
+ value["engine"] = "claude"
239
+ value["generation"] = store.generation
240
+ if "workerShort" in value:
241
+ raise ClaudeDeliveryError("legacy workerShort authority forbidden")
242
+ with store.lease():
243
+ with store._gen("semantic-deliveries", "claude", owner.worker_id,
244
+ create=True) as fd:
245
+ _write_json(fd, _key(delivery_id) + ".json", value)
246
+
247
+
248
+ def _assert_same(
249
+ entry: dict[str, Any],
250
+ text: str,
251
+ mode: str,
252
+ *,
253
+ caller_metadata: Any = _CALLER_METADATA_UNSET,
254
+ cap: dict | None = None,
255
+ ) -> None:
256
+ digest, byte_count = _prompt_identity(text)
257
+ metadata_conflict = (
258
+ caller_metadata is not _CALLER_METADATA_UNSET
259
+ and entry.get("callerMetadata") != caller_metadata
260
+ )
261
+ if (
262
+ entry.get("promptSha256") != digest
263
+ or entry.get("promptByteCount") != byte_count
264
+ or entry.get("requestedMode") != mode
265
+ or metadata_conflict
266
+ or entry.get("cap") != cap
267
+ ):
268
+ raise ClaudeDeliveryConflict(
269
+ "delivery_id is already owned by different prompt, mode, caps, "
270
+ "or caller metadata"
271
+ )
272
+
273
+
274
+ def require_matching(
275
+ target: str, delivery_id: str, text: str, *, mode: str,
276
+ cap: dict | None = None,
277
+ ) -> dict[str, Any] | None:
278
+ """Read-only exact original content/mode/cap check; never prepare."""
279
+ cap = validate_cap(cap)
280
+ prior = read_receipt(target, delivery_id)
281
+ if prior is not None:
282
+ _assert_same(prior, text, mode, cap=cap)
283
+ return prior
284
+
285
+
286
+ def prepare(
287
+ short: str,
288
+ delivery_id: str,
289
+ text: str,
290
+ *,
291
+ mode: str,
292
+ timeout: float | None = None,
293
+ caller_metadata: Any = _CALLER_METADATA_UNSET,
294
+ cap: dict | None = None,
295
+ ) -> tuple[dict[str, Any], bool]:
296
+ if not valid_delivery_id(delivery_id):
297
+ raise ClaudeDeliveryError(
298
+ "delivery_id must be a non-empty string of at most 128 characters"
299
+ )
300
+ if not isinstance(text, str) or not text.strip():
301
+ raise ClaudeDeliveryError("prompt text is required")
302
+ if mode not in MODES:
303
+ raise ClaudeDeliveryError(f"unsupported Claude delivery mode: {mode!r}")
304
+ cap = validate_cap(cap)
305
+ with receipt_lock(short, delivery_id, timeout=timeout):
306
+ prior = read_receipt(short, delivery_id)
307
+ if prior is not None:
308
+ _assert_same(
309
+ prior,
310
+ text,
311
+ mode,
312
+ caller_metadata=caller_metadata,
313
+ cap=cap,
314
+ )
315
+ return prior, False
316
+ digest, byte_count = _prompt_identity(text)
317
+ created = now_ms()
318
+ value = {
319
+ "schemaVersion": SCHEMA_VERSION,
320
+ "engine": "claude",
321
+ "workerId": _owner(_store(), short).worker_id,
322
+ "generation": _store().generation,
323
+ "deliveryId": delivery_id,
324
+ "promptSha256": digest,
325
+ "promptByteCount": byte_count,
326
+ "promptText": text,
327
+ "requestedMode": mode,
328
+ "cap": cap,
329
+ "callerMetadata": (
330
+ None
331
+ if caller_metadata is _CALLER_METADATA_UNSET
332
+ else caller_metadata
333
+ ),
334
+ "state": "prepared",
335
+ "route": None,
336
+ "placement": None,
337
+ "targetTurnRef": None,
338
+ "turnRef": None,
339
+ "managedTurnOrdinal": None,
340
+ "createdAtMs": created,
341
+ "updatedAtMs": created,
342
+ "ownedAtMs": None,
343
+ "acceptedAtMs": None,
344
+ "rejectedAtMs": None,
345
+ "lastRefusalCode": None,
346
+ "lastRefusalMessage": None,
347
+ "lastRefusalState": None,
348
+ "lastRefusalAtMs": None,
349
+ "attempts": 0,
350
+ "maybeWritten": False,
351
+ "evidence": None,
352
+ "evidenceUuid": None,
353
+ "evidenceOffset": None,
354
+ "evidenceStatus": None,
355
+ "rejectionCode": None,
356
+ "rejectionMessage": None,
357
+ }
358
+ _write(short, delivery_id, value)
359
+ return dict(value), True
360
+
361
+
362
+ def _mutate(
363
+ short: str,
364
+ delivery_id: str,
365
+ *,
366
+ timeout: float | None,
367
+ mutate,
368
+ ) -> dict[str, Any]:
369
+ with receipt_lock(short, delivery_id, timeout=timeout):
370
+ value = read_receipt(short, delivery_id)
371
+ if value is None:
372
+ raise ClaudeDeliveryError(f"unknown Claude delivery: {delivery_id}")
373
+ updated = mutate(dict(value))
374
+ _write(short, delivery_id, updated)
375
+ return updated
376
+
377
+
378
+ def mark_retryable_refusal(
379
+ short: str,
380
+ delivery_id: str,
381
+ *,
382
+ code: str,
383
+ message: str,
384
+ state: str | None = None,
385
+ timeout: float | None = None,
386
+ ) -> dict[str, Any]:
387
+ def mutate(value: dict[str, Any]) -> dict[str, Any]:
388
+ if value["state"] != "prepared":
389
+ return value
390
+ value.update(
391
+ lastRefusalCode=code,
392
+ lastRefusalMessage=message,
393
+ lastRefusalState=state,
394
+ lastRefusalAtMs=now_ms(),
395
+ )
396
+ return value
397
+
398
+ return _mutate(short, delivery_id, timeout=timeout, mutate=mutate)
399
+
400
+
401
+ def mark_owned(
402
+ short: str,
403
+ delivery_id: str,
404
+ *,
405
+ route: str,
406
+ target_turn_ref: str | None = None,
407
+ managed_turn_ordinal: int | None = None,
408
+ timeout: float | None = None,
409
+ ) -> dict[str, Any]:
410
+ if route not in ROUTES:
411
+ raise ClaudeDeliveryError(f"invalid Claude delivery route: {route!r}")
412
+
413
+ def mutate(value: dict[str, Any]) -> dict[str, Any]:
414
+ if value["state"] == "owned":
415
+ if (
416
+ value.get("route") != route
417
+ or value.get("targetTurnRef") != target_turn_ref
418
+ ):
419
+ raise ClaudeDeliveryConflict("owned delivery route/target mismatch")
420
+ return value
421
+ if value["state"] != "prepared":
422
+ return value
423
+ value.update(
424
+ state="owned",
425
+ route=route,
426
+ targetTurnRef=target_turn_ref,
427
+ managedTurnOrdinal=managed_turn_ordinal,
428
+ ownedAtMs=now_ms(),
429
+ lastRefusalCode=None,
430
+ lastRefusalMessage=None,
431
+ lastRefusalState=None,
432
+ lastRefusalAtMs=None,
433
+ )
434
+ return value
435
+
436
+ return _mutate(short, delivery_id, timeout=timeout, mutate=mutate)
437
+
438
+
439
+ def mark_attempt(
440
+ short: str,
441
+ delivery_id: str,
442
+ *,
443
+ maybe_written: bool = True,
444
+ timeout: float | None = None,
445
+ ) -> dict[str, Any]:
446
+ def mutate(value: dict[str, Any]) -> dict[str, Any]:
447
+ if value["state"] != "owned":
448
+ return value
449
+ value["attempts"] = int(value.get("attempts") or 0) + 1
450
+ value["maybeWritten"] = bool(maybe_written)
451
+ return value
452
+
453
+ return _mutate(short, delivery_id, timeout=timeout, mutate=mutate)
454
+
455
+
456
+ def mark_evidence(
457
+ short: str,
458
+ delivery_id: str,
459
+ *,
460
+ evidence: str | None = None,
461
+ evidence_uuid: str | None = None,
462
+ evidence_offset: int | None = None,
463
+ evidence_status: str | None = None,
464
+ maybe_written: bool | None = None,
465
+ timeout: float | None = None,
466
+ ) -> dict[str, Any]:
467
+ def mutate(value: dict[str, Any]) -> dict[str, Any]:
468
+ if value["state"] in TERMINAL_STATES:
469
+ return value
470
+ if evidence is not None:
471
+ value["evidence"] = evidence
472
+ if evidence_uuid is not None:
473
+ value["evidenceUuid"] = evidence_uuid
474
+ if evidence_offset is not None:
475
+ value["evidenceOffset"] = evidence_offset
476
+ if evidence_status is not None:
477
+ value["evidenceStatus"] = evidence_status
478
+ if maybe_written is not None:
479
+ value["maybeWritten"] = bool(maybe_written)
480
+ return value
481
+
482
+ return _mutate(short, delivery_id, timeout=timeout, mutate=mutate)
483
+
484
+
485
+ def mark_accepted(
486
+ short: str,
487
+ delivery_id: str,
488
+ *,
489
+ placement: str,
490
+ turn_ref: str,
491
+ managed_turn_ordinal: int | None = None,
492
+ evidence: str | None = None,
493
+ evidence_uuid: str | None = None,
494
+ evidence_offset: int | None = None,
495
+ timeout: float | None = None,
496
+ ) -> dict[str, Any]:
497
+ if placement not in PLACEMENTS:
498
+ raise ClaudeDeliveryError(f"invalid Claude delivery placement: {placement!r}")
499
+ if not isinstance(turn_ref, str) or not turn_ref:
500
+ raise ClaudeDeliveryError("accepted Claude delivery requires turn_ref")
501
+
502
+ def mutate(value: dict[str, Any]) -> dict[str, Any]:
503
+ if value["state"] == "accepted":
504
+ if value.get("placement") != placement or value.get("turnRef") != turn_ref:
505
+ raise ClaudeDeliveryConflict("accepted delivery placement/turn mismatch")
506
+ return value
507
+ if value["state"] != "owned":
508
+ raise ClaudeDeliveryError(
509
+ f"cannot accept Claude delivery from state={value['state']}"
510
+ )
511
+ value.update(
512
+ state="accepted",
513
+ placement=placement,
514
+ turnRef=turn_ref,
515
+ managedTurnOrdinal=managed_turn_ordinal,
516
+ acceptedAtMs=now_ms(),
517
+ maybeWritten=False,
518
+ evidenceStatus="accepted",
519
+ evidence=evidence or value.get("evidence"),
520
+ evidenceUuid=evidence_uuid or value.get("evidenceUuid"),
521
+ evidenceOffset=(
522
+ evidence_offset
523
+ if evidence_offset is not None
524
+ else value.get("evidenceOffset")
525
+ ),
526
+ promptText=None,
527
+ rejectionCode=None,
528
+ rejectionMessage=None,
529
+ )
530
+ return value
531
+
532
+ return _mutate(short, delivery_id, timeout=timeout, mutate=mutate)
533
+
534
+
535
+
536
+ def finalize_dead_steer(
537
+ target: str, delivery_id: str, *, fate: str,
538
+ evidence: dict | None,
539
+ ) -> dict[str, Any]:
540
+ """Terminalize a source-scanned steer or new-turn after confirmed child death.
541
+
542
+ No native replay. The new-turn delivered case requires exact ordinary-user
543
+ evidence; a queued steer event cannot prove a strict new-turn submission.
544
+ """
545
+ if fate not in {"delivered", "abandoned"}:
546
+ raise ClaudeDeliveryError("invalid dead-child steer outcome")
547
+ def mutate(row: dict[str, Any]) -> dict[str, Any]:
548
+ route = row.get("route")
549
+ if route not in {"steer", "new_turn"} or row.get("state") == "prepared":
550
+ raise ClaudeDeliveryError("dead-child finalizer requires owned delivery")
551
+ if route == "steer" and fate == "delivered" and (
552
+ not isinstance(evidence, dict)
553
+ or evidence.get("kind") not in {"enqueued", "started", "consumed"}
554
+ or type(evidence.get("offset")) is not int
555
+ ):
556
+ raise ClaudeDeliveryError("Steering cannot be verified; inspect its delivery")
557
+ if route == "new_turn" and fate == "delivered" and not isinstance(evidence, dict):
558
+ raise ClaudeDeliveryError("Message delivery is not confirmed; inspect the worker")
559
+ if row.get("state") in {"accepted", "rejected"}:
560
+ return row
561
+ if row.get("state") in {"delivered", "abandoned"}:
562
+ if row["state"] != fate:
563
+ raise ClaudeDeliveryConflict("Claude stopped but delivery records disagree; inspect the worker")
564
+ return row
565
+ if row.get("state") != "owned":
566
+ raise ClaudeDeliveryError("Steering status is invalid; inspect the delivery")
567
+ row.update(
568
+ state=fate, maybeWritten=False, promptText=None,
569
+ evidence=(evidence or {}).get("kind"),
570
+ evidenceUuid=(evidence or {}).get("uuid"),
571
+ evidenceOffset=(evidence or {}).get("offset"),
572
+ evidenceStatus=fate,
573
+ )
574
+ if route == "new_turn" and fate == "delivered":
575
+ row["placement"] = "new_turn"
576
+ return row
577
+
578
+ return _mutate(target, delivery_id, timeout=None, mutate=mutate)
579
+
580
+
581
+ def mark_settled(
582
+ short: str,
583
+ delivery_id: str,
584
+ *, turn_ref: str,
585
+ original_positive_completion: dict[str, Any],
586
+ timeout: float | None = None,
587
+ ) -> dict[str, Any]:
588
+ """Persist first exact original settled-turn evidence under receipt lock.
589
+
590
+ Does not repeat a native send, change original delivery ID, modify old
591
+ accepted timestamps or infer completion from a model wait timeout. It
592
+ records only a positive exact source/turn/status completion proof. A
593
+ subsequent same-ID response returns the identical settlement without
594
+ extra native effects. Original accepted/rejected state machine retained.
595
+ """
596
+ if not isinstance(turn_ref, str) or not turn_ref:
597
+ raise ClaudeDeliveryError("Completion requires a turn reference; inspect the delivery")
598
+ if not isinstance(original_positive_completion, dict):
599
+ raise ClaudeDeliveryError("Completion could not be verified; inspect the delivery")
600
+ if (original_positive_completion.get("turnRef") != turn_ref
601
+ or original_positive_completion.get("status") != "completed"
602
+ or not isinstance(original_positive_completion.get("text"), str)
603
+ or not isinstance(original_positive_completion.get("nativeEvidenceSha256"), str)
604
+ or len(original_positive_completion["nativeEvidenceSha256"]) != 64):
605
+ raise ClaudeDeliveryError("Claude turn is not confirmed complete; inspect its status")
606
+ from ccx.store.root import _HASH
607
+ if not _HASH.fullmatch(original_positive_completion["nativeEvidenceSha256"]):
608
+ raise ClaudeDeliveryError("Claude completion record is invalid; inspect the worker")
609
+
610
+ def mutate(value: dict[str, Any]) -> dict[str, Any]:
611
+ if (value.get("state") != "accepted"
612
+ or value.get("turnRef") != turn_ref):
613
+ raise ClaudeDeliveryError("Turn is not accepted or belongs to another delivery; inspect its ID")
614
+ old = value.get("settlement")
615
+ if old is not None:
616
+ if old != original_positive_completion:
617
+ raise ClaudeDeliveryConflict("original positive turn settlement is immutable")
618
+ return value
619
+ value["settlement"] = dict(original_positive_completion)
620
+ value["settledAtMs"] = now_ms()
621
+ return value
622
+
623
+ return _mutate(short, delivery_id, timeout=timeout, mutate=mutate)
624
+
625
+
626
+ def mark_rejected(
627
+ short: str,
628
+ delivery_id: str,
629
+ *,
630
+ code: str,
631
+ message: str,
632
+ timeout: float | None = None,
633
+ ) -> dict[str, Any]:
634
+ def mutate(value: dict[str, Any]) -> dict[str, Any]:
635
+ if value["state"] == "rejected":
636
+ return value
637
+ if value["state"] == "accepted":
638
+ raise ClaudeDeliveryError("accepted Claude delivery cannot be rejected")
639
+ value.update(
640
+ state="rejected",
641
+ rejectedAtMs=now_ms(),
642
+ rejectionCode=code,
643
+ rejectionMessage=message,
644
+ promptText=None,
645
+ maybeWritten=False,
646
+ )
647
+ return value
648
+
649
+ return _mutate(short, delivery_id, timeout=timeout, mutate=mutate)
650
+
651
+
652
+ def iter_receipts(short: str):
653
+ """Original best-effort status inventory, strict exact owner for each row."""
654
+ from ccx.store.root import _read_json, StorageIntegrityError
655
+ store = _store()
656
+ owner = _owner(store, short)
657
+ with store.lease():
658
+ try:
659
+ with store._gen("semantic-deliveries", "claude", owner.worker_id) as fd:
660
+ with os.scandir(fd) as entries:
661
+ names = sorted(e.name for e in entries if e.name.endswith(".json"))
662
+ if len(names) > 100000:
663
+ raise ClaudeDeliveryError("semantic receipt inventory exceeds budget")
664
+ rows = []
665
+ for name in names:
666
+ try:
667
+ rows.append(_read_json(fd, name))
668
+ except StorageIntegrityError:
669
+ pass # Preserve corrupt bytes when pruning cannot safely inspect them.
670
+ except FileNotFoundError:
671
+ rows = []
672
+ for value in rows:
673
+ identity = value.get("deliveryId") if isinstance(value, dict) else None
674
+ if not valid_delivery_id(identity):
675
+ continue
676
+ try:
677
+ yield _validate(value, short=short, delivery_id=identity)
678
+ except ClaudeDeliveryError:
679
+ continue
680
+
681
+
682
+ def prune_receipts(
683
+ short: str, *, keep_ids=(), retention_s: float = RECEIPT_RETENTION_S,
684
+ lock_timeout: float = 0.0,
685
+ ) -> int:
686
+ """Prune terminal JSON only; stable per-ID lock files must never unlink."""
687
+ from ccx.store.root import _read_json
688
+ store = _store()
689
+ owner = _owner(store, short)
690
+ keep = set(keep_ids)
691
+ cutoff_ms = now_ms() - int(retention_s * 1000)
692
+ total = 0
693
+ for value in iter_receipts(short):
694
+ delivery_id = value["deliveryId"]
695
+ if delivery_id in keep or not _prunable(value, cutoff_ms):
696
+ continue
697
+ try:
698
+ with receipt_lock(short, delivery_id, timeout=lock_timeout):
699
+ current = read_receipt(short, delivery_id)
700
+ if current is None or not _prunable(current, cutoff_ms):
701
+ continue
702
+ with store.lease():
703
+ with store._gen("semantic-deliveries", "claude",
704
+ owner.worker_id) as fd:
705
+ os.unlink(_key(delivery_id) + ".json", dir_fd=fd)
706
+ os.fsync(fd)
707
+ total += 1
708
+ except (ClaudeDeliveryBusy, FileNotFoundError):
709
+ continue
710
+ return total
711
+
712
+
713
+ def _prunable(value: dict[str, Any], cutoff_ms: int) -> bool:
714
+ updated = value.get("updatedAtMs")
715
+ return (
716
+ value.get("state") in TERMINAL_STATES
717
+ and isinstance(updated, int)
718
+ and updated < cutoff_ms
719
+ )
720
+
721
+
722
+ def public_receipt(value: dict[str, Any] | None) -> dict[str, Any] | None:
723
+ if value is None:
724
+ return None
725
+ state = value.get("state")
726
+ if state in {"accepted", "delivered"}:
727
+ status = "accepted"
728
+ action = "none"
729
+ elif state == "abandoned":
730
+ # Public placement remains unknown, not a native refusal. Old-child
731
+ # death resolved its ownership: nothing remains pending/reconcilable.
732
+ status = "unknown"
733
+ action = "none"
734
+ elif state == "rejected":
735
+ status = "rejected"
736
+ action = "none"
737
+ elif state == "prepared":
738
+ status = "pending"
739
+ action = "retry_same_id"
740
+ else:
741
+ status = "pending"
742
+ action = "wait"
743
+ return {
744
+ "delivery_id": value.get("deliveryId"),
745
+ "status": status,
746
+ "caller_metadata": value.get("callerMetadata"),
747
+ "owned": state in {"owned", "accepted", "delivered", "abandoned"},
748
+ "recommended_action": action,
749
+ "route": value.get("route"),
750
+ "placement": value.get("placement"),
751
+ "target_turn_ref": value.get("targetTurnRef"),
752
+ "turn_ref": value.get("turnRef"),
753
+ "managed_turn_ordinal": value.get("managedTurnOrdinal"),
754
+ "created_at_ms": value.get("createdAtMs"),
755
+ "updated_at_ms": value.get("updatedAtMs"),
756
+ "owned_at_ms": value.get("ownedAtMs"),
757
+ "accepted_at_ms": value.get("acceptedAtMs"),
758
+ "settled_at_ms": value.get("settledAtMs"),
759
+ "settlement": value.get("settlement"),
760
+ "rejected_at_ms": value.get("rejectedAtMs"),
761
+ "attempts": int(value.get("attempts") or 0),
762
+ "maybe_written": bool(value.get("maybeWritten")),
763
+ "evidence": value.get("evidence"),
764
+ "evidence_uuid": value.get("evidenceUuid"),
765
+ "evidence_offset": value.get("evidenceOffset"),
766
+ "evidence_status": value.get("evidenceStatus"),
767
+ "rejection_code": value.get("rejectionCode"),
768
+ "rejection_message": value.get("rejectionMessage"),
769
+ "last_refusal": (
770
+ {
771
+ "code": value.get("lastRefusalCode"),
772
+ "message": value.get("lastRefusalMessage"),
773
+ "state": value.get("lastRefusalState"),
774
+ "at_ms": value.get("lastRefusalAtMs"),
775
+ }
776
+ if value.get("lastRefusalCode") or value.get("lastRefusalMessage")
777
+ else None
778
+ ),
779
+ }
780
+
781
+
782
+ __all__ = [
783
+ "CLIENT_LOCK_TIMEOUT_S",
784
+ "HOST_LOCK_TIMEOUT_S",
785
+ "ClaudeDeliveryBusy",
786
+ "ClaudeDeliveryConflict",
787
+ "ClaudeDeliveryError",
788
+ "MODES",
789
+ "PLACEMENTS",
790
+ "ROUTES",
791
+ "TERMINAL_STATES",
792
+ "iter_receipts",
793
+ "mark_accepted",
794
+ "mark_attempt",
795
+ "mark_evidence",
796
+ "mark_owned",
797
+ "mark_rejected",
798
+ "mark_retryable_refusal",
799
+ "prepare",
800
+ "prune_receipts",
801
+ "public_receipt",
802
+ "read_receipt",
803
+ "receipt_lock",
804
+ "receipt_path",
805
+ "valid_delivery_id",
806
+ ]