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,762 @@
1
+ from __future__ import annotations
2
+ import time
3
+ from typing import Any
4
+ from ccx.ops import turn_reports
5
+ from ccx.notify.registry import public_registration as public_parent_notification, read_registration as read_parent_notification
6
+ from ccx.errors import (
7
+ CapabilityUnsupported,
8
+ InvalidRequest,
9
+ SessionNotFound,
10
+ SessionUnavailable,
11
+ )
12
+ from ccx.rpc.rpc import (
13
+ _rpc as claude_host_rpc,
14
+ )
15
+ from ccx.ops.common import split_target
16
+
17
+
18
+ def park_worker(
19
+ target: str, *, timeout: float | None = 90,
20
+ expected_generation: str | None = None,
21
+ _respawn_lock_held: bool = False,
22
+ ) -> dict[str, Any]:
23
+ """Confirmed teardown of one exact full-UUID runtime, native history kept."""
24
+ from ccx import constants
25
+ from ccx.store.root import require_ccx_active
26
+ from ccx.ops.terminate import confirmed_kill_uuid
27
+ handle, _engine, _worker_id = split_target(target)
28
+ store = require_ccx_active(constants.CCX_ROOT,
29
+ expected_generation=expected_generation)
30
+ return confirmed_kill_uuid(
31
+ store, handle, timeout=timeout,
32
+ _respawn_lock_held=_respawn_lock_held,
33
+ )
34
+
35
+
36
+ def inspect_notification(event_id: str) -> dict[str, Any]:
37
+ """Read the original notification's rich protocol projection, without delivery."""
38
+ from ccx.notify.client import _event_id
39
+ from ccx.notify.paths import registration_path
40
+
41
+ event_id = _event_id(event_id)
42
+ value = read_parent_notification(event_id)
43
+ if value is None:
44
+ # The existing reader returns None for both missing and damaged JSON.
45
+ # One path check distinguishes the two without another parse/registry.
46
+ if registration_path(event_id).exists():
47
+ raise SessionUnavailable(f"notification registration is unreadable or corrupt: {event_id}")
48
+ raise SessionNotFound(f"no such notification registration: {event_id}")
49
+ if value.get("eventId") != event_id:
50
+ raise SessionUnavailable(f"notification registration event ID differs from requested ID: {event_id}")
51
+ public = public_parent_notification(value)
52
+ operation = (
53
+ value.get("operation")
54
+ if isinstance(value.get("operation"), dict)
55
+ else {}
56
+ )
57
+ return {
58
+ **public,
59
+ "source": value.get("source"),
60
+ "attempts": int(value.get("attempts") or 0),
61
+ "last_attempt_at_ms": value.get("lastAttemptAtMs"),
62
+ "last_error": value.get("lastError"),
63
+ "delivered_at_ms": value.get("deliveredAtMs"),
64
+ "cancel_reason": value.get("cancelReason"),
65
+ "caller_metadata": operation.get("callerMetadata"),
66
+ "settlement": (
67
+ value.get("settlement")
68
+ if isinstance(value.get("settlement"), dict)
69
+ else None
70
+ ),
71
+ "payload": (
72
+ value.get("payload")
73
+ if isinstance(value.get("payload"), dict)
74
+ else None
75
+ ),
76
+ "delivery": (
77
+ value.get("delivery")
78
+ if isinstance(value.get("delivery"), dict)
79
+ else None
80
+ ),
81
+ "operation": {
82
+ "kind": operation.get("kind"),
83
+ "delivery_id": operation.get("deliveryId"),
84
+ "turn_id": operation.get("turnId"),
85
+ "resume_waiting_request_id": operation.get(
86
+ "resumeWaitingRequestId"
87
+ ),
88
+ "caller_metadata": operation.get("callerMetadata"),
89
+ },
90
+ }
91
+
92
+
93
+ def wait_worker(
94
+ target: str, *, timeout: float | None = None,
95
+ next_turn: bool = False, after_turn: str | None = None,
96
+ known_input: str | None = None, expected_generation: str | None = None,
97
+ ) -> dict[str, Any]:
98
+ """Observe an exact full-UUID worker, never starting or replaying a turn.
99
+
100
+ A stable-state or new-turn wait returns verdict data; the separate
101
+ wait_delivery observes one original semantic receipt and raises on timeout.
102
+ """
103
+ from math import isfinite
104
+ from ccx.codex import runtime as codex_runtime
105
+ from ccx import constants
106
+ from ccx.codex.read import pending_input as codex_pending
107
+ from ccx.constants import CODEXD_RPC_GENERATION
108
+ from ccx.store.liveness import owned_process
109
+ from ccx.store.root import GenerationError, require_ccx_active
110
+ from ccx.workers import _claude_pending
111
+
112
+ if (timeout is not None and (
113
+ type(timeout) not in (int, float) or not isfinite(timeout) or timeout < 0
114
+ )):
115
+ raise InvalidRequest("wait timeout must be finite and nonnegative")
116
+ if after_turn is not None and not next_turn:
117
+ raise InvalidRequest("after_turn requires next_turn=true")
118
+ if known_input is not None:
119
+ if not next_turn or not isinstance(known_input, str) or not known_input:
120
+ raise InvalidRequest("known_input requires next_turn and a nonempty request id")
121
+ handle, engine, worker_id = split_target(target)
122
+ if engine == "codex" and next_turn:
123
+ selector = ("wait_known_input" if known_input is not None else
124
+ "wait_after_turn" if after_turn is not None else "wait_next_turn")
125
+ raise CapabilityUnsupported("Codex next-turn wait is not supported",
126
+ engine=engine, capability=selector)
127
+ if after_turn is not None:
128
+ turn_reports._ref_id(after_turn, engine="claude")
129
+ try:
130
+ store = require_ccx_active(constants.CCX_ROOT, expected_generation=expected_generation)
131
+ store.resolve(handle)
132
+ except GenerationError as exc:
133
+ raise SessionUnavailable(f"Cannot access worker records to wait; check CCX_ROOT: {exc}") from exc
134
+
135
+ deadline = None if timeout is None else time.monotonic() + timeout
136
+ terminal = {"dead", "crashed", "idle-killed", "budget-stopped"}
137
+ host_missing_since = None
138
+
139
+ def remaining() -> float:
140
+ return float("inf") if deadline is None else max(0.0, deadline - time.monotonic())
141
+
142
+ def claude_record_settlement(record: dict) -> dict | None:
143
+ """One validated source-owned Claude checkpoint, or genuinely unknown."""
144
+ last = record.get("lastSettledTurn")
145
+ if not isinstance(last, dict):
146
+ return None
147
+ ref, stamped = last.get("turnRef"), last.get("settledAtMs")
148
+ floor = record.get("lastSettledAt")
149
+ if (isinstance(ref, str) and ref.startswith("ccx-turn:claude:v1:")
150
+ and len(ref) > len("ccx-turn:claude:v1:")
151
+ and type(stamped) is int
152
+ and (type(floor) is not int or stamped >= floor)):
153
+ return {
154
+ "turn_ref": ref, "turn_id": None,
155
+ "outcome": last.get("outcome") or "unknown",
156
+ "completed_at": stamped,
157
+ "context": ({**last["context"], "as_of_turn_ref": ref}
158
+ if isinstance(last.get("context"), dict) else None),
159
+ }
160
+ return None
161
+
162
+ def codex_record_settlement(record: dict) -> dict | None:
163
+ """Only a terminal native Codex turn is an exact checkpoint."""
164
+ last = record.get("lastTurn")
165
+ if (isinstance(last, dict)
166
+ and isinstance(last.get("turnId"), str) and last.get("turnId")
167
+ and last.get("status") in {"completed", "failed", "interrupted"}):
168
+ return turn_reports.codex_settled_turn(
169
+ last, fallback_completed_at=record.get("lastTurnAt"))
170
+ return None
171
+
172
+ def snapshot() -> dict[str, Any]:
173
+ store.revalidate()
174
+ rec = store.record(handle)
175
+ lc = rec.get("lifecycle") or {}
176
+ state = lc.get("state")
177
+ waiting = None
178
+ input_block = None
179
+ active = None
180
+ served = False
181
+ unavailable = False
182
+ pending = False
183
+ last_turn = None
184
+ last_settled = None
185
+
186
+ # Each probe fits the transport limit; the overall deadline is unchanged.
187
+ budget = 5.0 if deadline is None else min(600.0, max(0.01, remaining()))
188
+ if engine == "claude":
189
+ last_settled = claude_record_settlement(rec)
190
+ if state not in terminal:
191
+ try:
192
+ reply = claude_host_rpc(handle, {"op": "status"}, timeout=budget)
193
+ except (OSError, ValueError, TimeoutError):
194
+ reply = {}
195
+ if reply.get("ok") is True:
196
+ served = True
197
+ state = reply.get("state") or state
198
+ active = reply.get("activeTurnRef")
199
+ waiting = reply.get("waitingFor") if state == "waiting" else None
200
+ pending = bool(reply.get("pendingDeliveries"))
201
+ # Status may cross the original turn's durable CAS. A host
202
+ # that now certifies stable IDLE must not pair that new
203
+ # state with a stale pre-RPC null settlement snapshot.
204
+ if (state == "idle" and active is None and not pending
205
+ and last_settled is None):
206
+ store.revalidate()
207
+ last_settled = claude_record_settlement(store.record(handle))
208
+ if state == "waiting" and served:
209
+ waiting = waiting or rec.get("waitingFor")
210
+ input_block = _claude_pending(waiting)
211
+ if not served and state not in terminal:
212
+ unavailable = owned_process(
213
+ lc.get("hostPid"), lc.get("hostStart")) == "dead"
214
+ else:
215
+ last_turn = rec.get("lastTurn")
216
+ if state not in terminal:
217
+ try:
218
+ reply = codex_runtime.daemon_rpc({
219
+ "op": "status", "short": worker_id,
220
+ "rpcGeneration": CODEXD_RPC_GENERATION,
221
+ }, timeout=budget)
222
+ except (OSError, ValueError, TimeoutError):
223
+ reply = {}
224
+ served = (reply.get("ok") is True and reply.get("loaded") is True
225
+ and reply.get("transport") == "served"
226
+ and codex_runtime.daemon_generation(reply) == CODEXD_RPC_GENERATION)
227
+ if served:
228
+ state = reply.get("state") or state
229
+ active_id = reply.get("activeTurnId")
230
+ if isinstance(active_id, str) and active_id:
231
+ active = "ccx-turn:codex:v1:" + active_id
232
+ last_turn = reply.get("lastTurn") or last_turn
233
+ waiting = reply.get("pendingApprovals") if state == "waiting" else None
234
+ else:
235
+ unavailable = (reply.get("transport") == "absent"
236
+ or (reply.get("transport") == "served"
237
+ and reply.get("loaded") is False))
238
+ if state == "waiting" and served:
239
+ input_block = codex_pending(waiting or rec.get("pendingApprovals"))
240
+ last_settled = codex_record_settlement({
241
+ "lastTurn": last_turn, "lastTurnAt": rec.get("lastTurnAt")})
242
+ if (served and state == "idle" and active is None
243
+ and not reply.get("pendingApprovals")
244
+ and last_settled is None):
245
+ # The original codexd status normally includes lastTurn, but
246
+ # it can be a pre-commit snapshot while a separate record CAS
247
+ # completes. Refresh the same generation once, never infer a
248
+ # turn from an idle state or make another daemon RPC.
249
+ store.revalidate()
250
+ fresh = store.record(handle)
251
+ last_settled = codex_record_settlement(fresh)
252
+ if last_settled is not None:
253
+ last_turn = fresh.get("lastTurn")
254
+
255
+ # The recorded turn error is useful even when its host is gone.
256
+ # Prefer the exact terminal turn to a stale previous API error.
257
+ sources = (
258
+ (last_turn or {}).get("error") if engine == "codex" else None,
259
+ rec.get("activeTurnError") if engine == "codex" else None,
260
+ (lc.get("apiError") or {}).get("message") if engine == "claude" else None,
261
+ )
262
+ def error_text(value):
263
+ if isinstance(value, str):
264
+ return value or None
265
+ if isinstance(value, dict):
266
+ return value.get("message") or value.get("text") or value.get("error")
267
+ return None
268
+ failure = next((text for source in sources
269
+ if (text := error_text(source))), None)
270
+ result = {
271
+ "worker": handle, "worker_id": worker_id, "engine": engine,
272
+ "short": worker_id[:8], "mode": "managed", "state": state,
273
+ "native_state": state, "status": state,
274
+ "reason": None, "waiting_for": waiting if state == "waiting" and served else None,
275
+ "pending_input": input_block,
276
+ "wait_kind": (input_block or {}).get("kind"),
277
+ "last_settled_turn": last_settled, "active_turn_ref": active,
278
+ "apiError": lc.get("apiError") if engine == "claude" else None,
279
+ "error": failure if state in terminal else None,
280
+ "lastTurn": last_turn if engine == "codex" else None,
281
+ "pendingApprovals": waiting if engine == "codex" and served else None,
282
+ "timed_out": False,
283
+ "_served": served, "_unavailable": unavailable, "_pending": pending,
284
+ }
285
+ if engine == "codex" and state == "idle" and active is None:
286
+ stop = rec.get("quotaStop") or {}
287
+ if (isinstance(last_turn, dict)
288
+ and last_turn.get("turnId") == stop.get("turnId")
289
+ and last_turn.get("status") in {"interrupted", "cancelled", "failed"}
290
+ and stop.get("turnId")):
291
+ result["status"] = "budget-stopped"
292
+ result["reason"] = stop.get("reason") or "usage cap reached"
293
+ store.revalidate()
294
+ return result
295
+
296
+ def finish(value: dict[str, Any], status: str, *, observed: bool = False,
297
+ reason: str | None = None) -> dict[str, Any]:
298
+ out = {key: item for key, item in value.items() if not key.startswith("_")}
299
+ out.update(status=status, timed_out=status == "timeout")
300
+ if reason is not None:
301
+ out["reason"] = reason
302
+ if next_turn:
303
+ out.update(wait_mode="next-turn", next_turn_observed=observed,
304
+ next_turn_event=None)
305
+ store.revalidate()
306
+ return out
307
+
308
+ entry = snapshot()
309
+ entry_ref = (entry.get("last_settled_turn") or {}).get("turn_ref")
310
+ entry_gate = ((entry.get("waiting_for") or {}).get("id")
311
+ if entry["_served"] and engine == "claude" else None)
312
+ entry_waiting = entry["_served"] and entry["state"] == "waiting"
313
+ value = entry
314
+ while True:
315
+ current = value["status"]
316
+ is_terminal = current in terminal
317
+ latest = (value.get("last_settled_turn") or {}).get("turn_ref")
318
+ changed = (latest is not None and
319
+ latest != (after_turn if after_turn is not None else entry_ref))
320
+ gate = value.get("waiting_for") if value["_served"] else None
321
+ gate_id = gate.get("id") if isinstance(gate, dict) else None
322
+ waiting_now = value["_served"] and current == "waiting"
323
+
324
+ # A durable boundary precedes a later crash, without certifying that
325
+ # the model produced a completed assistant message.
326
+ if next_turn and changed:
327
+ return finish(value, current, observed=True)
328
+ if next_turn and known_input is not None:
329
+ if value["_served"] and (not waiting_now or gate_id != known_input):
330
+ return finish(value, current, observed=True)
331
+ if after_turn is not None and latest is None and not is_terminal:
332
+ raise SessionUnavailable(
333
+ "latest exact settlement is unknown; cannot compare after_turn")
334
+ elif next_turn and after_turn is not None:
335
+ if waiting_now:
336
+ return finish(value, "waiting", observed=True)
337
+ if latest is None and not is_terminal:
338
+ raise SessionUnavailable("latest exact settlement is unknown; cannot compare after_turn")
339
+ elif next_turn and waiting_now:
340
+ if not entry_waiting or (gate_id is not None and gate_id != entry_gate):
341
+ return finish(value, "waiting", observed=True)
342
+
343
+ if is_terminal:
344
+ return finish(value, current)
345
+ if value["_unavailable"]:
346
+ if engine == "codex":
347
+ activation = store.native_activation(handle)
348
+ unloaded = (current in {"prepared", "idle", "idle-killed"}
349
+ and activation.get("state") == "confirmed")
350
+ return finish(value, "stopped" if unloaded else "unavailable",
351
+ reason="Codex runtime is absent; no worker was resumed")
352
+ if host_missing_since is None:
353
+ host_missing_since = time.monotonic()
354
+ grace = 5.0 if current in {"booting", "resuming"} else 0.35
355
+ if time.monotonic() - host_missing_since >= grace:
356
+ return finish(value, "host-dead",
357
+ reason="owned Claude host is no longer alive")
358
+ else:
359
+ host_missing_since = None
360
+ if not next_turn and value["_served"]:
361
+ if waiting_now:
362
+ return finish(value, "waiting")
363
+ if (current == "idle" and not value["_pending"]
364
+ and value["active_turn_ref"] is None):
365
+ return finish(value, "idle", reason="worker is idle")
366
+
367
+ # The first observation wins when already stable, even at timeout=0.
368
+ if deadline is not None and time.monotonic() >= deadline:
369
+ return finish(value, "timeout", reason="wait deadline elapsed")
370
+ delay = min(0.2, remaining())
371
+ if delay > 0:
372
+ time.sleep(delay)
373
+ value = snapshot()
374
+
375
+
376
+ def reconcile_turn(
377
+ target: str, *, delivery_id: str | None = None,
378
+ turn_id: str | None = None, timeout: float | None = None,
379
+ include_worker: bool = True,
380
+ expected_generation: str | None = None,
381
+ ) -> dict[str, Any]:
382
+ """Original full-ID delivery receipt reconciliation.
383
+
384
+ Never infer acceptance from a native transcript prefix or retry unknown
385
+ effects; the existing semantic receipt and pinned generation are the
386
+ only authority until a real host receipt adapter is connected. Its one
387
+ write: a Codex receipt left submitted by a lost acknowledgment is accepted
388
+ from the exact journaled native user message carrying its own client id
389
+ (``codex_delivery_recovery``); anything less leaves it unchanged.
390
+ """
391
+ from ccx.claude import delivery as claude_delivery
392
+ from ccx import constants
393
+ from ccx.store.root import require_ccx_active
394
+ handle, engine, worker_id = split_target(target)
395
+ deadline = time.monotonic() + (timeout if timeout is not None else 30.0)
396
+ if expected_generation is not None:
397
+ require_ccx_active(constants.CCX_ROOT, expected_generation=expected_generation)
398
+ if engine == "codex":
399
+ from ccx.codex import delivery as codex_delivery
400
+ if (delivery_id is None) == (turn_id is None):
401
+ raise InvalidRequest("Codex reconcile needs exactly delivery_id or turn_id")
402
+ from ccx.codex.delivery_recovery import recover, recover_for_turn
403
+ from ccx.store.root import GenerationBusy
404
+ recovery_store = require_ccx_active(constants.CCX_ROOT)
405
+ if delivery_id is not None:
406
+ value = codex_delivery.read_receipt(handle, delivery_id)
407
+ if isinstance(value, dict) and value.get("state") in ("submitted", "accepted"):
408
+ # A lost acknowledgment is recovered only from the exact
409
+ # journaled native user message; otherwise unchanged.
410
+ try:
411
+ value = recover(recovery_store, handle, delivery_id) or value
412
+ except GenerationBusy:
413
+ pass
414
+ else:
415
+ value = codex_delivery.receipt_for_turn(handle, turn_id)
416
+ if value is None:
417
+ # Never call a turn foreign while a submitted delivery of ours
418
+ # may have opened it: recover from exact evidence first.
419
+ try:
420
+ recover_for_turn(recovery_store, handle, turn_id)
421
+ except GenerationBusy:
422
+ pass
423
+ value = codex_delivery.receipt_for_turn(handle, turn_id)
424
+ info = {"worker": handle, "worker_id": worker_id, "engine": "codex"}
425
+ if value is None:
426
+ return {
427
+ "worker": info if include_worker else None,
428
+ "status": "not_found", "pending": False, "owned": False,
429
+ "recommended_action": "none", "delivery": None,
430
+ }
431
+ native_observation = None
432
+ if (value.get("state") == "accepted"
433
+ and value.get("settlement") is None
434
+ and isinstance(value.get("turnId"), str)
435
+ and codex_delivery.route(value) != "steer"):
436
+ # Original turn/completed events survived in the sparse evidence
437
+ # journal, even when the native rollout is too large to hydrate.
438
+ # This also repairs completions that preceded acceptance or a
439
+ # daemon restart, without requiring the old codexd to run new code.
440
+ from ccx.codex.completion import settle_from_journal
441
+ store = require_ccx_active(constants.CCX_ROOT)
442
+ completed = settle_from_journal(store, handle, value["turnId"])
443
+ if completed is not None:
444
+ value = completed
445
+ else:
446
+ # A concurrent native completion may have settled this same
447
+ # accepted receipt while the journal was being observed.
448
+ value = codex_delivery.read_receipt(handle, value["deliveryId"])
449
+ if (value.get("state") == "accepted"
450
+ and value.get("settlement") is None
451
+ and isinstance(value.get("turnId"), str)
452
+ and codex_delivery.route(value) != "steer"):
453
+ # Read native provider-owned thread/read through the actual
454
+ # authenticated singleton codexd. A missing daemon, pending
455
+ # turn or lost observation never causes a second turn/start.
456
+ from ccx.codex.runtime import daemon_rpc
457
+ from ccx.constants import CODEXD_RPC_GENERATION
458
+ from ccx.store.store import _digest
459
+ budget = max(0.001, deadline - time.monotonic())
460
+ observed = daemon_rpc({
461
+ "op": "native-observe",
462
+ "workerId": worker_id,
463
+ "deliveryId": value["deliveryId"],
464
+ "turnId": value["turnId"],
465
+ "observeSeconds": budget,
466
+ "rpcGeneration": CODEXD_RPC_GENERATION,
467
+ }, timeout=budget)
468
+ if (observed.get("ok") is True
469
+ and observed.get("status") in ("failed", "interrupted", "cancelled")
470
+ and observed.get("workerId") == worker_id
471
+ and observed.get("turnId") == value["turnId"]
472
+ and observed.get("deliveryId") == value["deliveryId"]):
473
+ native_observation = observed
474
+ if (observed.get("ok") is True
475
+ and observed.get("status") == "completed"):
476
+ native = store.record(handle)["native"]
477
+ source = store.codex_source(handle)
478
+ if (observed.get("workerId") != worker_id
479
+ or observed.get("engine") != "codex"
480
+ or observed.get("nativeSource") != source
481
+ or observed.get("threadId") != native.get("threadId")
482
+ or native.get("threadSource") != source
483
+ or observed.get("turnId") != value["turnId"]
484
+ or observed.get("deliveryId") != value["deliveryId"]
485
+ or observed.get("nativeEvidenceSha256") != _digest({
486
+ "source": source, "threadId": native["threadId"],
487
+ "turnId": value["turnId"], "status": "completed",
488
+ "text": observed.get("text"),
489
+ })):
490
+ raise SessionUnavailable(
491
+ "Codex completion belongs to another turn; inspect the delivery"
492
+ )
493
+ value = codex_delivery.mark_settled(
494
+ handle, value["deliveryId"],
495
+ turn_id=value["turnId"],
496
+ original_positive_completion=observed,
497
+ )
498
+ result = codex_delivery.public_receipt(value)
499
+ status = result["status"]
500
+ if value.get("state") == "prepared":
501
+ # Auto/steer stay prepared until codexd routes them, but their
502
+ # global intent is fsynced first: once it advanced, the request
503
+ # may still land, so this is submission uncertainty, never retry.
504
+ effect = require_ccx_active(constants.CCX_ROOT).read_receipt(
505
+ handle, "codex-send", value["deliveryId"],
506
+ )
507
+ if isinstance(effect, dict) and effect.get("state") != "prepared":
508
+ status = "submitted"
509
+ result = {**result, "pending": True, "recommended_action": "wait"}
510
+ steered = codex_delivery.route(value) == "steer"
511
+ complete = isinstance(value.get("settlement"), dict) and not steered
512
+ return {
513
+ "worker": info if include_worker else None,
514
+ "status": "settled" if complete else status,
515
+ "pending": result["pending"] or (
516
+ value["state"] == "accepted" and not complete and not steered),
517
+ "owned": result["owned"],
518
+ "recommended_action": result["recommended_action"],
519
+ "route": result["route"], "placement": result["placement"],
520
+ "turn_ref": result["turn_ref"],
521
+ "caller_metadata": result["caller_metadata"],
522
+ "delivery": result,
523
+ **({"native_status": native_observation["status"],
524
+ "native_error": native_observation.get("error")}
525
+ if native_observation else {}),
526
+ }
527
+ if turn_id is not None:
528
+ raise CapabilityUnsupported(
529
+ "Claude reconcile does not support turn_id",
530
+ engine="claude", capability="reconcile_turn_id",
531
+ )
532
+ if delivery_id is None or not claude_delivery.valid_delivery_id(delivery_id):
533
+ raise InvalidRequest("Claude reconcile requires one valid delivery_id")
534
+ store = require_ccx_active(constants.CCX_ROOT,
535
+ expected_generation=expected_generation)
536
+ value = claude_delivery.read_receipt(handle, delivery_id)
537
+ recorded = {"worker": handle, "worker_id": worker_id, "engine": "claude"}
538
+ if value is None:
539
+ return {
540
+ "worker": recorded if include_worker else None,
541
+ "status": "not_found", "pending": False, "owned": False,
542
+ "recommended_action": "none", "delivery": None,
543
+ }
544
+ if (value.get("placement") == "current_turn"
545
+ and isinstance(value.get("settlement"), dict)):
546
+ from ccx.store.root import NativeEffectIndeterminate
547
+ raise NativeEffectIndeterminate(
548
+ "a steer joined the target turn; it cannot borrow its completion"
549
+ )
550
+ # Crash-window repair: the real host can fsync its original accepted
551
+ # completion before it fsyncs the companion native-effect terminal.
552
+ # Reconcile using the same full owner, immutable original prompt spec
553
+ # and native source evidence; never dispatch or invent a new turn.
554
+ if value.get("state") == "accepted" and isinstance(value.get("settlement"), dict):
555
+ from ccx.store.store import OperationIntent, _digest
556
+ from ccx.store.root import NativeEffectIndeterminate, _HASH
557
+ native = store.record(handle).get("native") or {}
558
+ proof = value["settlement"]
559
+ effect = store.read_receipt(handle, "claude-delivery", delivery_id)
560
+ original_spec = claude_delivery.spec_digest(value)
561
+ if (not isinstance(effect, dict)
562
+ or effect.get("intentSha256") != original_spec
563
+ or effect.get("action") != "send"
564
+ or effect.get("workerId") != worker_id
565
+ or effect.get("engine") != "claude"
566
+ or effect.get("generation") != store.generation
567
+ or proof.get("workerId") != worker_id
568
+ or proof.get("deliveryId") != delivery_id
569
+ or proof.get("nativeSource")
570
+ != f"claude-session:{native.get('sessionId')}"
571
+ # Recovery from an UNKNOWN native effect requires the
572
+ # original provider sessionId explicitly in Stop evidence.
573
+ # Historical/direct positive callbacks already settled the
574
+ # native effect before their independent completion callback;
575
+ # they may contain only the exact nativeSource, not a separate
576
+ # sessionId field. Never weaken submitted-unknown recovery.
577
+ or (effect.get("state") == "submitted-unknown"
578
+ and proof.get("sessionId") != native.get("sessionId"))
579
+ or proof.get("turnRef") != value["turnRef"]
580
+ or proof.get("status") != "completed"
581
+ or not isinstance(proof.get("nativeEvidenceSha256"), str)
582
+ or not _HASH.fullmatch(proof["nativeEvidenceSha256"])):
583
+ raise NativeEffectIndeterminate(
584
+ "Completed turn has no matching delivery record; reconcile it"
585
+ "native source/effect proof; no false settlement"
586
+ )
587
+ if effect.get("state") == "submitted-unknown":
588
+ store.settle_native_intent(
589
+ OperationIntent(
590
+ worker_id, "claude", "claude-delivery", delivery_id,
591
+ original_spec, "submitted-unknown", store.generation,
592
+ ),
593
+ state="accepted",
594
+ native_evidence_sha256=proof["nativeEvidenceSha256"],
595
+ )
596
+ elif effect.get("state") != "accepted":
597
+ raise NativeEffectIndeterminate(
598
+ "native-effect state contradicts positively settled original turn"
599
+ )
600
+ projected = claude_delivery.public_receipt(value)
601
+ complete = isinstance(value.get("settlement"), dict)
602
+ status = "settled" if complete else projected["status"]
603
+ return {
604
+ "worker": recorded if include_worker else None,
605
+ "status": status,
606
+ "pending": value["state"] in ("prepared", "owned")
607
+ or value["state"] == "accepted" and not complete,
608
+ "owned": projected["owned"],
609
+ "recommended_action": projected["recommended_action"],
610
+ "route": projected["route"],
611
+ "placement": projected["placement"],
612
+ "turn_ref": projected["turn_ref"],
613
+ "caller_metadata": projected["caller_metadata"],
614
+ "delivery": projected,
615
+ }
616
+
617
+
618
+ def wait_delivery(
619
+ target: str, *, delivery_id: str, timeout: float | None = None,
620
+ expected_generation: str | None = None,
621
+ ) -> dict[str, Any]:
622
+ """Wait for one delivery, raising on failure or required input."""
623
+ from ccx.ops.report import report_worker
624
+ from ccx import constants
625
+ from ccx.errors import (DeliveryRejected, SessionWaiting, TurnIncomplete,
626
+ SessionUnavailable, BudgetStopped)
627
+ from ccx.store.liveness import owned_process
628
+ from ccx.store.root import require_ccx_active
629
+
630
+ handle, engine, worker_id = split_target(target)
631
+ store = require_ccx_active(constants.CCX_ROOT,
632
+ expected_generation=expected_generation)
633
+ deadline = None if timeout is None else time.monotonic() + timeout
634
+ last_status, last_ref = None, None
635
+
636
+ def fail(exc):
637
+ # The high-level creator attaches the original full UUID; retain the
638
+ # latest *observed* semantic identity on its typed cause as well.
639
+ exc.delivery_id = delivery_id
640
+ exc.delivery_status = last_status
641
+ exc.turn_ref = last_ref
642
+ raise exc
643
+
644
+ while True:
645
+ store.revalidate()
646
+ observation = None
647
+ try:
648
+ observation = reconcile_turn(
649
+ handle, delivery_id=delivery_id,
650
+ expected_generation=store.generation,
651
+ timeout=(None if deadline is None else
652
+ max(0.001, deadline - time.monotonic())),
653
+ )
654
+ except (SessionUnavailable, OSError):
655
+ # A transiently unreachable native daemon cannot prove a new
656
+ # effect or a terminal outcome. Keep the original receipt pending.
657
+ pass
658
+ store.revalidate()
659
+ if observation is not None:
660
+ last_status = observation.get("status")
661
+ receipt = observation.get("delivery") or {}
662
+ last_ref = receipt.get("turn_ref") or last_ref
663
+ if last_status == "settled":
664
+ if (receipt.get("delivery_id") != delivery_id
665
+ or not isinstance(last_ref, str) or not last_ref
666
+ or not isinstance(receipt.get("settlement"), dict)):
667
+ fail(TurnIncomplete("turn has no confirmed completed reply",
668
+ short=worker_id[:8]))
669
+ report = report_worker(handle, turn_ref=last_ref, max_chars=None)
670
+ store.revalidate()
671
+ if (report.get("worker") != handle
672
+ or report.get("delivery_id") != delivery_id
673
+ or report.get("turn_ref") != last_ref
674
+ or report.get("status") != "completed"):
675
+ fail(TurnIncomplete("turn reply does not match its delivery receipt",
676
+ short=worker_id[:8]))
677
+ return {"delivery": receipt, "delivery_status": "settled",
678
+ "turn_ref": last_ref, "report": report}
679
+ if last_status == "rejected":
680
+ fail(DeliveryRejected(
681
+ receipt.get("rejection_message") or "turn's prompt was rejected",
682
+ short=worker_id[:8], refusal=receipt.get("rejection_code"),
683
+ delivery_id=delivery_id, delivery_status="rejected",
684
+ ))
685
+ if (engine == "codex" and observation.get("native_status")
686
+ in ("failed", "interrupted", "cancelled")):
687
+ marker = store.record(handle).get("quotaStop") or {}
688
+ if marker.get("turnId") == receipt.get("turn_id"):
689
+ reason = marker.get("reason") or "usage cap reached"
690
+ fail(BudgetStopped(f"budget stop: {reason}",
691
+ short=worker_id[:8], reason=reason, phase="turn"))
692
+ fail(TurnIncomplete(
693
+ "Codex turn ended " + observation["native_status"],
694
+ short=worker_id[:8], turn_id=receipt.get("turn_id"),
695
+ turn_status=observation["native_status"],
696
+ turn_error=observation.get("native_error"),
697
+ ))
698
+ if last_status in ("not_found", "prepared"):
699
+ fail(TurnIncomplete(
700
+ "Message submission is not confirmed; "
701
+ f"reconcile delivery_id={delivery_id!r}, or retry with that "
702
+ "same delivery ID and message",
703
+ short=worker_id[:8],
704
+ ))
705
+ # A missing Stop can leave a Claude owned receipt forever. The
706
+ # original worker's own gate/death is conclusive only while this first
707
+ # delivery remains unsettled (the exact receipt check runs first).
708
+ record = store.record(handle)
709
+ lifecycle = record.get("lifecycle") or {}
710
+ state = lifecycle.get("state")
711
+ if engine == "claude":
712
+ settled = record.get("lastSettledTurn") or {}
713
+ outcome = settled.get("outcome")
714
+ if last_ref and settled.get("turnRef") == last_ref and outcome in {
715
+ "interrupted", "failed", "cancelled"}:
716
+ fail(TurnIncomplete("Claude turn ended " + outcome,
717
+ short=worker_id[:8], turn_status=outcome))
718
+ pending = record.get("waitingFor")
719
+ if pending or state == "waiting":
720
+ fail(SessionWaiting(
721
+ "Claude turn needs input; use ccx answer or approve", short=worker_id[:8],
722
+ waiting_for=pending,
723
+ ))
724
+ if state == "budget-stopped":
725
+ reason = lifecycle.get("lastError") or "usage cap reached"
726
+ fail(BudgetStopped(
727
+ f"budget stop: {reason}", short=worker_id[:8],
728
+ reason=reason, phase="turn",
729
+ ))
730
+ if state in {"dead", "crashed", "idle-killed", "killed", "stopped", "budget-stopped"}:
731
+ fail(TurnIncomplete("Claude turn cannot complete: " + state,
732
+ short=worker_id[:8], state=state))
733
+ if (lifecycle.get("hostPid") and owned_process(
734
+ lifecycle.get("hostPid"), lifecycle.get("hostStart")) == "dead"):
735
+ fail(TurnIncomplete("Claude stopped before completing the turn",
736
+ short=worker_id[:8], state=state))
737
+ elif engine == "codex":
738
+ # This stored original native turn is another source of positive
739
+ # terminal evidence when codexd becomes temporarily unavailable.
740
+ receipt_id = (observation or {}).get("delivery") or {}
741
+ if not receipt_id:
742
+ from ccx.codex.delivery import public_receipt, read_receipt
743
+ receipt_id = public_receipt(read_receipt(handle, delivery_id)) or {}
744
+ native_turn = receipt_id.get("turn_id")
745
+ last = record.get("lastTurn") or {}
746
+ if (native_turn and last.get("turnId") == native_turn
747
+ and last.get("status") in {"failed", "interrupted", "cancelled"}):
748
+ marker = record.get("quotaStop") or {}
749
+ if marker.get("turnId") == native_turn:
750
+ reason = marker.get("reason") or "usage cap reached"
751
+ fail(BudgetStopped(f"budget stop: {reason}",
752
+ short=worker_id[:8], reason=reason, phase="turn"))
753
+ fail(TurnIncomplete(
754
+ "Codex turn ended " + last["status"],
755
+ short=worker_id[:8], turn_id=native_turn,
756
+ turn_status=last["status"], turn_error=last.get("error"),
757
+ ))
758
+ if deadline is not None and time.monotonic() >= deadline:
759
+ fail(TurnIncomplete("turn did not finish before timeout",
760
+ short=worker_id[:8], state=state))
761
+ time.sleep(.2 if deadline is None else
762
+ min(.2, max(0., deadline - time.monotonic())))