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.
- package/LICENSE +21 -0
- package/README.md +240 -4
- package/bin/ccx +55 -2
- package/ccx/__init__.py +167 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +807 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +269 -0
- package/ccx/claude/host/session.py +5244 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2535 -0
- package/ccx/claude/usage.py +796 -0
- package/ccx/claude/usage_control.py +204 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/models.py +53 -0
- package/ccx/cli/commands/read.py +1104 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +302 -0
- package/ccx/cli/parser.py +1439 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1904 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +738 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1558 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2424 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/history_store.py +131 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1454 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/models.py +225 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +296 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1593 -0
- package/ccx/ops/conversation_paging.py +1339 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +575 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/history.py +1041 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +773 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +348 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +640 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +250 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +789 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/private_json.py +54 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +809 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2093 -0
- package/ccx.py +9 -0
- package/package.json +39 -7
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +166 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/history.md +50 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- 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
|
+
]
|