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,952 @@
1
+ """Identity-first creation: one durable claim and one semantic state machine.
2
+
3
+ Provider-specific hooks below do only native record preparation and activation.
4
+ No display-name search, reservation or compatibility create path lives here.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import json
10
+ import os
11
+ import time
12
+ import uuid
13
+ from typing import Any
14
+
15
+ from ccx import constants
16
+ from ccx.constants import DEFAULT_IDLE_TIMEOUT_S
17
+ from ccx.errors import (
18
+ BudgetStopped, CreatePartialFailure, InvalidRequest, OperationConflict,
19
+ SessionUnavailable, SessionWaiting, TurnIncomplete,
20
+ )
21
+ from ccx.controllers import live_vars
22
+ from ccx.store.root import CreationConflict
23
+ from ccx.store.identity import OwnerConflict, IdentityError
24
+
25
+ def new_worker_id() -> str:
26
+ """Mint one complete, canonical, caller-persistable UUIDv4 without IO."""
27
+ return str(uuid.uuid4())
28
+
29
+
30
+ def _bind_intent(attachment: live_vars.PreparedAttachment | None) -> dict:
31
+ if attachment is None:
32
+ return {"mode": "none"}
33
+ if attachment.mode == "live":
34
+ return {
35
+ "mode": "live", "names": sorted(attachment.names),
36
+ "owner": attachment.owner.public_identity() if attachment.owner else None,
37
+ }
38
+ if attachment.state_id is not None:
39
+ return {"mode": "durable-shared", "stateId": attachment.state_id}
40
+ return {"mode": "durable", "values": attachment.values or {}}
41
+
42
+
43
+ def _bind_once(store, engine: str, worker_id: str,
44
+ attachment: live_vars.PreparedAttachment | None) -> list[str]:
45
+ """Commit the first variable intent after its worker record, before native effects.
46
+
47
+ This receipt is independent of the immutable creation fingerprint. A
48
+ matching create retry must never republish mutable durable or live values.
49
+ """
50
+ from ccx.store.root import _read_json, _write_json
51
+
52
+ intent = _bind_intent(attachment)
53
+ digest = hashlib.sha256(json.dumps(
54
+ intent, sort_keys=True, separators=(",", ":"), allow_nan=False,
55
+ ).encode("utf-8")).hexdigest()
56
+ with store.lease():
57
+ with store._claim_lock(worker_id):
58
+ _owner, record = store._owned(f"{engine}:{worker_id}")
59
+ claim = store._claim(worker_id)
60
+ if (claim is None or claim.get("workerId") != worker_id
61
+ or claim.get("engine") != engine
62
+ or claim.get("generation") != store.generation
63
+ or claim.get("createSha256") != record.get("createSha256")):
64
+ raise SessionUnavailable("The worker could not be created; inspect its saved worker ID",
65
+ short=worker_id[:8])
66
+ with store._gen("variables", "bindings", engine, create=True) as directory:
67
+ prior = _read_json(directory, worker_id + ".json")
68
+ if prior is not None:
69
+ if prior.get("schemaVersion") != 1 or prior.get("sha256") != digest:
70
+ raise OperationConflict(
71
+ "initial variables intent differs from first binding",
72
+ short=worker_id[:8], reason="initial_variables_mismatch",
73
+ worker_id=worker_id,
74
+ )
75
+ names = prior.get("names")
76
+ if (not isinstance(names, list) or any(not isinstance(n, str)
77
+ for n in names)):
78
+ raise SessionUnavailable("initial variable receipt has invalid names",
79
+ short=worker_id[:8])
80
+ return list(names)
81
+ if live_vars.read_sidecar(engine, worker_id) is not None:
82
+ # The first sidecar may have landed before its receipt, or
83
+ # a later SEND may own it. Never guess/reapply first values.
84
+ raise SessionUnavailable(
85
+ f"first variable binding for {engine}:{worker_id} lacks commit proof",
86
+ short=worker_id[:8],
87
+ )
88
+ if attachment is not None:
89
+ activation = claim.get("nativeActivation")
90
+ if (not isinstance(activation, dict)
91
+ or activation.get("state") not in {"unprepared", "prepared"}):
92
+ raise SessionUnavailable(
93
+ "Worker creation has already been submitted; "
94
+ "first variables cannot be attached retroactively",
95
+ short=worker_id[:8],
96
+ )
97
+ live_vars.publish_attachment(attachment, engine, worker_id)
98
+ names = sorted(attachment.names) if attachment else []
99
+ _write_json(directory, worker_id + ".json", {
100
+ "schemaVersion": 1, "sha256": digest, "names": names,
101
+ })
102
+ return names
103
+
104
+
105
+ def _require_codex_fork_ready(store, original, record: dict) -> dict:
106
+ """Offline original fork proof; never require a live daemon/held reply."""
107
+ anchor = original.record.get("forkAnchor")
108
+ if anchor is None:
109
+ return record
110
+ import hashlib
111
+ from ccx.store.root import NativeEffectIndeterminate, _read_json, _flock_at, _HASH
112
+ from ccx.store.store import _digest, fingerprint_v2
113
+
114
+ wid = original.worker_id
115
+ native = record.get("native") or {}
116
+ lineage = record.get("forkedFrom")
117
+ from ccx.store.store import codex_fork_lineage
118
+ expected_lineage = codex_fork_lineage(anchor, child_id=original.worker_id)
119
+ activation = store.native_activation(original.worker)
120
+ bound = activation.get("nativeBinding") or {}
121
+ thread_id = bound.get("threadId")
122
+ if (not isinstance(anchor, dict) or anchor.get("engine") != "codex"
123
+ or record.get("forkAnchor") != anchor
124
+ or not isinstance(lineage, dict)
125
+ or lineage != expected_lineage
126
+ or not isinstance(anchor.get("createArgs"), dict)
127
+ or anchor.get("cwd") != record.get("cwd")
128
+ or fingerprint_v2(engine="codex", cwd=anchor["cwd"],
129
+ create_args=anchor["createArgs"],
130
+ fork_anchor=anchor) != record.get("createSha256")
131
+ or activation.get("state") != "confirmed"
132
+ or activation.get("source") != original.native_source
133
+ or not isinstance(activation.get("submissionId"), str)
134
+ or not activation["submissionId"]
135
+ or not isinstance(thread_id, str) or not thread_id
136
+ or thread_id == anchor.get("sourceNativeThreadId")
137
+ or native.get("threadId") != thread_id
138
+ or native.get("threadSource") != original.native_source
139
+ or native.get("nativeState") != "thread-bound"):
140
+ raise NativeEffectIndeterminate(
141
+ f"Codex fork {wid} lacks original child/anchor/activation proof; never re-fork"
142
+ )
143
+ token = activation["submissionId"]
144
+ key = hashlib.sha256(("activate\x00" + token).encode()).hexdigest()
145
+ with store.lease():
146
+ owner, claim = store._owner(wid), store._claim(wid)
147
+ if not isinstance(owner, dict) or not isinstance(claim, dict):
148
+ raise NativeEffectIndeterminate("Codex fork immutable original owner/claim absent")
149
+ store._check_original_create_intent(
150
+ owner, engine="codex", worker_id=wid,
151
+ fingerprint=record["createSha256"], native={}, anchor=anchor,
152
+ )
153
+ if (claim.get("forkAnchor") != anchor
154
+ or claim.get("createSha256") != record["createSha256"]
155
+ or claim.get("fingerprintVersion") != 2
156
+ or claim.get("nativeBinding") != {}
157
+ or claim.get("nativeActivation") != activation):
158
+ raise NativeEffectIndeterminate("Codex fork original immutable claim changed")
159
+ try:
160
+ with store._gen("runtime-metadata", "codex-native-effects", wid) as directory:
161
+ with _flock_at(directory, key + ".lock", exclusive=False,
162
+ create=False, timeout=.5):
163
+ effect = _read_json(directory, key + ".json")
164
+ except FileNotFoundError as exc:
165
+ raise NativeEffectIndeterminate("Codex fork original effect marker absent") from exc
166
+ candidate = effect.get("nativeCandidate") if isinstance(effect, dict) else None
167
+ if (not isinstance(effect, dict) or not isinstance(candidate, dict)
168
+ or any(effect.get(k) != v for k, v in {
169
+ "workerId": wid, "engine": "codex", "generation": store.generation,
170
+ "kind": "activate", "token": token, "state": "submitted-unknown",
171
+ "source": original.native_source,
172
+ "createSha256": record["createSha256"],
173
+ "nativeMethod": "thread/fork", "forkAnchor": anchor,
174
+ }.items())
175
+ or effect.get("nativeRejected") is not None
176
+ or effect.get("nativeNotSubmitted") is not None
177
+ or not isinstance(effect.get("daemonInstanceId"), str)
178
+ or not effect["daemonInstanceId"]
179
+ or type(effect.get("daemonPid")) is not int or effect["daemonPid"] <= 0
180
+ or type(effect.get("appServerPid")) is not int or effect["appServerPid"] <= 0
181
+ or candidate.get("threadId") != thread_id
182
+ or candidate.get("submittedSource") != original.native_source
183
+ or candidate.get("appServerPid") != effect["appServerPid"]
184
+ or not isinstance(candidate.get("nativeReplySha256"), str)
185
+ or not _HASH.fullmatch(candidate["nativeReplySha256"])):
186
+ raise NativeEffectIndeterminate("Codex fork records disagree; inspect the worker before retrying")
187
+ # All inputs are stable original marker/claim facts. A successful provider
188
+ # read was mandatory before the claim could receive this precise digest;
189
+ # no current PID, held-slot, native probe or mutable child setting is used.
190
+ evidence = {
191
+ "workerId": wid, "threadSource": original.native_source,
192
+ "threadId": thread_id, "submissionId": token,
193
+ "daemonPid": effect["daemonPid"],
194
+ "appServerPid": effect["appServerPid"],
195
+ "nativeMethod": "thread/fork", "forkAnchor": anchor,
196
+ "forkedFromId": anchor["sourceNativeThreadId"],
197
+ "lastTurnId": anchor["lastTurnId"], "lastTurnStatus": "completed",
198
+ "nativeReplySha256": candidate["nativeReplySha256"],
199
+ }
200
+ if (activation.get("nativeEvidenceSha256") != _digest(evidence)
201
+ or not _HASH.fullmatch(activation["nativeEvidenceSha256"])):
202
+ raise NativeEffectIndeterminate("Codex fork records changed; inspect the worker before retrying")
203
+ return record
204
+
205
+
206
+ def _complete_codex_create(store, original, info: dict, *, start_timeout, deadline):
207
+ """Complete only the one original Codex start; submitted fate is observational."""
208
+ from ccx.codex.runtime import daemon_rpc, ensure_daemon
209
+ from ccx.system.engine_binary import resolve_engine_binary
210
+ from ccx.constants import CODEXD_RPC_GENERATION
211
+ from ccx.ops.creation_steps import dispatch_prepared_codex_native, reconcile_submitted_native
212
+ from ccx.store.root import CreationIndeterminate, NativeEffectIndeterminate
213
+
214
+ def terminal_proof():
215
+ # Durable provider errors and pre-IPC refusals are different fates.
216
+ rejected = store.reconcile_codex_native_rejection(original.worker)
217
+ if rejected is not None:
218
+ info["native_rejection"] = {
219
+ "code": rejected["code"], "message": rejected["message"],
220
+ }
221
+ info["activation_state"] = "rejected"
222
+ raise NativeEffectIndeterminate("original Codex provider rejected " + (
223
+ "thread/fork" if original.record.get("forkAnchor") else "thread/start"))
224
+ local = store.reconcile_codex_local_refusal(original.worker)
225
+ if local is not None:
226
+ info["native_local_refusal"] = {
227
+ "reason": local["reason"], "stage": local["stage"], "proven": True,
228
+ }
229
+ info["activation_state"] = "local-refused"
230
+ raise NativeEffectIndeterminate("Codex refused to start; create a new worker ID to retry")
231
+
232
+ def remaining():
233
+ return float('inf') if deadline is None else deadline - time.monotonic()
234
+
235
+ terminal_proof() # sealed terminal claims must replay without a daemon/binary
236
+ activation = store.native_activation(original.worker)
237
+ if activation["state"] == "confirmed":
238
+ return _require_codex_fork_ready(
239
+ store, original, store.bind_confirmed_native(original.worker))
240
+ try:
241
+ resolve_engine_binary("codex")
242
+ except SessionUnavailable as error:
243
+ raise NativeEffectIndeterminate(
244
+ "Codex executable not found; set CCX_CODEX_BIN or PATH and retry"
245
+ ) from error
246
+ if remaining() <= 0:
247
+ raise NativeEffectIndeterminate("Codex startup timed out; inspect this worker instead of starting it twice")
248
+ # The shared app-server may need longer than four seconds to initialize on
249
+ # a cold, loaded machine. Both concurrent first users must wait for the
250
+ # *same* codexd within their caller-selected native start observation
251
+ # budget, rather than expiring an unrelated fixed four-second boot timer.
252
+ ensure_daemon(deadline=min(time.monotonic() + start_timeout,
253
+ deadline if deadline is not None else float('inf')))
254
+
255
+ def submit_original(budget):
256
+ admitted_until = min(time.monotonic() + budget,
257
+ deadline if deadline is not None else float('inf'))
258
+ if time.monotonic() >= admitted_until:
259
+ return {"ok": False, "error": "Codex startup timed out; check worker status"}
260
+ claim = store.native_activation(original.worker)
261
+ answer = daemon_rpc({
262
+ "op": "native-activate", "workerId": original.worker_id,
263
+ "submissionId": claim["submissionId"],
264
+ "nativeAdmissionDeadline": admitted_until,
265
+ "rpcGeneration": CODEXD_RPC_GENERATION,
266
+ }, timeout=max(0., admitted_until - time.monotonic()))
267
+ if (answer.get("transport") == "served"
268
+ and isinstance(answer.get("localRefusalReason"), str)):
269
+ info["native_local_refusal"] = {
270
+ "reason": answer["localRefusalReason"], "proven": False,
271
+ }
272
+ return answer
273
+
274
+ if activation["state"] != "submitted-unknown":
275
+ try:
276
+ budget = min(2., remaining())
277
+ if budget <= 0:
278
+ raise NativeEffectIndeterminate("Codex startup timed out; check worker status; no replay")
279
+ dispatch_prepared_codex_native(
280
+ store, original, bounded_native_submit=submit_original,
281
+ dispatch_budget_s=budget,
282
+ )
283
+ except CreationIndeterminate as contention:
284
+ if str(contention) not in {
285
+ "native activation already submitted or claimed by another caller",
286
+ "native activation already submitted/settled/ambiguous",
287
+ }:
288
+ raise
289
+ advanced = store.reserve_creation(
290
+ original.worker_id, engine="codex",
291
+ fingerprint=original.record["createSha256"], native_binding={},
292
+ fork_anchor=original.record.get("forkAnchor"),
293
+ )
294
+ claim = store.native_activation(original.worker)
295
+ if (advanced.activation_state not in {
296
+ "submitted-unknown", "confirmed", "rejected", "local-refused"
297
+ } or advanced.submission_id != claim.get("submissionId")
298
+ or claim.get("source") != original.native_source
299
+ or not isinstance(advanced.submission_id, str)
300
+ or not advanced.submission_id):
301
+ raise contention
302
+
303
+ observe_until = min(time.monotonic() + start_timeout,
304
+ deadline if deadline is not None else float('inf'))
305
+ while True:
306
+ claim = store.native_activation(original.worker)
307
+ if claim["state"] == "confirmed":
308
+ return _require_codex_fork_ready(
309
+ store, original, store.bind_confirmed_native(original.worker))
310
+ if claim["state"] in {"rejected", "local-refused"}:
311
+ terminal_proof()
312
+ remaining_observation = observe_until - time.monotonic()
313
+ if remaining_observation <= 0:
314
+ raise NativeEffectIndeterminate(
315
+ "original Codex " + (
316
+ "thread/fork" if original.record.get("forkAnchor") else "thread/start")
317
+ + " is still submitted-unknown; "
318
+ "retain original full worker UUID; never replay"
319
+ )
320
+ observed = daemon_rpc({
321
+ "op": "native-activation-status", "workerId": original.worker_id,
322
+ "submissionId": claim["submissionId"],
323
+ "rpcGeneration": CODEXD_RPC_GENERATION,
324
+ }, timeout=min(3., remaining_observation))
325
+ if observed.get("localRefused") or observed.get("rejected"):
326
+ terminal_proof()
327
+ if observed.get("accepted") is True:
328
+ if time.monotonic() >= observe_until:
329
+ raise NativeEffectIndeterminate("Codex startup timed out; check the worker before retrying")
330
+ return _require_codex_fork_ready(
331
+ store, original, reconcile_submitted_native(
332
+ store, original, original_receipt=observed,
333
+ ).record)
334
+ if observed.get("ok") is not True:
335
+ # A peer could have confirmed during status; never re-submit.
336
+ state = store.native_activation(original.worker)["state"]
337
+ if state == "confirmed":
338
+ return _require_codex_fork_ready(
339
+ store, original, store.bind_confirmed_native(original.worker))
340
+ if state in {"rejected", "local-refused"}:
341
+ terminal_proof()
342
+ raise NativeEffectIndeterminate(
343
+ "Codex did not confirm starting; check the worker: "
344
+ + str(observed.get("error") or "native evidence unavailable")
345
+ )
346
+ if time.monotonic() >= observe_until:
347
+ raise NativeEffectIndeterminate(
348
+ "original Codex " + (
349
+ "thread/fork" if original.record.get("forkAnchor") else "thread/start")
350
+ + " is still submitted-unknown; "
351
+ "retain original full worker UUID; never replay"
352
+ )
353
+ time.sleep(min(.1, max(0., observe_until - time.monotonic())))
354
+
355
+
356
+ def _require_claude_fork_ready(store, original, record: dict) -> dict:
357
+ """A local transcript copy and a genuine original SessionStart are separate.
358
+
359
+ The original claim is only confirmed from the authenticated host proof;
360
+ retain its durable proof without requiring a still-live host on ID retry.
361
+ """
362
+ if original.record.get("forkAnchor") is None:
363
+ return record
364
+ from ccx.ops.forking import _claude_copy_proof
365
+ from ccx.store.root import NativeEffectIndeterminate
366
+ from ccx.store.store import _digest
367
+ activation = store.native_activation(original.worker)
368
+ start = (record.get("lifecycle") or {}).get("nativeStartProof")
369
+ native_sid = (record.get("native") or {}).get("sessionId")
370
+ source = f"claude-session:{native_sid}"
371
+ if (record.get("forkAnchor") != original.record.get("forkAnchor")
372
+ or _claude_copy_proof(record) is None
373
+ or activation.get("state") != "confirmed"
374
+ or activation.get("source") != source
375
+ or (activation.get("nativeBinding") or {}).get("sessionId") != native_sid
376
+ or not isinstance(start, dict)
377
+ or start.get("workerId") != original.worker_id
378
+ or start.get("engine") != "claude"
379
+ or start.get("generation") != store.generation
380
+ or start.get("nativeSource") != source
381
+ or start.get("sessionId") != native_sid
382
+ or start.get("submissionId") != activation.get("submissionId")
383
+ or start.get("nativeEvidenceSha256") != _digest({
384
+ key: value for key, value in start.items()
385
+ if key != "nativeEvidenceSha256"
386
+ })):
387
+ raise NativeEffectIndeterminate(
388
+ f"Claude fork {original.worker_id} lacks exact copy and native SessionStart proofs; "
389
+ "retain original worker UUID, never recopy or restart"
390
+ )
391
+ return record
392
+
393
+
394
+ def _complete_claude_create(store, original, *, start_timeout, deadline):
395
+ """Admit only the original Claude host, then verify its exact SessionStart."""
396
+ from ccx.claude.spawn import submit_original_claude_start, verified_original_claude_start
397
+ from ccx.ops.creation_steps import activate_prepared_worker, reconcile_submitted_native
398
+ from ccx.store.root import CreationIndeterminate, NativeEffectIndeterminate
399
+
400
+ activation = store.native_activation(original.worker)
401
+ if activation["state"] == "confirmed":
402
+ return _require_claude_fork_ready(
403
+ store, original, store.bind_confirmed_native(original.worker))
404
+ if activation["state"] not in {"unprepared", "prepared", "submitted-unknown"}:
405
+ raise NativeEffectIndeterminate("Claude is not ready to start; check worker status")
406
+ if activation["state"] != "submitted-unknown":
407
+ if original.record.get("origin") == "ccx-import":
408
+ from ccx.claude.external import exact_import_candidate, import_eligibility
409
+ from ccx.store.paths import transcript_path_for
410
+ sid = original.record["native"]["sessionId"]
411
+ source = exact_import_candidate(sid)
412
+ ok, _code, why = import_eligibility(source)
413
+ if (not ok or source["cwd"] != original.record["cwd"]
414
+ or source["transcript"] != original.record["transcript"]
415
+ or source["transcript"] != str(transcript_path_for(source["cwd"], sid))):
416
+ raise SessionUnavailable("import source changed before first managed resume: "
417
+ + (why if not ok else "original transcript/cwd differs"))
418
+ from ccx.system.engine_binary import resolve_engine_binary
419
+ try:
420
+ resolve_engine_binary("claude")
421
+ except SessionUnavailable as error:
422
+ raise NativeEffectIndeterminate(
423
+ "Claude executable not found; set CCX_CLAUDE_BIN or PATH and retry"
424
+ ) from error
425
+ rem = float('inf') if deadline is None else deadline - time.monotonic()
426
+ # A real host proof requires a bounded local status handshake. Do not
427
+ # deliberately enter a >.15s launcher on a .15s protocol deadline.
428
+ if rem < .3:
429
+ raise NativeEffectIndeterminate("Claude could not start before timeout; retry with more time")
430
+ from ccx.claude.settings import ensure_trusted_paths
431
+ from ccx.errors import SpawnFailed
432
+ try:
433
+ ensure_trusted_paths([
434
+ original.record["cwd"],
435
+ *((original.record.get("createArgs") or {}).get("add_dir") or []),
436
+ ])
437
+ except OSError as exc:
438
+ raise SpawnFailed(f"Claude trust settings could not be written: {exc}", short=original.worker_id[:8],
439
+ phase="settings-io", detail=str(exc)) from exc
440
+ try:
441
+ accepted = activate_prepared_worker(
442
+ store, original,
443
+ bounded_native_submit=lambda budget: submit_original_claude_start(
444
+ store, original, budget, deadline=deadline),
445
+ dispatch_budget_s=min(2., rem),
446
+ )
447
+ except NativeEffectIndeterminate:
448
+ # Original submitted-unknown may already have produced an
449
+ # authentic host SessionStart after its short admission budget.
450
+ # Observe that same submission below, never dispatch again.
451
+ if store.native_activation(original.worker)["state"] != "submitted-unknown":
452
+ raise
453
+ except CreationIndeterminate as contention:
454
+ if str(contention) not in {
455
+ "native activation already submitted or claimed by another caller",
456
+ "native activation already submitted/settled/ambiguous",
457
+ }:
458
+ raise
459
+ binding = {"sessionId": original.record["native"]["sessionId"]}
460
+ advanced = store.reserve_creation(
461
+ original.worker_id, engine="claude",
462
+ fingerprint=original.record["createSha256"], native_binding=binding,
463
+ fork_anchor=original.record.get("forkAnchor"),
464
+ provenance=("ccx-import" if original.record.get("origin") == "ccx-import"
465
+ else "ccx-new"),
466
+ imported_from=original.record.get("importedFrom"),
467
+ )
468
+ claim = store.native_activation(original.worker)
469
+ if (advanced.activation_state not in {"submitted-unknown", "confirmed"}
470
+ or advanced.submission_id != claim.get("submissionId")
471
+ or claim.get("source") != original.native_source
472
+ or not isinstance(advanced.submission_id, str)
473
+ or not advanced.submission_id):
474
+ raise contention
475
+ else:
476
+ if accepted.activation_state == "confirmed":
477
+ if deadline is not None and time.monotonic() >= deadline:
478
+ raise NativeEffectIndeterminate(
479
+ "original Claude SessionStart confirmed after request deadline; no replay"
480
+ )
481
+ return _require_claude_fork_ready(store, original, accepted.record)
482
+
483
+ observe_until = min(time.monotonic() + start_timeout,
484
+ deadline if deadline is not None else float('inf'))
485
+ while True:
486
+ claim = store.native_activation(original.worker)
487
+ if claim["state"] == "confirmed":
488
+ return _require_claude_fork_ready(
489
+ store, original, store.bind_confirmed_native(original.worker))
490
+ remaining = observe_until - time.monotonic()
491
+ if remaining <= 0:
492
+ raise NativeEffectIndeterminate("Claude startup is not confirmed; check worker status before retrying")
493
+ proof = verified_original_claude_start(store, original, deadline=observe_until)
494
+ if proof:
495
+ if time.monotonic() >= observe_until:
496
+ raise NativeEffectIndeterminate("Claude startup timed out; inspect worker status before retrying")
497
+ bound = reconcile_submitted_native(
498
+ store, original, original_receipt=proof,
499
+ ).record
500
+ return _require_claude_fork_ready(store, original, bound)
501
+ time.sleep(min(.025, max(0., observe_until - time.monotonic())))
502
+
503
+
504
+ def _finalize_empty_codex_create(ctx, result: dict, states=("prepared",)) -> None:
505
+ """Apply the promptless Codex empty-history proof to a bound, turnless worker.
506
+
507
+ The native admission path may already have loaded the thread into idle
508
+ before refusing its first turn. Both states carry the same original
509
+ thread/start proof; neither is evidence that a turn was delivered.
510
+ """
511
+ latest = ctx.record(result["worker"])
512
+ native = latest.get("native") or {}
513
+ lc = latest.get("lifecycle") or {}
514
+ state = lc.get("state")
515
+ if (ctx.native_activation(result["worker"])["state"] == "confirmed"
516
+ and native.get("nativeState") == "thread-bound"
517
+ and native.get("threadSource") == ctx.codex_source(result["worker"])
518
+ and state in states
519
+ and not lc.get("activeTurnId")
520
+ and not latest.get("pendingSend")
521
+ and not latest.get("activeCompact")
522
+ and not latest.get("nativeTurnStart")):
523
+ inherited = latest.get("forkAnchor") is not None
524
+ turns = (latest.get("stats") or {}).get("turns")
525
+ empty = (not inherited and turns == 0 and not latest.get("lastTurn"))
526
+ from ccx.store.root import RecordConflict
527
+ patches = {"historyKnownEmpty": empty}
528
+ if state == "prepared":
529
+ patches["lifecycle.state"] = "idle"
530
+ try:
531
+ latest = ctx.mutate_record(
532
+ result["worker"], patches=patches,
533
+ expect={"lifecycle.state": state,
534
+ "lifecycle.activeTurnId": None},
535
+ )
536
+ except RecordConflict:
537
+ # A concurrent original activation may already be idle.
538
+ latest = ctx.record(result["worker"])
539
+ if (latest.get("lifecycle") or {}).get("state") != "idle":
540
+ raise
541
+ result["record"] = latest
542
+
543
+
544
+ def create(*, prompt: str | None = None,
545
+ caps: dict | None = None, wait: bool = False,
546
+ delivery_id: str | None = None,
547
+ parent: dict | None = None, settle: bool = False,
548
+ start_timeout: float | None = None,
549
+ expected_generation: str | None = None,
550
+ deadline: float | None = None,
551
+ **options: Any) -> dict:
552
+ """One proven full-UUID native create for CLI, Python and local protocol."""
553
+ from math import isfinite
554
+ from ccx.store.root import require_ccx_active, GenerationError, NativeEffectIndeterminate
555
+ from ccx.ops.creation_steps import prepare_worker, validated_create_spec
556
+ from ccx.store.identity import InvalidTarget, canonical_worker_id, mint_worker_id
557
+
558
+ if prompt is not None and (not isinstance(prompt, str) or not prompt.strip()):
559
+ raise InvalidRequest("initial prompt must be nonblank text; no identity claimed")
560
+ cap = caps
561
+ if cap is not None:
562
+ from ccx.workers import _check_caps
563
+ _check_caps(cap, engine=options.get("engine"))
564
+ if not prompt:
565
+ raise InvalidRequest("initial cap requires a prompt; no identity claimed")
566
+ if delivery_id is not None and (prompt is None or delivery_id != "create:initial"):
567
+ raise InvalidRequest(
568
+ "initial delivery_id must be create:initial with a prompt; no identity claimed"
569
+ )
570
+ if wait and prompt is None:
571
+ raise InvalidRequest("initial-turn wait needs a prompt; no identity claimed")
572
+ imported_sid = options.get("import_session_id")
573
+ imported_from = options.get("imported_from")
574
+ if (imported_sid is not None or imported_from is not None) and (
575
+ options.get("engine") != "claude" or prompt is not None
576
+ or options.get("created_via") != "import"
577
+ or not isinstance(imported_sid, str)
578
+ or not isinstance(imported_from, dict)):
579
+ raise InvalidRequest("Import requires a Claude session without a pending prompt")
580
+ turn_timeout = options.get("timeout")
581
+ if turn_timeout is not None and (
582
+ not wait or type(turn_timeout) not in (int, float)
583
+ or not isfinite(turn_timeout) or turn_timeout <= 0):
584
+ raise InvalidRequest(
585
+ "initial-turn timeout requires wait and positive finite seconds; "
586
+ "no identity claimed"
587
+ )
588
+ if options.get("engine") == "claude" and settle:
589
+ raise InvalidRequest(
590
+ "recursive settlement currently requires a Codex target; no identity claimed"
591
+ )
592
+ if settle and parent is None:
593
+ raise InvalidRequest("--settle requires --notify-parent; no identity claimed")
594
+ if parent is not None and (wait or not isinstance(parent, dict)):
595
+ raise InvalidRequest("Notifications cannot be combined with waiting for creation; choose one")
596
+ if options.get("fork") is not None:
597
+ raise SessionUnavailable(
598
+ "This fork format is not supported; "
599
+ "no identity claimed"
600
+ )
601
+ # Preserve the old explicit preparation signature: never expose lower test hooks.
602
+ unexpected = sorted(set(options) - {
603
+ "engine", "cwd", "worker_id", "name", "model", "effort",
604
+ "idle_timeout", "permissions", "sandbox", "approval_policy", "agent",
605
+ "append_system_prompt", "allowed_tools", "disallowed_tools",
606
+ "add_dir", "permission_mode", "created_via", "import_session_id",
607
+ "imported_from", "fork", "_fork_anchor", "timeout", "attachment",
608
+ })
609
+ if unexpected:
610
+ raise TypeError(f"create() got unexpected keyword arguments: {unexpected}")
611
+ # Attachment is internal to creation; the lower builder accepts only its intent.
612
+ attachment = options.pop("attachment", None)
613
+ engine = options.get("engine")
614
+ if attachment is not None:
615
+ from ccx.errors import CapabilityUnsupported
616
+ if not isinstance(attachment, live_vars.PreparedAttachment):
617
+ raise InvalidRequest("initial attachment must be prepared before creation")
618
+ # Same normalizer and defaults as creation_steps.prepare_worker; a
619
+ # duplicate permission-policy interpretation would admit wrong shells.
620
+ _, create_args = validated_create_spec(
621
+ engine=engine, cwd=options.get("cwd"), model=options.get("model"),
622
+ effort=options.get("effort"),
623
+ idle_timeout=options.get("idle_timeout", DEFAULT_IDLE_TIMEOUT_S),
624
+ permissions=options.get("permissions"), sandbox=options.get("sandbox"),
625
+ approval_policy=options.get("approval_policy"), agent=options.get("agent"),
626
+ append_system_prompt=options.get("append_system_prompt"),
627
+ allowed_tools=options.get("allowed_tools"),
628
+ disallowed_tools=options.get("disallowed_tools"),
629
+ add_dir=options.get("add_dir"), permission_mode=options.get("permission_mode"),
630
+ )
631
+ if not (create_args.get("skip_permissions") is True if engine == "claude"
632
+ else create_args.get("sandbox") == "danger-full-access"
633
+ and create_args.get("approvalPolicy") == "never"):
634
+ raise CapabilityUnsupported(
635
+ 'Variables require full permissions (permissions="full").',
636
+ engine=engine, capability="shared_python_variables",
637
+ )
638
+ if start_timeout is None:
639
+ start_timeout = 90.
640
+ if (type(start_timeout) not in (int, float)
641
+ or not isfinite(start_timeout) or not 0 < start_timeout <= 90):
642
+ raise InvalidRequest("start_timeout must be positive, finite and at most 90 seconds")
643
+ if deadline is not None and (type(deadline) not in (float, int)
644
+ or not isfinite(deadline)):
645
+ raise InvalidRequest("native creation deadline must be finite")
646
+
647
+ # Pin this original ACTIVE generation before minting or touching a native
648
+ # path. A different generation is never an implicit fallback for a retry.
649
+ try:
650
+ ctx = require_ccx_active(constants.CCX_ROOT,
651
+ expected_generation=expected_generation)
652
+ except (GenerationError, OSError) as exc:
653
+ raise SessionUnavailable(f"Worker records are unavailable; check CCX_ROOT: {exc}") from exc
654
+ pinned_generation = ctx.generation
655
+ supplied = options.get("worker_id")
656
+ if supplied is not None:
657
+ try:
658
+ supplied = canonical_worker_id(supplied)
659
+ except InvalidTarget as exc:
660
+ raise InvalidRequest(
661
+ f"worker_id must be canonical full UUIDv4: {exc}"
662
+ ) from exc
663
+ # A *fresh* creation cannot claim a UUID without a native executable.
664
+ # Existing caller-owned UUID retries still reconcile their durable claim,
665
+ # including a terminal provider refusal when the CLI has since disappeared.
666
+ existing = False
667
+ if supplied is not None:
668
+ from ccx.store.identity import UnknownWorker
669
+ try:
670
+ existing = ctx.resolve(supplied).engine == engine
671
+ except UnknownWorker:
672
+ pass
673
+ if engine in {"claude", "codex"} and not existing:
674
+ from ccx.system.engine_binary import resolve_engine_binary
675
+ resolve_engine_binary(engine)
676
+ # Public new/fork (CLI, Python and protocol) must validate the explicit
677
+ # native model before minting an identity or writing any claim/record.
678
+ if (engine == "codex" and options.get("model") is not None
679
+ and options.get("_fork_anchor") is None):
680
+ from ccx.codex.runtime import validate_model
681
+ from ccx.system.cwd_paths import canonical_cwd
682
+ validate_model(options["model"], cwd=canonical_cwd(options["cwd"]))
683
+ # Only a CCX-owned full-UUID caller is a delegation edge. Native provider
684
+ # session/thread environment is not parent authority, and malformed/stale
685
+ # self stamps must never be promoted into one.
686
+ from ccx.store.records import spawned_by_from_env
687
+ try:
688
+ caller = spawned_by_from_env()
689
+ except SessionUnavailable:
690
+ caller = None
691
+ spawned_by = caller if caller and caller.get("kind") == "ccx-worker" else None
692
+ chosen = supplied if supplied is not None else mint_worker_id()
693
+ info = {
694
+ "worker_id": chosen, "worker": f"{engine}:{chosen}",
695
+ "engine": engine, "short": chosen[:8], "record": None,
696
+ "created": False, "reconciled": False,
697
+ "activation_state": "unproven", "native_ready": False,
698
+ }
699
+ # Only the recognized private frozen fork anchor is admitted.
700
+ options.pop("timeout", None) # Already validated; retained as turn_timeout for wait.
701
+ options.pop("fork", None) # Legacy fork was refused before the ACTIVE pin.
702
+ options["fork_anchor"] = options.pop("_fork_anchor", None)
703
+ options["worker_id"] = chosen
704
+ options.setdefault("idle_timeout", DEFAULT_IDLE_TIMEOUT_S)
705
+ try:
706
+ original = prepare_worker(ctx, spawned_by=spawned_by, **options)
707
+ except (CreationConflict, OwnerConflict) as exc:
708
+ raise OperationConflict(
709
+ f"Worker ID already belongs to another configuration; choose a new ID: {exc}",
710
+ short=chosen[:8], reason="global_worker_uuid_conflict", worker_id=chosen,
711
+ ) from exc
712
+ except (GenerationError, IdentityError) as exc:
713
+ raise CreatePartialFailure(
714
+ "Worker setup is incomplete; check status before retrying; "
715
+ "retain and reconcile exact worker_id",
716
+ info=info, phase="record", cause=exc,
717
+ ) from exc
718
+ info.update(original.as_dict())
719
+ info["record"] = original.record
720
+ try:
721
+ names = _bind_once(ctx, original.engine, original.worker_id, attachment)
722
+ except OperationConflict:
723
+ raise
724
+ except Exception as exc:
725
+ # The first record already belongs to this exact UUID. A failed
726
+ # bind is not a native failure, and could be on an activated retry.
727
+ activation_state = original.activation_state
728
+ try:
729
+ activation_state = ctx.native_activation(original.worker)["state"]
730
+ except Exception:
731
+ pass # Never mask the original missing/unsafe binding proof.
732
+ raise CreatePartialFailure(
733
+ "Worker record exists but variables could not be attached; "
734
+ "is not proven; retain and reconcile the same worker_id",
735
+ info={**info, "activation_state": activation_state,
736
+ "cause_type": type(exc).__name__},
737
+ phase="initial-variables", cause=exc,
738
+ ) from exc
739
+ try:
740
+ ctx.revalidate()
741
+ if deadline is not None and time.monotonic() >= deadline:
742
+ raise NativeEffectIndeterminate("native creation request deadline expired; no replay")
743
+ if engine == "codex":
744
+ bound = _complete_codex_create(
745
+ ctx, original, info, start_timeout=start_timeout, deadline=deadline,
746
+ )
747
+ else:
748
+ bound = _complete_claude_create(
749
+ ctx, original, start_timeout=start_timeout, deadline=deadline,
750
+ )
751
+ ctx.revalidate()
752
+ if deadline is not None and time.monotonic() >= deadline:
753
+ raise NativeEffectIndeterminate(
754
+ "original native creation confirmed after request deadline; "
755
+ "retain the exact full worker UUID"
756
+ )
757
+ result = {
758
+ **original.as_dict(), "record": bound,
759
+ "activation_state": "confirmed", "native_ready": True,
760
+ "record_committed": True,
761
+ }
762
+ except Exception as exc:
763
+ # Once the original owner exists, even a stale generation, corrupt
764
+ # marker, or failed claim-CAS cannot lose its minted full UUID.
765
+ # Preserve the exact cause: corruption is NOT retryable unknown.
766
+ info["native_start_error"] = str(exc)
767
+ info["cause_type"] = type(exc).__name__
768
+ from ccx.store.root import StorageIntegrityError
769
+ if isinstance(exc, StorageIntegrityError):
770
+ info["integrity_error"] = True
771
+ # Keep the offline native-start reconciliation for genuinely
772
+ # indeterminate effects or transport failures, but never probe a
773
+ # suspect stale/corrupt store merely to decorate an integrity error.
774
+ if isinstance(exc, (NativeEffectIndeterminate, SessionUnavailable, OSError)):
775
+ try:
776
+ if engine == "codex" and "native_rejection" not in info:
777
+ rejected = ctx.reconcile_codex_native_rejection(original.worker)
778
+ if rejected is not None:
779
+ info["native_rejection"] = {
780
+ "code": rejected["code"], "message": rejected["message"],
781
+ }
782
+ if engine == "codex" and "native_rejection" not in info:
783
+ local = ctx.reconcile_codex_local_refusal(original.worker)
784
+ if local is not None:
785
+ info["native_local_refusal"] = {
786
+ "reason": local["reason"], "stage": local["stage"],
787
+ "proven": True,
788
+ }
789
+ except Exception:
790
+ pass # Optional terminal proof never replaces the cause.
791
+ if engine == "claude" and deadline is None and not info.get("integrity_error"):
792
+ # A signed-out Claude sits on its login screen until activation
793
+ # times out. Name the real cause; only checked after a failure,
794
+ # and never past a caller's deadline.
795
+ try:
796
+ from ccx.system.engine_binary import account_signed_out, resolve_engine_binary
797
+ if account_signed_out("claude", resolve_engine_binary("claude")):
798
+ info["signed_out"] = True
799
+ except Exception:
800
+ pass
801
+ try:
802
+ # Even after a stale/failed bind, the original immutable claim
803
+ # can sometimes be read safely. An unavailable or suspect
804
+ # store must never hide the original error or its UUID.
805
+ state = ctx.native_activation(original.worker)["state"]
806
+ if state in {"unprepared", "prepared", "submitted-unknown",
807
+ "confirmed", "rejected", "local-refused"}:
808
+ info["activation_state"] = state
809
+ except Exception:
810
+ pass
811
+ partial = CreatePartialFailure(
812
+ "Claude Code is not signed in: run `claude` once to sign in, then try again"
813
+ if info.get("signed_out") else
814
+ "full worker UUID and original first record are durable, but creation "
815
+ "did not complete cleanly; retain worker_id, do not remint",
816
+ info=info, phase="native-activation",
817
+ )
818
+ partial.cause = exc # Keep the original native-partial EXIT_UNAVAILABLE.
819
+ raise partial from exc
820
+ if prompt is None:
821
+ if engine == "codex":
822
+ _finalize_empty_codex_create(ctx, result)
823
+ if parent is None:
824
+ return result
825
+ # No first model turn exists. Arm exactly one create-keyed stable
826
+ # outcome after the original native binding is confirmed.
827
+ try:
828
+ # Codex may need an original-thread load to prove idle; a
829
+ # confirmed Claude SessionStart already proves its stable
830
+ # creation outcome without another host readiness action.
831
+ if (engine == "codex" and
832
+ (result["record"].get("lifecycle") or {}).get("state") == "prepared"):
833
+ from ccx.ops.send import ensure_ready
834
+ ensure_ready(result["worker"], expected_generation=pinned_generation)
835
+ from ccx.notify.registry import create_registration, public_registration
836
+ from ccx.notify.daemon import ensure_notify_daemon
837
+ registered = create_registration(
838
+ parent=parent, worker_engine=engine, worker_id=result["worker_id"],
839
+ worker_name=(result["record"] or {}).get("name"),
840
+ operation={"kind": f"{engine}-stable", "expectTurn": False,
841
+ "selectionMode": "status-only", "stableState": "idle"},
842
+ source="new", settle=settle,
843
+ )
844
+ ensure_notify_daemon()
845
+ return {**result, "notification": public_registration(registered)}
846
+ except Exception as exc:
847
+ # The original full UUID and native identity were already
848
+ # confirmed; failure to arm must never create a new worker.
849
+ raise CreatePartialFailure(
850
+ "worker created, but original promptless parent notification "
851
+ "is not proven armed; retain the exact worker_id",
852
+ info={**result, "cause_type": type(exc).__name__},
853
+ phase="notification-registration", cause=exc,
854
+ ) from exc
855
+ # Native creation is always promptless. A first message uses precisely
856
+ # the ordinary semantic receipt: the full UUID + create:initial fixes
857
+ # one original prompt across process loss and all public adapters.
858
+ from ccx.ops.send import send_turn
859
+ from ccx.claude.delivery import ClaudeDeliveryConflict
860
+ from ccx.codex.delivery import DeliveryReceiptConflict
861
+ from ccx.store.root import NativeEffectIndeterminate
862
+ initial = "create:initial"
863
+ status = None
864
+ try:
865
+ delivered = send_turn(
866
+ result["worker"], live_vars.with_variable_hint(prompt, names),
867
+ delivery_id=initial,
868
+ mode="new_turn_only", expected_generation=pinned_generation,
869
+ notify_parent=parent is not None, settle=settle, cap=cap,
870
+ )
871
+ status = delivered.get("status")
872
+ receipt = delivered.get("delivery")
873
+ if status == "rejected" and isinstance(receipt, dict):
874
+ from ccx.errors import DeliveryRejected
875
+ raise DeliveryRejected(
876
+ receipt.get("rejection_message") or "original initial prompt rejected",
877
+ short=result["short"], refusal=receipt.get("rejection_code"),
878
+ delivery_id=initial, delivery_status="rejected",
879
+ )
880
+ # The real Claude host ACKs durable ownership before evidence of
881
+ # native turn placement; owned is PENDING, not accepted/completed.
882
+ valid = (
883
+ isinstance(receipt, dict)
884
+ and receipt.get("delivery_id") == initial
885
+ and receipt.get("route") == "new_turn"
886
+ and receipt.get("owned") is True
887
+ and (
888
+ (engine == "claude" and status in {"owned", "accepted"}
889
+ and receipt.get("status") == (
890
+ "pending" if status == "owned" else "accepted"))
891
+ or (engine == "codex" and status == "accepted"
892
+ and receipt.get("status") == "accepted"
893
+ and isinstance(receipt.get("turn_id"), str)
894
+ and bool(receipt["turn_id"]))
895
+ )
896
+ )
897
+ if not valid:
898
+ raise NativeEffectIndeterminate(
899
+ "initial prompt has no positive original delivery receipt; "
900
+ "reconcile create:initial, never send a new token"
901
+ )
902
+ ctx.revalidate()
903
+ result = {**result, "delivery_id": initial,
904
+ "delivery_status": status, "delivery": receipt,
905
+ **({"notification": delivered["notification"]}
906
+ if parent is not None and delivered.get("notification") is not None
907
+ else {})}
908
+ except Exception as exc:
909
+ # Identity/activation were positively bound. A delivery failure
910
+ # cannot roll them back or turn this into a fresh native creation.
911
+ from ccx.errors import DeliveryRejected
912
+ if (engine == "codex" and isinstance(exc, BudgetStopped)
913
+ and exc.phase == "preflight"):
914
+ # Original cap admission refused before any provider turn/start.
915
+ # This is the same promptless bound thread, now ready for peek.
916
+ _finalize_empty_codex_create(ctx, result, ("prepared", "idle"))
917
+ conflict = isinstance(exc, (ClaudeDeliveryConflict, DeliveryReceiptConflict, OperationConflict))
918
+ info = {**result, "delivery_id": initial,
919
+ "delivery_status": status,
920
+ "cause_type": type(exc).__name__,
921
+ "initial_delivery_conflict": conflict,
922
+ "initial_delivery_rejected": isinstance(exc, DeliveryRejected)}
923
+ raise CreatePartialFailure(
924
+ "worker created, but original initial prompt is not proven "
925
+ "delivered; retain worker_id and reconcile create:initial",
926
+ info=info, phase="initial-delivery", delivery_id=initial,
927
+ delivery_status=status, cause=exc,
928
+ ) from exc
929
+ if not wait:
930
+ return result
931
+ from ccx.ops.wait import wait_delivery
932
+ try:
933
+ if deadline is not None:
934
+ left = max(0.0, deadline - time.monotonic())
935
+ turn_timeout = left if turn_timeout is None else min(turn_timeout, left)
936
+ observed = wait_delivery(
937
+ result["worker"], delivery_id=initial, timeout=turn_timeout,
938
+ expected_generation=pinned_generation,
939
+ )
940
+ return {**result, **observed, "waited": True}
941
+ except Exception as exc:
942
+ info = {**result, "cause_type": type(exc).__name__,
943
+ "turn_ref": getattr(exc, "turn_ref", None)}
944
+ status = getattr(exc, "delivery_status", result["delivery_status"])
945
+ info["delivery_status"] = status
946
+ raise CreatePartialFailure(
947
+ "original first turn did not complete with exact proof; "
948
+ "retain worker_id and reconcile create:initial",
949
+ info=info, phase="wait-report", delivery_id=initial,
950
+ delivery_status=status, cause=exc,
951
+ ) from exc
952
+ raise RuntimeError("Worker startup is not confirmed; inspect worker status")