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.
- package/LICENSE +21 -0
- package/README.md +241 -2
- package/bin/ccx +56 -0
- package/ccx/__init__.py +163 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +762 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +250 -0
- package/ccx/claude/host/session.py +5184 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2281 -0
- package/ccx/claude/usage.py +837 -0
- package/ccx/claude/usage_control.py +152 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/read.py +1020 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +301 -0
- package/ccx/cli/parser.py +1324 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1900 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +732 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1556 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2396 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1412 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +289 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1540 -0
- package/ccx/ops/conversation_paging.py +1338 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +579 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +770 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +306 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +636 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +254 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +804 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +705 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2081 -0
- package/ccx.py +9 -0
- package/package.json +40 -4
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +155 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
|
@@ -0,0 +1,1590 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
8
|
+
from ccx.codex.delivery import (
|
|
9
|
+
route as send_receipt_route,
|
|
10
|
+
)
|
|
11
|
+
from ccx.codex.protocol import AppServerClient, ProtocolError, ProtocolNotSent, ProtocolTransportError
|
|
12
|
+
from ccx.codex.reports import select_report_for_turn
|
|
13
|
+
from ccx.codex.records import now_ms
|
|
14
|
+
from ccx.codex.daemon.models import WorkerKilled, _TurnLaunch, _native_history_budget
|
|
15
|
+
import ccx.codex.daemon.worker as daemon_worker
|
|
16
|
+
import ccx.codex.daemon.release as daemon_release
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ccx.codex.daemon.worker import Worker
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_CODEX_NATIVE_START_HELD_LIMIT = 16
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_CODEX_NATIVE_START_UNSAFE_LIMIT = 32
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_CODEX_NATIVE_START_RETRY_PER_PASS = 2
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_CODEX_NATIVE_START_ID_MAX_BYTES = 2048
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_CODEX_NATIVE_START_REPLY_MAX_BYTES = 64 * 1024
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _original_caller_config(worker_id: str) -> dict[str, Any]:
|
|
38
|
+
"""Per-thread shell env naming the exact full-UUID caller.
|
|
39
|
+
|
|
40
|
+
Codex applies it per start/resume request, so an original thread gets it
|
|
41
|
+
again on every resume, not only at its first thread/start.
|
|
42
|
+
"""
|
|
43
|
+
from ccx.controllers.live_vars import codex_thread_config
|
|
44
|
+
return codex_thread_config(worker_id)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _with_default_effort(config: dict[str, Any], args: dict[str, Any]) -> dict[str, Any]:
|
|
48
|
+
"""Make the worker's effort the thread default, not only a per-turn value.
|
|
49
|
+
|
|
50
|
+
Turns typed in an attached TUI carry no effort, so a thread whose CCX turn
|
|
51
|
+
has not run yet (fresh or just revived) would otherwise use Codex's default.
|
|
52
|
+
"""
|
|
53
|
+
if args.get("effort"):
|
|
54
|
+
return {**config, "model_reasoning_effort": args["effort"]}
|
|
55
|
+
return config
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _original_subscription_in_this_runtime(
|
|
59
|
+
daemon, target: str, thread_id: str, record: dict[str, Any],
|
|
60
|
+
) -> bool:
|
|
61
|
+
"""Original activation's current daemon/app-server lifetime, not PID alone."""
|
|
62
|
+
from ccx.store.root import _read_json
|
|
63
|
+
import hashlib
|
|
64
|
+
store = daemon._uuid_store
|
|
65
|
+
client = daemon.client
|
|
66
|
+
if (daemon.stopping or not daemon.ready.is_set() or client is None
|
|
67
|
+
or client.process.poll() is not None):
|
|
68
|
+
return False
|
|
69
|
+
original = store.native_activation(target)
|
|
70
|
+
owner = store.resolve(target)
|
|
71
|
+
token = original.get("submissionId")
|
|
72
|
+
source = f"ccx-worker:{owner.worker_id}"
|
|
73
|
+
if (original.get("state") != "confirmed"
|
|
74
|
+
or original.get("source") != source
|
|
75
|
+
or (original.get("nativeBinding") or {}).get("threadId") != thread_id
|
|
76
|
+
or not isinstance(token, str) or not token):
|
|
77
|
+
return False
|
|
78
|
+
key = hashlib.sha256(("activate\x00" + token).encode()).hexdigest()
|
|
79
|
+
try:
|
|
80
|
+
with store.lease():
|
|
81
|
+
with store._gen("runtime-metadata", "codex-native-effects",
|
|
82
|
+
owner.worker_id) as directory:
|
|
83
|
+
effect = _read_json(directory, key + ".json")
|
|
84
|
+
except FileNotFoundError:
|
|
85
|
+
return False
|
|
86
|
+
if not isinstance(effect, dict):
|
|
87
|
+
return False
|
|
88
|
+
candidate = effect.get("nativeCandidate") or {}
|
|
89
|
+
return bool(
|
|
90
|
+
effect.get("kind") == "activate"
|
|
91
|
+
and effect.get("workerId") == owner.worker_id
|
|
92
|
+
and effect.get("engine") == "codex"
|
|
93
|
+
and effect.get("generation") == store.generation
|
|
94
|
+
and effect.get("token") == token
|
|
95
|
+
and effect.get("source") == source
|
|
96
|
+
and effect.get("createSha256") == record.get("createSha256")
|
|
97
|
+
and effect.get("daemonInstanceId") == daemon.instance_id
|
|
98
|
+
and effect.get("appServerPid") == client.process.pid
|
|
99
|
+
and isinstance(candidate, dict)
|
|
100
|
+
and candidate.get("threadId") == thread_id
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _load_original_native_worker(
|
|
105
|
+
daemon, worker_id: str, timeout: float | None,
|
|
106
|
+
) -> dict[str, Any]:
|
|
107
|
+
"""Ensure one source-proven full-UUID thread is natively subscribed.
|
|
108
|
+
|
|
109
|
+
Thread/read alone is never native loadedness after park. Existing
|
|
110
|
+
per-worker load/kill locks and release fences own the race.
|
|
111
|
+
"""
|
|
112
|
+
from ccx.store.root import NativeActionDenied
|
|
113
|
+
if timeout is not None and (not isinstance(timeout, (int, float)) or timeout <= 0):
|
|
114
|
+
return {"ok": False, "error": "invalid full-UUID load timeout"}
|
|
115
|
+
budget = min(float(timeout) if timeout is not None else 90., 90.)
|
|
116
|
+
handle = "codex:" + worker_id
|
|
117
|
+
try:
|
|
118
|
+
daemon._uuid_store.authorize_native_action(handle, "resume")
|
|
119
|
+
worker = daemon._adopt_original_native_worker(handle, read_timeout=budget)
|
|
120
|
+
original_native = (worker.record.get("native") or {}).get("threadId")
|
|
121
|
+
with worker.load_lock:
|
|
122
|
+
with daemon.lock:
|
|
123
|
+
if daemon.workers.get(worker_id) is not worker or daemon.stopping:
|
|
124
|
+
raise WorkerKilled("original Codex Worker unloaded during resume")
|
|
125
|
+
with worker.lock:
|
|
126
|
+
if worker.killed.is_set() or worker.kill_intent.is_set():
|
|
127
|
+
raise WorkerKilled("original Codex Worker is being killed")
|
|
128
|
+
lifecycle = worker.record["lifecycle"]
|
|
129
|
+
if isinstance(worker.record.get("nativeTurnStart"), dict):
|
|
130
|
+
raise RuntimeError("original native turn/start is unresolved; no resume")
|
|
131
|
+
if lifecycle["state"] in ("busy", "waiting"):
|
|
132
|
+
# Exact source-verified read-only adoption remains a
|
|
133
|
+
# valid low-level load result for an old active gate,
|
|
134
|
+
# but it is not an idle/subscribed resume. The public
|
|
135
|
+
# ensure_ready insists on loaded=True and state=idle.
|
|
136
|
+
return {"ok": True, "changed": False,
|
|
137
|
+
"loaded": bool(worker.loaded),
|
|
138
|
+
**worker.public_status()}
|
|
139
|
+
if worker.loaded:
|
|
140
|
+
if lifecycle["state"] == "prepared":
|
|
141
|
+
# Original promptless native activation only wrote
|
|
142
|
+
# the initial prepared lifecycle. Matching the
|
|
143
|
+
# original current app-server effect+thread/read
|
|
144
|
+
# proves the subscribed zero-turn thread is ready.
|
|
145
|
+
worker._commit_state("idle", activeTurnId=None)
|
|
146
|
+
if worker.record["lifecycle"]["state"] != "idle":
|
|
147
|
+
raise RuntimeError("original Codex worker is not idle")
|
|
148
|
+
return {"ok": True, "changed": False, "loaded": True,
|
|
149
|
+
**worker.public_status()}
|
|
150
|
+
# An earlier resume may have timed out while Codex kept
|
|
151
|
+
# loading the thread. Resuming again is safe: Codex attaches
|
|
152
|
+
# to a thread it already has loaded.
|
|
153
|
+
if not isinstance(original_native, str) or not original_native:
|
|
154
|
+
raise NativeActionDenied("original native Codex threadId missing")
|
|
155
|
+
if not os.path.isdir(worker.record["cwd"]):
|
|
156
|
+
raise RuntimeError("original Codex workspace no longer exists")
|
|
157
|
+
args = worker.record.get("createArgs") or {}
|
|
158
|
+
params = {"cwd": worker.record["cwd"],
|
|
159
|
+
"sandbox": args.get("sandbox"),
|
|
160
|
+
"approvalPolicy": args.get("approvalPolicy"),
|
|
161
|
+
"serviceName": "ccx",
|
|
162
|
+
"config": _with_default_effort(
|
|
163
|
+
_original_caller_config(worker.worker_id), args)}
|
|
164
|
+
if args.get("model"):
|
|
165
|
+
params["model"] = args["model"]
|
|
166
|
+
def publish_resume():
|
|
167
|
+
with worker.lock:
|
|
168
|
+
if worker.killed.is_set() or worker.kill_intent.is_set():
|
|
169
|
+
raise WorkerKilled("original Codex Worker killed before native resume")
|
|
170
|
+
current = worker.record["lifecycle"]
|
|
171
|
+
current["state"] = "resuming"
|
|
172
|
+
current["since"] = now_ms()
|
|
173
|
+
current["activeTurnId"] = None
|
|
174
|
+
worker.record["pendingApprovals"] = []
|
|
175
|
+
worker._save()
|
|
176
|
+
try:
|
|
177
|
+
reply = worker._resume_exact_thread(
|
|
178
|
+
original_native, params, timeout=budget,
|
|
179
|
+
before_submit=publish_resume,
|
|
180
|
+
)
|
|
181
|
+
except ProtocolError as error:
|
|
182
|
+
# An answered JSON-RPC rejection is not an uncertain
|
|
183
|
+
# subscription. A local timeout/transport loss is: its
|
|
184
|
+
# request_id is absent, so preserve the no-replay fence.
|
|
185
|
+
received = (error.request_id is not None or
|
|
186
|
+
(isinstance(error.__cause__, ProtocolError)
|
|
187
|
+
and error.__cause__.request_id is not None))
|
|
188
|
+
if received and not isinstance(error, ProtocolTransportError):
|
|
189
|
+
with worker.lock:
|
|
190
|
+
worker._commit_state(
|
|
191
|
+
"crashed", lastError=f"original native Codex resume rejected: {error}",
|
|
192
|
+
)
|
|
193
|
+
raise
|
|
194
|
+
native_thread = reply.get("thread") if isinstance(reply, dict) else None
|
|
195
|
+
native_status = (native_thread.get("status") if isinstance(native_thread, dict)
|
|
196
|
+
else None)
|
|
197
|
+
# The independent source-fenced thread/read proved the source
|
|
198
|
+
# even if the reply omits its optional threadSource field.
|
|
199
|
+
if not isinstance(native_thread, dict):
|
|
200
|
+
error_message = "original native Codex resume reply missing thread"
|
|
201
|
+
elif native_thread.get("id") != original_native:
|
|
202
|
+
error_message = "Codex resumed the wrong thread; inspect the worker"
|
|
203
|
+
elif native_thread.get("threadSource") not in (
|
|
204
|
+
None, daemon._uuid_store.codex_source(handle)
|
|
205
|
+
):
|
|
206
|
+
error_message = "Codex thread belongs to another worker; inspect its handle"
|
|
207
|
+
elif not isinstance(native_status, dict):
|
|
208
|
+
error_message = "original native Codex resume reply missing thread status"
|
|
209
|
+
elif native_status.get("type") not in ("idle", "systemError"):
|
|
210
|
+
error_message = (
|
|
211
|
+
"original native Codex resume reply not ready: "
|
|
212
|
+
f"status {native_status.get('type')!r}"
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
error_message = None
|
|
216
|
+
if error_message is not None:
|
|
217
|
+
with worker.lock:
|
|
218
|
+
if worker.killed.is_set() or worker.kill_intent.is_set():
|
|
219
|
+
raise WorkerKilled("original Codex Worker killed during resume")
|
|
220
|
+
worker._commit_state("crashed", lastError=error_message)
|
|
221
|
+
raise RuntimeError(error_message)
|
|
222
|
+
with worker.lock:
|
|
223
|
+
if worker.killed.is_set() or worker.kill_intent.is_set():
|
|
224
|
+
raise WorkerKilled("original Codex Worker killed during resume")
|
|
225
|
+
worker.record["pendingApprovals"] = []
|
|
226
|
+
worker.record["activeTurnError"] = None
|
|
227
|
+
worker.record["lifecycle"]["lastError"] = None
|
|
228
|
+
worker.record["lifecycle"]["activeTurnId"] = None
|
|
229
|
+
worker._commit_state("idle")
|
|
230
|
+
worker._resumed_system_error_pending = (
|
|
231
|
+
native_status.get("type") == "systemError"
|
|
232
|
+
)
|
|
233
|
+
worker.loaded = True
|
|
234
|
+
return {"ok": True, "changed": True, "loaded": True,
|
|
235
|
+
**worker.public_status()}
|
|
236
|
+
except WorkerKilled as error:
|
|
237
|
+
if "worker" in locals():
|
|
238
|
+
daemon_release._abandon_killed_load(daemon, worker)
|
|
239
|
+
return {"ok": False, "error": str(error)}
|
|
240
|
+
except Exception as error:
|
|
241
|
+
# On a lost native reply the resuming record is deliberately held
|
|
242
|
+
# and native_threads is a possible-subscription cleanup obligation.
|
|
243
|
+
return {"ok": False, "error": str(error)}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _adopt_original_native_worker(
|
|
247
|
+
daemon, target: str, *, read_timeout: float = 1.,
|
|
248
|
+
) -> "Worker":
|
|
249
|
+
"""Register one immutable full-UUID thread in the original daemon index.
|
|
250
|
+
|
|
251
|
+
App-server notifications, lifecycle locks and native send/observe
|
|
252
|
+
share one Worker instance. Merely
|
|
253
|
+
knowing a threadId is never authority: compare exact durable native
|
|
254
|
+
source and confirmed creation claim before constructing anything.
|
|
255
|
+
This never calls thread/start or thread/resume.
|
|
256
|
+
"""
|
|
257
|
+
from ccx.store.root import NativeActionDenied
|
|
258
|
+
store = daemon._uuid_store
|
|
259
|
+
owner = store.authorize_native_action(target, "send")
|
|
260
|
+
if owner.engine != "codex":
|
|
261
|
+
raise NativeActionDenied("daemon adoption requires Codex full UUID")
|
|
262
|
+
rec = store.record(owner.worker)
|
|
263
|
+
native = rec.get("native") or {}
|
|
264
|
+
thread_id = native.get("threadId")
|
|
265
|
+
source = store.codex_source(owner.worker)
|
|
266
|
+
if (not isinstance(thread_id, str) or not thread_id
|
|
267
|
+
or native.get("threadSource") != source):
|
|
268
|
+
raise NativeActionDenied("native threadId/source was not confirmed")
|
|
269
|
+
# A live Worker is a cache of the immutable original global owner;
|
|
270
|
+
# never key it by a short/native thread ID. Construct before taking
|
|
271
|
+
# the daemon-global lock to avoid serializing journal inspections.
|
|
272
|
+
with daemon._lifecycle_lock(owner.worker_id):
|
|
273
|
+
# After a daemon restart the original native thread may no
|
|
274
|
+
# longer exist. A recorded threadId alone is not proof this
|
|
275
|
+
# app-server owns it. Read the exact original native source
|
|
276
|
+
# before adopting; never call thread/start or thread/resume
|
|
277
|
+
# as a fallback for a missing/different native thread.
|
|
278
|
+
if daemon.client is None:
|
|
279
|
+
raise RuntimeError("Codex app-server unavailable for adoption")
|
|
280
|
+
observed=daemon.client.request(
|
|
281
|
+
"thread/read",
|
|
282
|
+
{"threadId":thread_id,"includeTurns":False},
|
|
283
|
+
timeout=read_timeout,
|
|
284
|
+
)
|
|
285
|
+
native_proof=(
|
|
286
|
+
observed.get("thread") if isinstance(observed,dict) else None
|
|
287
|
+
)
|
|
288
|
+
if (not isinstance(native_proof,dict)
|
|
289
|
+
or native_proof.get("id")!=thread_id
|
|
290
|
+
or native_proof.get("threadSource") != source):
|
|
291
|
+
raise RuntimeError(
|
|
292
|
+
"original Codex native thread/read source absent or foreign; "
|
|
293
|
+
"never recreate an original thread"
|
|
294
|
+
)
|
|
295
|
+
with daemon.lock:
|
|
296
|
+
existing = daemon.workers.get(owner.worker_id)
|
|
297
|
+
if existing is not None:
|
|
298
|
+
if (existing.worker_id != owner.worker_id
|
|
299
|
+
or daemon.by_thread.get(thread_id) != owner.worker_id):
|
|
300
|
+
raise RuntimeError(
|
|
301
|
+
"Codex registry contradicts original full UUID"
|
|
302
|
+
)
|
|
303
|
+
return existing
|
|
304
|
+
if daemon.stopping or not daemon.ready.is_set():
|
|
305
|
+
raise RuntimeError("Codex singleton not accepting new workers")
|
|
306
|
+
bound = daemon.by_thread.get(thread_id)
|
|
307
|
+
if bound is not None and bound != owner.worker_id:
|
|
308
|
+
raise RuntimeError("native thread already owned by another UUID")
|
|
309
|
+
worker = daemon_worker.Worker(daemon, rec)
|
|
310
|
+
# Native membership is an unsubscribe obligation, not readiness.
|
|
311
|
+
# A parked or old-daemon thread/read can see history without a
|
|
312
|
+
# current app-server subscription. Only original current-epoch
|
|
313
|
+
# activation with an unparked record proves the first subscription.
|
|
314
|
+
worker.native_threads.add(thread_id)
|
|
315
|
+
worker.loaded = (rec["lifecycle"].get("state") in ("prepared", "idle", "busy", "waiting")
|
|
316
|
+
and _original_subscription_in_this_runtime(daemon, owner.worker, thread_id, rec))
|
|
317
|
+
with daemon.lock:
|
|
318
|
+
prior = daemon.workers.get(owner.worker_id)
|
|
319
|
+
if prior is not None:
|
|
320
|
+
return prior
|
|
321
|
+
if daemon.by_thread.get(thread_id) not in (None, owner.worker_id):
|
|
322
|
+
raise RuntimeError("native thread ownership changed")
|
|
323
|
+
daemon.workers[owner.worker_id] = worker
|
|
324
|
+
daemon.by_thread[thread_id] = owner.worker_id
|
|
325
|
+
daemon.empty_since = None
|
|
326
|
+
# A newly adopted app-server is not evidence that a command from a
|
|
327
|
+
# previous native runtime still runs. Repair a surviving stale record.
|
|
328
|
+
with worker.lock:
|
|
329
|
+
if worker._lose_native_background_locked():
|
|
330
|
+
worker._save()
|
|
331
|
+
worker.refresh_parent_notifications()
|
|
332
|
+
return worker
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _native_start_retryable_save_error(daemon, error: BaseException) -> bool:
|
|
336
|
+
"""Retry only recognized transient persistence failures, not corruption."""
|
|
337
|
+
import errno
|
|
338
|
+
from ccx.store.root import GenerationBusy
|
|
339
|
+
return isinstance(error, GenerationBusy) or (
|
|
340
|
+
isinstance(error, OSError)
|
|
341
|
+
and getattr(error, "errno", None) in {
|
|
342
|
+
errno.ENOSPC, errno.EIO, errno.EAGAIN, errno.EINTR, errno.EBUSY,
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _mark_unsafe_native_start_locked(
|
|
348
|
+
daemon, hold_key: tuple[str, str, str], held: dict[str, Any],
|
|
349
|
+
detail: str,
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Record a bounded unsafe diagnostic without pinning native capacity.
|
|
352
|
+
|
|
353
|
+
Caller holds only `_native_start_hold_lock`. Original one-shot effect
|
|
354
|
+
marker/claim stay untouched, and this rejected evidence is never used
|
|
355
|
+
to write a candidate or to justify a new native dispatch.
|
|
356
|
+
"""
|
|
357
|
+
if daemon._native_start_held.get(hold_key) is not held:
|
|
358
|
+
return
|
|
359
|
+
held["unsafe"] = True
|
|
360
|
+
held["error"] = detail
|
|
361
|
+
del daemon._native_start_held[hold_key]
|
|
362
|
+
daemon._native_start_unsafe[hold_key] = detail[:250]
|
|
363
|
+
daemon._native_start_unsafe_total += 1
|
|
364
|
+
if len(daemon._native_start_unsafe) > _CODEX_NATIVE_START_UNSAFE_LIMIT:
|
|
365
|
+
del daemon._native_start_unsafe[next(iter(daemon._native_start_unsafe))]
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _native_start_counts(daemon) -> dict[str, int]:
|
|
369
|
+
"""Bounded daemon health, no exposure of native IDs or source tokens."""
|
|
370
|
+
with daemon._native_start_hold_lock:
|
|
371
|
+
pending = sum(value["candidate"] is None
|
|
372
|
+
and value.get("rejection") is None
|
|
373
|
+
for value in daemon._native_start_held.values())
|
|
374
|
+
held = len(daemon._native_start_held) - pending
|
|
375
|
+
return {
|
|
376
|
+
"pending": pending, "held": held,
|
|
377
|
+
"unsafe": len(daemon._native_start_unsafe),
|
|
378
|
+
"unsafeTotal": daemon._native_start_unsafe_total,
|
|
379
|
+
"capacity": _CODEX_NATIVE_START_HELD_LIMIT,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _janitor_native_start_pass(daemon) -> None:
|
|
384
|
+
"""Retry a bounded number of transient original saves every janitor tick.
|
|
385
|
+
|
|
386
|
+
No native requests or extra threads. Retained evidence is never evicted
|
|
387
|
+
on age; timed-out still-in-flight PendingRequest must continue to pin
|
|
388
|
+
the daemon. Backoff bounds repeated disk/lock work. Explicit daemon
|
|
389
|
+
stop/cutover wins immediately and is never blocked by retry loops.
|
|
390
|
+
"""
|
|
391
|
+
if daemon.stop.is_set():
|
|
392
|
+
return
|
|
393
|
+
now = time.monotonic()
|
|
394
|
+
with daemon._native_start_hold_lock:
|
|
395
|
+
attempts = [key for key, held in daemon._native_start_held.items()
|
|
396
|
+
if (held["candidate"] is not None
|
|
397
|
+
or held.get("rejection") is not None)
|
|
398
|
+
and held["error"] is not None
|
|
399
|
+
and held.get("retryAfter", float("inf")) <= now]
|
|
400
|
+
attempts = attempts[:_CODEX_NATIVE_START_RETRY_PER_PASS]
|
|
401
|
+
for key in attempts:
|
|
402
|
+
if daemon.stop.is_set():
|
|
403
|
+
return
|
|
404
|
+
try:
|
|
405
|
+
daemon._save_held_codex_candidate(key)
|
|
406
|
+
except Exception as error: # noqa: BLE001
|
|
407
|
+
# Do not let a janitor exception disable idle shutdown or
|
|
408
|
+
# notifications for unrelated workers.
|
|
409
|
+
daemon_release._log_janitor_failure(daemon, "original native candidate", error)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _save_held_codex_candidate(
|
|
413
|
+
daemon, hold_key: tuple[str, str, str],
|
|
414
|
+
) -> dict[str, Any]:
|
|
415
|
+
"""Retry fsync of only the original native reply outcome, never dispatch.
|
|
416
|
+
|
|
417
|
+
Called once from the app-server reader callback, or later from a
|
|
418
|
+
native-activation-status socket handler. Both use the same pinned
|
|
419
|
+
generation and original effect lock; a concurrent successful fsync is
|
|
420
|
+
idempotent. Unsafe disk identities and a changed generation stay
|
|
421
|
+
blocked in memory, not overwritten on any later status request.
|
|
422
|
+
"""
|
|
423
|
+
from ccx.store.root import StorageIntegrityError, _read_json, _write_json, _flock_at
|
|
424
|
+
import hashlib
|
|
425
|
+
with daemon._native_start_hold_lock:
|
|
426
|
+
held = daemon._native_start_held.get(hold_key)
|
|
427
|
+
if held is None:
|
|
428
|
+
unsafe = daemon._native_start_unsafe.get(hold_key)
|
|
429
|
+
return ({"status": "unsafe", "error": unsafe}
|
|
430
|
+
if unsafe is not None else {"status": "not-held"})
|
|
431
|
+
if held["unsafe"]:
|
|
432
|
+
return {"status": "unsafe", "error": held["error"]}
|
|
433
|
+
if held["candidate"] is None and held.get("rejection") is None:
|
|
434
|
+
return {"status": "not-held"}
|
|
435
|
+
original = dict(held["original"])
|
|
436
|
+
candidate = dict(held["candidate"]) if held["candidate"] is not None else None
|
|
437
|
+
rejection = dict(held["rejection"]) if held.get("rejection") is not None else None
|
|
438
|
+
generation, worker_id, token = hold_key
|
|
439
|
+
key = hashlib.sha256(("activate" + "\x00" + token).encode()).hexdigest()
|
|
440
|
+
store = daemon._uuid_store
|
|
441
|
+
try:
|
|
442
|
+
if generation != store.generation:
|
|
443
|
+
raise StorageIntegrityError("held native reply belongs to another generation")
|
|
444
|
+
with store.lease():
|
|
445
|
+
with store._gen("runtime-metadata", "codex-native-effects",
|
|
446
|
+
worker_id) as directory:
|
|
447
|
+
with _flock_at(directory, key + ".lock", exclusive=True,
|
|
448
|
+
create=False, timeout=.5):
|
|
449
|
+
saved = _read_json(directory, key + ".json")
|
|
450
|
+
if (not isinstance(saved, dict) or any(
|
|
451
|
+
saved.get(k) != v for k, v in original.items())):
|
|
452
|
+
raise StorageIntegrityError(
|
|
453
|
+
"original Codex effect marker absent/changed; no evidence rewrite"
|
|
454
|
+
)
|
|
455
|
+
if (candidate is None) == (rejection is None):
|
|
456
|
+
raise StorageIntegrityError(
|
|
457
|
+
"original Codex native response outcome is contradictory"
|
|
458
|
+
)
|
|
459
|
+
field, intended, opposite = (
|
|
460
|
+
("nativeCandidate", candidate, "nativeRejected")
|
|
461
|
+
if candidate is not None
|
|
462
|
+
else ("nativeRejected", rejection, "nativeCandidate")
|
|
463
|
+
)
|
|
464
|
+
if (saved.get(opposite) is not None
|
|
465
|
+
or saved.get("nativeNotSubmitted") is not None):
|
|
466
|
+
raise StorageIntegrityError(
|
|
467
|
+
"original Codex native outcome conflicts with local non-submission"
|
|
468
|
+
)
|
|
469
|
+
previous = saved.get(field)
|
|
470
|
+
if previous is not None and previous != intended:
|
|
471
|
+
raise StorageIntegrityError(
|
|
472
|
+
"original Codex native outcome changed; no overwrite"
|
|
473
|
+
)
|
|
474
|
+
if previous is None:
|
|
475
|
+
_write_json(directory, key + ".json", {
|
|
476
|
+
**saved, field: intended,
|
|
477
|
+
})
|
|
478
|
+
if rejection is not None:
|
|
479
|
+
# A marker is always fsynced before terminal claim transition.
|
|
480
|
+
# Failure to seal the claim keeps this exact rejection held for
|
|
481
|
+
# bounded maintenance/status retry; the durable marker also
|
|
482
|
+
# permits a new CLI to reconcile entirely offline.
|
|
483
|
+
sealed = store.reconcile_codex_native_rejection(
|
|
484
|
+
f"codex:{worker_id}"
|
|
485
|
+
)
|
|
486
|
+
if (not isinstance(sealed, dict)
|
|
487
|
+
or sealed.get("code") != rejection["code"]
|
|
488
|
+
or sealed.get("message") != rejection["message"]):
|
|
489
|
+
raise StorageIntegrityError("original native rejection not sealed")
|
|
490
|
+
except Exception as error:
|
|
491
|
+
retryable = _native_start_retryable_save_error(daemon, error)
|
|
492
|
+
# Do not silently discard original response evidence after one
|
|
493
|
+
# ENOSPC/lease-contention failure. Unsafe identity/generation
|
|
494
|
+
# failures are visible and *never* reattempted by status.
|
|
495
|
+
detail = f"{type(error).__name__}: {error}"[:250]
|
|
496
|
+
with daemon._native_start_hold_lock:
|
|
497
|
+
if daemon._native_start_held.get(hold_key) is held:
|
|
498
|
+
held["error"] = detail
|
|
499
|
+
if not retryable:
|
|
500
|
+
_mark_unsafe_native_start_locked(daemon, hold_key, held, detail)
|
|
501
|
+
else:
|
|
502
|
+
# First retry is no sooner than one janitor tick; each
|
|
503
|
+
# failed retry backs off, capped at 30 seconds.
|
|
504
|
+
failures = min(6, held.get("retryFailures", 0) + 1)
|
|
505
|
+
held["retryFailures"] = failures
|
|
506
|
+
held["retryAfter"] = time.monotonic() + min(
|
|
507
|
+
30.0, float(2 ** (failures - 1))
|
|
508
|
+
)
|
|
509
|
+
print(
|
|
510
|
+
f"ccx codexd: original Codex start reply pending disk save "
|
|
511
|
+
f"for {worker_id} ({'transient' if retryable else 'unsafe'}): {detail}",
|
|
512
|
+
file=sys.stderr, flush=True,
|
|
513
|
+
)
|
|
514
|
+
return {"status": "transient" if retryable else "unsafe",
|
|
515
|
+
"error": detail}
|
|
516
|
+
with daemon._native_start_hold_lock:
|
|
517
|
+
if daemon._native_start_held.get(hold_key) is held:
|
|
518
|
+
del daemon._native_start_held[hold_key]
|
|
519
|
+
return {"status": "saved"}
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _native_codex_local_proof(daemon, original: dict, reason: str,
|
|
523
|
+
stage: str) -> dict[str, Any]:
|
|
524
|
+
"""Bounded local attribution, not a cryptographically signed proof."""
|
|
525
|
+
from ccx.store.store import _digest
|
|
526
|
+
import hashlib
|
|
527
|
+
message = reason.encode("utf-8")
|
|
528
|
+
if not message or len(message) > 1024:
|
|
529
|
+
raise ValueError("local Codex refusal reason must be <=1024 bytes")
|
|
530
|
+
fields = {
|
|
531
|
+
"stage": stage, "reason": reason,
|
|
532
|
+
"reasonSha256": hashlib.sha256(message).hexdigest(),
|
|
533
|
+
"nativeRequestWritten": False,
|
|
534
|
+
"appServerPid": original["appServerPid"],
|
|
535
|
+
}
|
|
536
|
+
return {**fields, "localEvidenceSha256": _digest({
|
|
537
|
+
"workerId": original["workerId"],
|
|
538
|
+
"generation": original["generation"],
|
|
539
|
+
"createSha256": original["createSha256"],
|
|
540
|
+
"source": original["source"],
|
|
541
|
+
"submissionId": original["token"],
|
|
542
|
+
"daemonInstanceId": original["daemonInstanceId"],
|
|
543
|
+
"daemonPid": original["daemonPid"],
|
|
544
|
+
"nativeNotSubmitted": fields,
|
|
545
|
+
})}
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _record_original_codex_local_refusal(daemon, worker_id: str, record: dict,
|
|
549
|
+
source: str, token: str, *,
|
|
550
|
+
stage: str, reason: str) -> dict[str, Any]:
|
|
551
|
+
"""fsync a pre-IPC local refusal under the original one-shot effect lock.
|
|
552
|
+
|
|
553
|
+
Recheck the existing same-ID held/unsafe reservations under self.lock,
|
|
554
|
+
in the same order as _submit_original_codex_thread. No competitor can
|
|
555
|
+
register a native writer between this check and the immutable marker
|
|
556
|
+
write. Never infer refusal from a missing marker or a timeout; only
|
|
557
|
+
this controlled pre-dispatch branch has positive zero-native-write proof.
|
|
558
|
+
"""
|
|
559
|
+
from ccx.store.root import _read_json, _write_json, _flock_at
|
|
560
|
+
import hashlib
|
|
561
|
+
store = daemon._uuid_store
|
|
562
|
+
hold_key = (store.generation, worker_id, token)
|
|
563
|
+
original = {
|
|
564
|
+
"workerId": worker_id, "engine": "codex",
|
|
565
|
+
"generation": store.generation, "kind": "activate",
|
|
566
|
+
"token": token, "state": "submitted-unknown",
|
|
567
|
+
"source": source, "createSha256": record["createSha256"],
|
|
568
|
+
"daemonInstanceId": daemon.instance_id,
|
|
569
|
+
"daemonPid": os.getpid(),
|
|
570
|
+
"appServerPid": (daemon.client.process.pid if daemon.client else None),
|
|
571
|
+
**({"nativeMethod": "thread/fork", "forkAnchor": record["forkAnchor"]}
|
|
572
|
+
if record.get("forkAnchor") is not None else {}),
|
|
573
|
+
}
|
|
574
|
+
result = {
|
|
575
|
+
"ok": False, "error": reason, "workerId": worker_id,
|
|
576
|
+
"localRefusalReason": reason, "localRefusalProven": False,
|
|
577
|
+
}
|
|
578
|
+
key = hashlib.sha256(("activate\x00" + token).encode()).hexdigest()
|
|
579
|
+
try:
|
|
580
|
+
# Reentrant lock is intentionally held while writing the one-shot
|
|
581
|
+
# marker: new same-ID reservations need this exact lock first.
|
|
582
|
+
with daemon.lock:
|
|
583
|
+
with daemon._native_start_hold_lock:
|
|
584
|
+
if (hold_key in daemon._native_start_held
|
|
585
|
+
or hold_key in daemon._native_start_unsafe):
|
|
586
|
+
result["error"] = (
|
|
587
|
+
reason + "; existing same-ID native attempt unresolved"
|
|
588
|
+
)
|
|
589
|
+
return result
|
|
590
|
+
with store.lease():
|
|
591
|
+
with store._gen("runtime-metadata", "codex-native-effects",
|
|
592
|
+
worker_id, create=True) as directory:
|
|
593
|
+
with _flock_at(directory, key + ".lock", exclusive=True,
|
|
594
|
+
create=True, timeout=.5):
|
|
595
|
+
if _read_json(directory, key + ".json") is not None:
|
|
596
|
+
result["error"] = (
|
|
597
|
+
reason + "; original effect already fenced; "
|
|
598
|
+
"cannot prove non-submission"
|
|
599
|
+
)
|
|
600
|
+
return result
|
|
601
|
+
proof = _native_codex_local_proof(daemon, original, reason, stage)
|
|
602
|
+
_write_json(directory, key + ".json", {
|
|
603
|
+
**original, "nativeNotSubmitted": proof,
|
|
604
|
+
})
|
|
605
|
+
# From here onward the original marker alone permits offline
|
|
606
|
+
# reconciliation after crash or any transient claim-write failure.
|
|
607
|
+
result["localRefusalProven"] = True
|
|
608
|
+
except Exception as error:
|
|
609
|
+
result["error"] = (
|
|
610
|
+
f"{reason}; original not-submitted proof unavailable: "
|
|
611
|
+
f"{type(error).__name__}: {error}"
|
|
612
|
+
)[:400]
|
|
613
|
+
return result
|
|
614
|
+
try:
|
|
615
|
+
sealed = store.reconcile_codex_local_refusal(f"codex:{worker_id}")
|
|
616
|
+
if not isinstance(sealed, dict) or sealed.get("reason") != reason:
|
|
617
|
+
raise RuntimeError("local refusal terminal claim not sealed")
|
|
618
|
+
except Exception as error:
|
|
619
|
+
# Never lose the fsynced marker merely because the CLI's original
|
|
620
|
+
# claim lock is busy or its terminal claim fsync transiently fails.
|
|
621
|
+
result["error"] = (
|
|
622
|
+
f"{reason}; original marker durable, terminal claim pending: "
|
|
623
|
+
f"{type(error).__name__}: {error}"
|
|
624
|
+
)[:400]
|
|
625
|
+
return result
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _submit_original_codex_thread(daemon, worker_id: str, record: dict,
|
|
629
|
+
source: str, token: str, *,
|
|
630
|
+
admission_deadline: float | None = None) -> dict[str, Any]:
|
|
631
|
+
"""Fence and write one native thread/start, not its slow completion.
|
|
632
|
+
|
|
633
|
+
Reserve bounded daemon memory before native admission. Original
|
|
634
|
+
positive provider evidence stays held until its marker is fsynced,
|
|
635
|
+
including after a transient callback failure. No native request is
|
|
636
|
+
ever submitted from status or the completion callback.
|
|
637
|
+
"""
|
|
638
|
+
from ccx.store.root import NativeEffectIndeterminate, _read_json, _write_json, _flock_at
|
|
639
|
+
import hashlib
|
|
640
|
+
store = daemon._uuid_store
|
|
641
|
+
client = daemon.client
|
|
642
|
+
# Compatibility for explicit internal callers predating the deadline
|
|
643
|
+
# argument; regular CLI always supplies the original shared budget.
|
|
644
|
+
if admission_deadline is None:
|
|
645
|
+
admission_deadline = time.monotonic() + 2.0
|
|
646
|
+
if time.monotonic() >= admission_deadline:
|
|
647
|
+
# A generic elapsed deadline is not a proof of local non-write.
|
|
648
|
+
# Without an exact original ProtocolNotSent or pre-IPC gate,
|
|
649
|
+
# retain the original submitted-unknown fence.
|
|
650
|
+
raise NativeEffectIndeterminate("original Codex admission deadline expired")
|
|
651
|
+
if client is None or not daemon.ready.is_set():
|
|
652
|
+
return _record_original_codex_local_refusal(daemon, worker_id, record, source, token,
|
|
653
|
+
stage="not-ready", reason="Codex app-server not ready",
|
|
654
|
+
)
|
|
655
|
+
try:
|
|
656
|
+
caller_config = _original_caller_config(worker_id)
|
|
657
|
+
except Exception as error:
|
|
658
|
+
return _record_original_codex_local_refusal(daemon, worker_id, record, source, token, stage="shell-env",
|
|
659
|
+
reason=f"managed shell environment unavailable: {type(error).__name__}",
|
|
660
|
+
)
|
|
661
|
+
key = hashlib.sha256(("activate" + "\x00" + token).encode()).hexdigest()
|
|
662
|
+
args = record.get("createArgs") or {}
|
|
663
|
+
start_params = {
|
|
664
|
+
"cwd": record["cwd"],
|
|
665
|
+
"sandbox": args.get("sandbox"),
|
|
666
|
+
"approvalPolicy": args.get("approvalPolicy"),
|
|
667
|
+
"serviceName": "ccx", "threadSource": source,
|
|
668
|
+
"config": _with_default_effort(caller_config, args),
|
|
669
|
+
}
|
|
670
|
+
if args.get("model"):
|
|
671
|
+
start_params["model"] = args["model"]
|
|
672
|
+
fork = record.get("forkAnchor")
|
|
673
|
+
method = "thread/fork" if fork is not None else "thread/start"
|
|
674
|
+
request_params = dict(start_params)
|
|
675
|
+
if fork is not None:
|
|
676
|
+
request_params.pop("serviceName") # Not in pinned ThreadForkParams.
|
|
677
|
+
request_params["threadId"] = fork["sourceNativeThreadId"]
|
|
678
|
+
request_params["lastTurnId"] = fork["lastTurnId"]
|
|
679
|
+
request_params["excludeTurns"] = True
|
|
680
|
+
original = {
|
|
681
|
+
"workerId": worker_id, "engine": "codex",
|
|
682
|
+
"generation": store.generation,
|
|
683
|
+
"kind": "activate", "token": token,
|
|
684
|
+
"state": "submitted-unknown", "source": source,
|
|
685
|
+
"createSha256": record["createSha256"],
|
|
686
|
+
"daemonInstanceId": daemon.instance_id,
|
|
687
|
+
"daemonPid": os.getpid(),
|
|
688
|
+
"appServerPid": client.process.pid,
|
|
689
|
+
**({"nativeMethod": method, "forkAnchor": fork} if fork is not None else {}),
|
|
690
|
+
}
|
|
691
|
+
hold_key = (store.generation, worker_id, token)
|
|
692
|
+
held: dict[str, Any] = {
|
|
693
|
+
"original": original, "candidate": None, "rejection": None,
|
|
694
|
+
"unsafe": False, "error": None,
|
|
695
|
+
}
|
|
696
|
+
# Total order against the janitor's idle-exit decision. Reset linger
|
|
697
|
+
# at admission, not when (possibly slow) provider reply arrives.
|
|
698
|
+
with daemon.lock:
|
|
699
|
+
if daemon.stopping or daemon.stop.is_set():
|
|
700
|
+
return _record_original_codex_local_refusal(daemon, worker_id, record, source, token,
|
|
701
|
+
stage="daemon-stopping", reason="Codex daemon is stopping; no native start",
|
|
702
|
+
)
|
|
703
|
+
with daemon._native_start_hold_lock:
|
|
704
|
+
if hold_key in daemon._native_start_held or hold_key in daemon._native_start_unsafe:
|
|
705
|
+
raise NativeEffectIndeterminate(
|
|
706
|
+
"same original Codex native start already recorded; no replay"
|
|
707
|
+
)
|
|
708
|
+
full = len(daemon._native_start_held) >= _CODEX_NATIVE_START_HELD_LIMIT
|
|
709
|
+
if not full:
|
|
710
|
+
daemon._native_start_held[hold_key] = held
|
|
711
|
+
if full:
|
|
712
|
+
return _record_original_codex_local_refusal(daemon, worker_id, record, source, token,
|
|
713
|
+
stage="capacity", reason=(
|
|
714
|
+
"Codex native start retention full; new provider submission refused"
|
|
715
|
+
),
|
|
716
|
+
)
|
|
717
|
+
daemon.empty_since = None
|
|
718
|
+
|
|
719
|
+
def capture_native_result(answer) -> None:
|
|
720
|
+
# Runs on the app-server reader (or immediately upon callback
|
|
721
|
+
# registration for a fast reply). Do not call any provider RPC.
|
|
722
|
+
try:
|
|
723
|
+
provider_rejection = answer.original_provider_rejection()
|
|
724
|
+
if provider_rejection is not None:
|
|
725
|
+
message = provider_rejection["message"]
|
|
726
|
+
from ccx.store.store import _digest
|
|
727
|
+
rejected = {
|
|
728
|
+
**provider_rejection,
|
|
729
|
+
"messageSha256": hashlib.sha256(message.encode("utf-8")).hexdigest(),
|
|
730
|
+
"appServerPid": original["appServerPid"],
|
|
731
|
+
"nativeErrorSha256": _digest({
|
|
732
|
+
"code": provider_rejection["code"],
|
|
733
|
+
"message": message,
|
|
734
|
+
"requestId": provider_rejection["requestId"],
|
|
735
|
+
"appServerPid": original["appServerPid"],
|
|
736
|
+
}),
|
|
737
|
+
}
|
|
738
|
+
# Retain exact original coded provider reply before any
|
|
739
|
+
# claim/marker fsync, just as for an accepted candidate.
|
|
740
|
+
# A numeric code on a local transport exception is not
|
|
741
|
+
# proof of provider rejection.
|
|
742
|
+
with daemon._native_start_hold_lock:
|
|
743
|
+
if daemon._native_start_held.get(hold_key) is not held:
|
|
744
|
+
raise NativeEffectIndeterminate(
|
|
745
|
+
"original provider rejection retention replaced"
|
|
746
|
+
)
|
|
747
|
+
held["rejection"] = rejected
|
|
748
|
+
daemon._save_held_codex_candidate(hold_key)
|
|
749
|
+
return
|
|
750
|
+
native = answer.result()
|
|
751
|
+
thread = native.get("thread") if isinstance(native, dict) else None
|
|
752
|
+
candidate_id = thread.get("id") if isinstance(thread, dict) else None
|
|
753
|
+
if not isinstance(candidate_id, str) or not candidate_id:
|
|
754
|
+
with daemon._native_start_hold_lock:
|
|
755
|
+
if daemon._native_start_held.get(hold_key) is held:
|
|
756
|
+
del daemon._native_start_held[hold_key]
|
|
757
|
+
return # no positive candidate; provider rejection is handled above
|
|
758
|
+
if fork is not None and candidate_id == fork["sourceNativeThreadId"]:
|
|
759
|
+
raise ValueError("native fork response reused its source thread ID")
|
|
760
|
+
if len(candidate_id.encode("utf-8")) > _CODEX_NATIVE_START_ID_MAX_BYTES:
|
|
761
|
+
raise ValueError("provider native thread id exceeds retained bound")
|
|
762
|
+
# Fork responses may include enormous copied history. Hash and
|
|
763
|
+
# bound a shallow response without serializing those turns;
|
|
764
|
+
# independent thread/read is the only history/point proof.
|
|
765
|
+
reply = (dict(native, thread={k: v for k, v in thread.items()
|
|
766
|
+
if k != "turns"})
|
|
767
|
+
if fork is not None else native)
|
|
768
|
+
raw = json.dumps(reply, sort_keys=True,
|
|
769
|
+
separators=(",", ":"), allow_nan=False).encode()
|
|
770
|
+
if len(raw) > _CODEX_NATIVE_START_REPLY_MAX_BYTES:
|
|
771
|
+
raise ValueError("provider native reply exceeds retained bound")
|
|
772
|
+
candidate = {
|
|
773
|
+
"threadId": candidate_id,
|
|
774
|
+
# Source is the original immutable request, not provider
|
|
775
|
+
# attribution. Independent thread/read is still mandatory.
|
|
776
|
+
"submittedSource": source,
|
|
777
|
+
"appServerPid": original["appServerPid"],
|
|
778
|
+
"nativeReplySha256": hashlib.sha256(raw).hexdigest(),
|
|
779
|
+
}
|
|
780
|
+
with daemon._native_start_hold_lock:
|
|
781
|
+
if daemon._native_start_held.get(hold_key) is not held:
|
|
782
|
+
raise NativeEffectIndeterminate(
|
|
783
|
+
"original reply retention was replaced before disk save"
|
|
784
|
+
)
|
|
785
|
+
held["candidate"] = candidate # before any filesystem call
|
|
786
|
+
daemon._save_held_codex_candidate(hold_key)
|
|
787
|
+
except (ProtocolError, ProtocolTransportError) as error:
|
|
788
|
+
# No confirmed positive reply. Never infer rejection from
|
|
789
|
+
# a timeout, missing code or transport loss.
|
|
790
|
+
with daemon._native_start_hold_lock:
|
|
791
|
+
if daemon._native_start_held.get(hold_key) is held:
|
|
792
|
+
del daemon._native_start_held[hold_key]
|
|
793
|
+
print(
|
|
794
|
+
f"ccx codexd: Codex original start outcome unconfirmed "
|
|
795
|
+
f"for {worker_id}: {error}",
|
|
796
|
+
file=sys.stderr, flush=True,
|
|
797
|
+
)
|
|
798
|
+
except Exception as error:
|
|
799
|
+
detail = f"{type(error).__name__}: {error}"[:250]
|
|
800
|
+
with daemon._native_start_hold_lock:
|
|
801
|
+
_mark_unsafe_native_start_locked(daemon, hold_key, held, detail)
|
|
802
|
+
print(
|
|
803
|
+
f"ccx codexd: cannot retain original native reply safely "
|
|
804
|
+
f"for {worker_id}: {detail}",
|
|
805
|
+
file=sys.stderr, flush=True,
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
pending = None
|
|
809
|
+
local_reason = None
|
|
810
|
+
local_marker_saved = False
|
|
811
|
+
local_marker_error = None
|
|
812
|
+
try:
|
|
813
|
+
with store.lease():
|
|
814
|
+
with store._gen("runtime-metadata", "codex-native-effects",
|
|
815
|
+
worker_id, create=True) as directory:
|
|
816
|
+
with _flock_at(directory, key + ".lock", exclusive=True,
|
|
817
|
+
create=True, timeout=.5):
|
|
818
|
+
if _read_json(directory, key + ".json") is not None:
|
|
819
|
+
raise NativeEffectIndeterminate(
|
|
820
|
+
"original Codex native start already submitted; "
|
|
821
|
+
"no replay after uncertain provider response"
|
|
822
|
+
)
|
|
823
|
+
_write_json(directory, key + ".json", original)
|
|
824
|
+
store.revalidate()
|
|
825
|
+
# sole native IPC write, after both original fences.
|
|
826
|
+
try:
|
|
827
|
+
pending = client.begin_request(
|
|
828
|
+
method, request_params,
|
|
829
|
+
write_deadline=admission_deadline,
|
|
830
|
+
)
|
|
831
|
+
except ProtocolNotSent as transport_error:
|
|
832
|
+
# AppServerClient knows that this exact
|
|
833
|
+
# request wrote zero bytes. Unlike a generic
|
|
834
|
+
# ProtocolError, numeric errno, timeout or partial
|
|
835
|
+
# ProtocolTransportError, it is attributable to
|
|
836
|
+
# the current thread/start attempt. The original
|
|
837
|
+
# per-ID effect lock is still held; no second
|
|
838
|
+
# writer can overtake this terminal marker.
|
|
839
|
+
if (type(client) is not AppServerClient
|
|
840
|
+
or type(transport_error) is not ProtocolNotSent
|
|
841
|
+
or transport_error.method != method):
|
|
842
|
+
raise
|
|
843
|
+
local_reason = (
|
|
844
|
+
f"Codex original {method} not submitted: "
|
|
845
|
+
+ transport_error.message
|
|
846
|
+
)
|
|
847
|
+
proof = _native_codex_local_proof(daemon, original, local_reason, "transport-zero-bytes",
|
|
848
|
+
)
|
|
849
|
+
try:
|
|
850
|
+
_write_json(directory, key + ".json", {
|
|
851
|
+
**original, "nativeNotSubmitted": proof,
|
|
852
|
+
})
|
|
853
|
+
local_marker_saved = True
|
|
854
|
+
except Exception as persistence_error:
|
|
855
|
+
local_marker_error = (
|
|
856
|
+
f"{type(persistence_error).__name__}: "
|
|
857
|
+
f"{persistence_error}"
|
|
858
|
+
)
|
|
859
|
+
finally:
|
|
860
|
+
if pending is not None:
|
|
861
|
+
# Also register after a post-dispatch generation/lease failure:
|
|
862
|
+
# a delivered provider reply must remain capturable.
|
|
863
|
+
pending.add_done_callback(capture_native_result)
|
|
864
|
+
else:
|
|
865
|
+
# No positive original PendingRequest exists to retain.
|
|
866
|
+
with daemon._native_start_hold_lock:
|
|
867
|
+
if daemon._native_start_held.get(hold_key) is held:
|
|
868
|
+
del daemon._native_start_held[hold_key]
|
|
869
|
+
if local_reason is not None:
|
|
870
|
+
result = {
|
|
871
|
+
"ok": False, "workerId": worker_id,
|
|
872
|
+
"localRefusalReason": local_reason,
|
|
873
|
+
"localRefusalProven": local_marker_saved,
|
|
874
|
+
"error": (local_reason if local_marker_saved
|
|
875
|
+
else local_reason + "; original refusal marker unavailable: "
|
|
876
|
+
+ str(local_marker_error)),
|
|
877
|
+
}
|
|
878
|
+
if local_marker_saved:
|
|
879
|
+
try:
|
|
880
|
+
sealed = store.reconcile_codex_local_refusal(
|
|
881
|
+
f"codex:{worker_id}"
|
|
882
|
+
)
|
|
883
|
+
if sealed is None or sealed.get("reason") != local_reason:
|
|
884
|
+
raise RuntimeError("native zero-byte refusal did not seal")
|
|
885
|
+
except Exception as error:
|
|
886
|
+
result["error"] = (
|
|
887
|
+
local_reason + "; durable marker retained, claim pending: "
|
|
888
|
+
+ str(error)
|
|
889
|
+
)
|
|
890
|
+
return result
|
|
891
|
+
return {
|
|
892
|
+
"ok": True, "queued": True,
|
|
893
|
+
"workerId": worker_id, "engine": "codex",
|
|
894
|
+
"source": source, "submissionId": token,
|
|
895
|
+
# Transport admission, never native thread binding.
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _observe_original_codex_thread(daemon, message: dict[str, Any]) -> dict[str, Any]:
|
|
900
|
+
"""Observe an exact original candidate or replay its coded refusal.
|
|
901
|
+
|
|
902
|
+
A provider thread with the same source tag but no local one-shot reply
|
|
903
|
+
candidate is not attributable to this dispatch. No thread/list scans,
|
|
904
|
+
native resume, or speculative recreation are allowed.
|
|
905
|
+
"""
|
|
906
|
+
from ccx.store.root import NativeEffectIndeterminate, _read_json, _flock_at, _HASH
|
|
907
|
+
from ccx.store.store import _digest
|
|
908
|
+
import hashlib
|
|
909
|
+
store = daemon._uuid_store
|
|
910
|
+
owner = store.resolve(message.get("workerId"))
|
|
911
|
+
if owner.engine != "codex":
|
|
912
|
+
raise NativeEffectIndeterminate("Codex original full worker UUID required")
|
|
913
|
+
wid = owner.worker_id
|
|
914
|
+
source = f"ccx-worker:{wid}"
|
|
915
|
+
activation = store.native_activation(owner.worker)
|
|
916
|
+
token = message.get("submissionId")
|
|
917
|
+
if (activation.get("source") != source
|
|
918
|
+
or activation.get("submissionId") != token
|
|
919
|
+
or not isinstance(token, str) or not token):
|
|
920
|
+
raise NativeEffectIndeterminate("original submitted Codex activation unproven")
|
|
921
|
+
if activation.get("state") == "rejected":
|
|
922
|
+
sealed = store.reconcile_codex_native_rejection(owner.worker)
|
|
923
|
+
if sealed is None:
|
|
924
|
+
raise NativeEffectIndeterminate("original rejected claim lost provider proof")
|
|
925
|
+
return {"ok": True, **sealed}
|
|
926
|
+
if activation.get("state") == "local-refused":
|
|
927
|
+
local = store.reconcile_codex_local_refusal(owner.worker)
|
|
928
|
+
if local is None:
|
|
929
|
+
raise NativeEffectIndeterminate("original local-refused claim lost proof")
|
|
930
|
+
return {"ok": True, **local}
|
|
931
|
+
# For submitted-unknown first allow the held reply to persist. An
|
|
932
|
+
# unconditional effect read here could contend with the callback
|
|
933
|
+
# before its original result was saved, or change original tamper
|
|
934
|
+
# diagnostics. Marker-to-claim recovery follows the held step below.
|
|
935
|
+
if activation.get("state") != "submitted-unknown":
|
|
936
|
+
raise NativeEffectIndeterminate("original submitted Codex activation unproven")
|
|
937
|
+
record = store.record(owner.worker)
|
|
938
|
+
native = record.get("native") or {}
|
|
939
|
+
if (record.get("fingerprintVersion") != 2
|
|
940
|
+
or record.get("terminalOnly") or record.get("nativeCreate")
|
|
941
|
+
or native.get("threadId") is not None
|
|
942
|
+
or native.get("nativeState") != "not-started-proven"):
|
|
943
|
+
raise NativeEffectIndeterminate("original Codex first native record not eligible")
|
|
944
|
+
# Compare the immutable source/preimage, not mutable current display
|
|
945
|
+
# name, short, provider thread id, or reconstructed operation hash.
|
|
946
|
+
with store.lease():
|
|
947
|
+
original_owner = store._owner(wid)
|
|
948
|
+
claim = store._claim(wid)
|
|
949
|
+
if not isinstance(original_owner, dict) or not isinstance(claim, dict):
|
|
950
|
+
raise NativeEffectIndeterminate("Codex owner/create claim missing")
|
|
951
|
+
fork = record.get("forkAnchor")
|
|
952
|
+
if fork is not None:
|
|
953
|
+
from ccx.store.store import fingerprint_v2, codex_fork_lineage
|
|
954
|
+
lineage = record.get("forkedFrom")
|
|
955
|
+
if (not isinstance(fork, dict) or fork.get("engine") != "codex"
|
|
956
|
+
or fork.get("cwd") != record.get("cwd")
|
|
957
|
+
or not isinstance(fork.get("createArgs"), dict)
|
|
958
|
+
or fingerprint_v2(
|
|
959
|
+
engine="codex", cwd=fork["cwd"],
|
|
960
|
+
create_args=fork["createArgs"], fork_anchor=fork,
|
|
961
|
+
) != record["createSha256"]
|
|
962
|
+
or not isinstance(lineage, dict)
|
|
963
|
+
or lineage != codex_fork_lineage(fork, child_id=wid)):
|
|
964
|
+
raise NativeEffectIndeterminate("Codex original fork lineage/point changed")
|
|
965
|
+
store._check_original_create_intent(
|
|
966
|
+
original_owner, engine="codex", worker_id=wid,
|
|
967
|
+
fingerprint=record["createSha256"], native={}, anchor=fork,
|
|
968
|
+
)
|
|
969
|
+
if (claim.get("createSha256") != record["createSha256"]
|
|
970
|
+
or claim.get("fingerprintVersion") != 2
|
|
971
|
+
or claim.get("nativeBinding") != {}
|
|
972
|
+
or claim.get("forkAnchor") != fork):
|
|
973
|
+
raise NativeEffectIndeterminate("Codex original create hash mismatch")
|
|
974
|
+
key = hashlib.sha256(("activate" + "\x00" + token).encode()).hexdigest()
|
|
975
|
+
# Status handler may retry only an in-memory original positive native
|
|
976
|
+
# reply whose callback met a recognized transient disk/lock failure.
|
|
977
|
+
# Outside the owner lease: never nest a second lease across a native
|
|
978
|
+
# observation, and never invoke thread/start, resume or list here.
|
|
979
|
+
held = daemon._save_held_codex_candidate((store.generation, wid, token))
|
|
980
|
+
if held["status"] == "saved":
|
|
981
|
+
# A status handler may have just sealed the original rejection.
|
|
982
|
+
# The same exact provider proof is also available offline from
|
|
983
|
+
# its marker/claim; no native thread/read or native start needed.
|
|
984
|
+
sealed = store.reconcile_codex_native_rejection(owner.worker)
|
|
985
|
+
if sealed is not None:
|
|
986
|
+
return {"ok": True, **sealed}
|
|
987
|
+
if held["status"] == "unsafe":
|
|
988
|
+
raise NativeEffectIndeterminate(
|
|
989
|
+
"original Codex candidate persistence unsafe: " + held["error"]
|
|
990
|
+
)
|
|
991
|
+
if held["status"] == "transient":
|
|
992
|
+
return {"ok": True, "status": "indeterminate", "workerId": wid,
|
|
993
|
+
"candidatePersistence": "transient", "error": held["error"]}
|
|
994
|
+
with store.lease():
|
|
995
|
+
try:
|
|
996
|
+
with store._gen("runtime-metadata", "codex-native-effects",
|
|
997
|
+
wid) as directory:
|
|
998
|
+
with _flock_at(directory, key + ".lock", exclusive=False,
|
|
999
|
+
create=False, timeout=.5):
|
|
1000
|
+
saved = _read_json(directory, key + ".json")
|
|
1001
|
+
except FileNotFoundError:
|
|
1002
|
+
return {"ok": True, "status": "indeterminate", "workerId": wid}
|
|
1003
|
+
if saved is None:
|
|
1004
|
+
return {"ok": True, "status": "indeterminate", "workerId": wid}
|
|
1005
|
+
if saved.get("nativeNotSubmitted") is not None:
|
|
1006
|
+
local = store.reconcile_codex_local_refusal(owner.worker)
|
|
1007
|
+
if local is None:
|
|
1008
|
+
raise NativeEffectIndeterminate("original local refusal marker unproven")
|
|
1009
|
+
return {"ok": True, **local}
|
|
1010
|
+
if saved.get("nativeRejected") is not None:
|
|
1011
|
+
sealed = store.reconcile_codex_native_rejection(owner.worker)
|
|
1012
|
+
if sealed is None:
|
|
1013
|
+
raise NativeEffectIndeterminate("original rejected marker not reconciled")
|
|
1014
|
+
return {"ok": True, **sealed}
|
|
1015
|
+
if (not isinstance(saved, dict) or any(saved.get(k) != v for k, v in {
|
|
1016
|
+
"workerId": wid, "engine": "codex", "generation": store.generation,
|
|
1017
|
+
"kind": "activate", "token": token, "state": "submitted-unknown",
|
|
1018
|
+
"source": source, "createSha256": record["createSha256"],
|
|
1019
|
+
}.items()) or (
|
|
1020
|
+
(saved.get("nativeMethod") != "thread/fork"
|
|
1021
|
+
or saved.get("forkAnchor") != fork)
|
|
1022
|
+
if fork is not None else
|
|
1023
|
+
("nativeMethod" in saved or "forkAnchor" in saved)
|
|
1024
|
+
) or not isinstance(saved.get("daemonInstanceId"), str)
|
|
1025
|
+
or not saved["daemonInstanceId"]
|
|
1026
|
+
or type(saved.get("daemonPid")) is not int
|
|
1027
|
+
or saved["daemonPid"] <= 0
|
|
1028
|
+
or type(saved.get("appServerPid")) is not int
|
|
1029
|
+
or saved["appServerPid"] <= 0):
|
|
1030
|
+
raise NativeEffectIndeterminate("original Codex one-shot marker invalid")
|
|
1031
|
+
if saved.get("nativeRejected") is not None:
|
|
1032
|
+
raise NativeEffectIndeterminate("native rejected marker contradicts candidate")
|
|
1033
|
+
candidate = saved.get("nativeCandidate")
|
|
1034
|
+
if candidate is None:
|
|
1035
|
+
return {"ok": True, "status": "indeterminate", "workerId": wid}
|
|
1036
|
+
if (not isinstance(candidate, dict)
|
|
1037
|
+
or not isinstance(candidate.get("threadId"), str)
|
|
1038
|
+
or not candidate["threadId"]
|
|
1039
|
+
or (fork is not None and candidate["threadId"] == fork["sourceNativeThreadId"])
|
|
1040
|
+
or candidate.get("submittedSource") != source
|
|
1041
|
+
or candidate.get("appServerPid") != saved["appServerPid"]
|
|
1042
|
+
or not isinstance(candidate.get("nativeReplySha256"), str)
|
|
1043
|
+
or not _HASH.fullmatch(candidate["nativeReplySha256"])):
|
|
1044
|
+
raise NativeEffectIndeterminate("Codex candidate lacks original effect proof")
|
|
1045
|
+
client = daemon.client
|
|
1046
|
+
if client is None or not daemon.ready.is_set():
|
|
1047
|
+
raise NativeEffectIndeterminate("Codex app-server not available to inspect")
|
|
1048
|
+
if fork is None:
|
|
1049
|
+
observed = client.request("thread/read", {
|
|
1050
|
+
"threadId": candidate["threadId"], "includeTurns": False,
|
|
1051
|
+
}, timeout=2)
|
|
1052
|
+
thread = observed.get("thread") if isinstance(observed, dict) else None
|
|
1053
|
+
else:
|
|
1054
|
+
from ccx.codex.history import read_original_thread
|
|
1055
|
+
try:
|
|
1056
|
+
thread = read_original_thread(
|
|
1057
|
+
client.request, candidate["threadId"],
|
|
1058
|
+
deadline=time.monotonic() + 2, limit=1, newest_first=True,
|
|
1059
|
+
expected_source=source,
|
|
1060
|
+
)
|
|
1061
|
+
except ValueError as error:
|
|
1062
|
+
raise NativeEffectIndeterminate(
|
|
1063
|
+
"independent Codex fork read did not prove original source"
|
|
1064
|
+
) from error
|
|
1065
|
+
if (not isinstance(thread, dict)
|
|
1066
|
+
or thread.get("id") != candidate["threadId"]
|
|
1067
|
+
or thread.get("threadSource") != source):
|
|
1068
|
+
raise NativeEffectIndeterminate(
|
|
1069
|
+
"independent Codex thread/read did not prove original candidate source"
|
|
1070
|
+
)
|
|
1071
|
+
if fork is not None:
|
|
1072
|
+
turns = thread.get("turns")
|
|
1073
|
+
last = turns[-1] if isinstance(turns, list) and turns else None
|
|
1074
|
+
if (thread.get("forkedFromId") != fork["sourceNativeThreadId"]
|
|
1075
|
+
or not isinstance(last, dict)
|
|
1076
|
+
or last.get("id") != fork["lastTurnId"]
|
|
1077
|
+
or last.get("status") != "completed"):
|
|
1078
|
+
raise NativeEffectIndeterminate(
|
|
1079
|
+
"independent Codex fork thread/read lacks original source/point proof"
|
|
1080
|
+
)
|
|
1081
|
+
evidence = {
|
|
1082
|
+
"workerId": wid, "threadSource": source,
|
|
1083
|
+
"threadId": candidate["threadId"], "submissionId": token,
|
|
1084
|
+
"daemonPid": saved["daemonPid"],
|
|
1085
|
+
"appServerPid": saved["appServerPid"],
|
|
1086
|
+
}
|
|
1087
|
+
if fork is not None:
|
|
1088
|
+
# These facts come from one independent native read; the original
|
|
1089
|
+
# candidate stays the ordinary response/thread/PID/hash shape.
|
|
1090
|
+
evidence.update(
|
|
1091
|
+
nativeMethod="thread/fork", forkAnchor=fork,
|
|
1092
|
+
forkedFromId=fork["sourceNativeThreadId"],
|
|
1093
|
+
lastTurnId=fork["lastTurnId"], lastTurnStatus="completed",
|
|
1094
|
+
nativeReplySha256=candidate["nativeReplySha256"],
|
|
1095
|
+
)
|
|
1096
|
+
store.revalidate()
|
|
1097
|
+
return {
|
|
1098
|
+
"accepted": True, "workerId": wid, "engine": "codex",
|
|
1099
|
+
"source": source, "submissionId": token,
|
|
1100
|
+
"nativeEvidenceSha256": _digest(evidence),
|
|
1101
|
+
"nativeBinding": {"threadId": candidate["threadId"]},
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
def _original_full_uuid_native(daemon, message: dict[str, Any]) -> dict[str, Any]:
|
|
1106
|
+
"""One generation-bound Codex native action through the owning daemon.
|
|
1107
|
+
|
|
1108
|
+
A client must have durably fsynced the original generation-bound
|
|
1109
|
+
native intent before this daemon can touch the provider. The daemon
|
|
1110
|
+
independently gates exact full owner/source/thread/receipt and
|
|
1111
|
+
publishes a second per-operation local single-dispatch marker before
|
|
1112
|
+
the app-server write: a lost socket response never replays native.
|
|
1113
|
+
"""
|
|
1114
|
+
from ccx.store.root import NativeEffectIndeterminate, _read_json, _write_json, _flock_at
|
|
1115
|
+
from ccx.store.store import _digest
|
|
1116
|
+
from ccx.codex.delivery import read_receipt as semantic_receipt
|
|
1117
|
+
from ccx.store.identity import EngineMismatch
|
|
1118
|
+
import hashlib
|
|
1119
|
+
store = daemon._uuid_store
|
|
1120
|
+
store.revalidate()
|
|
1121
|
+
owner = store.resolve(message.get("workerId"))
|
|
1122
|
+
if owner.engine != "codex":
|
|
1123
|
+
raise EngineMismatch("Codex daemon needs exact owned Codex full UUID")
|
|
1124
|
+
worker_id = owner.worker_id
|
|
1125
|
+
action = message.get("op")
|
|
1126
|
+
record = store.record(owner.worker)
|
|
1127
|
+
native = record.get("native") or {}
|
|
1128
|
+
if not isinstance(native, dict):
|
|
1129
|
+
raise RuntimeError("Codex original native binding malformed")
|
|
1130
|
+
# First native activation still issues only a freshly minted UUID tag.
|
|
1131
|
+
source = (f"ccx-worker:{worker_id}" if action == "native-activate"
|
|
1132
|
+
else store.codex_source(owner.worker))
|
|
1133
|
+
if action == "native-observe":
|
|
1134
|
+
if native.get("threadSource") != source:
|
|
1135
|
+
raise RuntimeError("native Codex thread source not bound")
|
|
1136
|
+
thread_id = native.get("threadId")
|
|
1137
|
+
turn_id = message.get("turnId")
|
|
1138
|
+
delivery_id = message.get("deliveryId")
|
|
1139
|
+
if (not isinstance(thread_id,str) or not isinstance(turn_id,str)
|
|
1140
|
+
or not turn_id or not isinstance(delivery_id,str)
|
|
1141
|
+
or not delivery_id):
|
|
1142
|
+
raise RuntimeError("native completion requires original thread, turn and delivery")
|
|
1143
|
+
semantic = semantic_receipt(owner.worker, delivery_id)
|
|
1144
|
+
original_effect = store.read_receipt(
|
|
1145
|
+
owner.worker, "codex-send", delivery_id,
|
|
1146
|
+
)
|
|
1147
|
+
if (not isinstance(semantic,dict)
|
|
1148
|
+
or semantic.get("state") != "accepted"
|
|
1149
|
+
or semantic.get("turnId") != turn_id
|
|
1150
|
+
or not isinstance(original_effect,dict)
|
|
1151
|
+
or original_effect.get("state") != "accepted"
|
|
1152
|
+
or original_effect.get("workerId") != worker_id):
|
|
1153
|
+
raise RuntimeError(
|
|
1154
|
+
"native turn completion has no exact original accepted"
|
|
1155
|
+
" delivery and effect receipt"
|
|
1156
|
+
)
|
|
1157
|
+
deadline = time.monotonic() + _native_history_budget(message)
|
|
1158
|
+
from ccx.codex.history import read_original_thread
|
|
1159
|
+
thread = read_original_thread(
|
|
1160
|
+
daemon.client.request, thread_id, deadline=deadline,
|
|
1161
|
+
exact_turn_id=turn_id, expected_source=source,
|
|
1162
|
+
)
|
|
1163
|
+
if (not isinstance(thread,dict)
|
|
1164
|
+
or thread.get("id") != thread_id
|
|
1165
|
+
or thread.get("threadSource") != source):
|
|
1166
|
+
raise RuntimeError("native thread/read has no original full UUID source")
|
|
1167
|
+
matches = [
|
|
1168
|
+
turn for turn in (thread.get("turns") or [])
|
|
1169
|
+
if isinstance(turn,dict) and turn.get("id") == turn_id
|
|
1170
|
+
]
|
|
1171
|
+
if len(matches) != 1:
|
|
1172
|
+
return {"ok":True, "status":"pending", "workerId":worker_id}
|
|
1173
|
+
native_status = matches[0].get("status")
|
|
1174
|
+
if native_status in ("failed", "interrupted", "cancelled"):
|
|
1175
|
+
# thread/read can report a still-running turn as interrupted;
|
|
1176
|
+
# trust it once this runtime no longer runs that turn.
|
|
1177
|
+
with daemon.lock:
|
|
1178
|
+
live = daemon.workers.get(owner.worker_id)
|
|
1179
|
+
if (live is not None and live.loaded and
|
|
1180
|
+
live.record["lifecycle"].get("activeTurnId") == turn_id):
|
|
1181
|
+
return {"ok":True, "status":"pending", "workerId":worker_id}
|
|
1182
|
+
return {"ok":True, "status":native_status,
|
|
1183
|
+
"workerId":worker_id, "turnId":turn_id,
|
|
1184
|
+
"deliveryId":delivery_id,
|
|
1185
|
+
"error":matches[0].get("error")}
|
|
1186
|
+
if native_status != "completed":
|
|
1187
|
+
return {"ok":True, "status":"pending", "workerId":worker_id}
|
|
1188
|
+
# A completed turn without an agent message is still completed.
|
|
1189
|
+
# Phase-aware final_answer wins over later phase-less text;
|
|
1190
|
+
# commentary alone is not a final reply.
|
|
1191
|
+
selected = select_report_for_turn(
|
|
1192
|
+
matches[0], live_report=record.get("lastReport"))
|
|
1193
|
+
text = selected["text"] if selected is not None else ""
|
|
1194
|
+
return {
|
|
1195
|
+
"ok":True, "status":"completed", "workerId":worker_id,
|
|
1196
|
+
"engine":"codex", "nativeSource":source,
|
|
1197
|
+
"threadId":thread_id, "turnId":turn_id,
|
|
1198
|
+
"deliveryId":delivery_id, "text":text,
|
|
1199
|
+
"nativeEvidenceSha256":_digest({
|
|
1200
|
+
"source":source, "threadId":thread_id,
|
|
1201
|
+
"turnId":turn_id,"status":"completed",
|
|
1202
|
+
"text":text,
|
|
1203
|
+
}),
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if action == "native-activate":
|
|
1207
|
+
activation = store.native_activation(owner.worker)
|
|
1208
|
+
if (activation.get("state") != "submitted-unknown"
|
|
1209
|
+
or activation.get("source") != source
|
|
1210
|
+
or activation.get("submissionId") != message.get("submissionId")
|
|
1211
|
+
or native.get("threadId") is not None):
|
|
1212
|
+
raise RuntimeError("original Codex thread/create submitted claim unproven")
|
|
1213
|
+
token = activation["submissionId"]
|
|
1214
|
+
if (record.get("fingerprintVersion") != 2
|
|
1215
|
+
or record.get("terminalOnly") or record.get("nativeCreate")
|
|
1216
|
+
or native.get("nativeState") != "not-started-proven"):
|
|
1217
|
+
raise NativeEffectIndeterminate("original first Codex native record invalid")
|
|
1218
|
+
with store.lease():
|
|
1219
|
+
owner_entry, creation_claim = store._owner(worker_id), store._claim(worker_id)
|
|
1220
|
+
if not isinstance(owner_entry, dict) or not isinstance(creation_claim, dict):
|
|
1221
|
+
raise NativeEffectIndeterminate("original Codex create claim missing")
|
|
1222
|
+
fork = record.get("forkAnchor")
|
|
1223
|
+
if fork is not None:
|
|
1224
|
+
from ccx.store.store import fingerprint_v2, codex_fork_lineage
|
|
1225
|
+
lineage = record.get("forkedFrom")
|
|
1226
|
+
if (not isinstance(fork, dict) or fork.get("engine") != "codex"
|
|
1227
|
+
or fork.get("cwd") != record.get("cwd")
|
|
1228
|
+
or not isinstance(fork.get("createArgs"), dict)
|
|
1229
|
+
or fingerprint_v2(
|
|
1230
|
+
engine="codex", cwd=fork["cwd"],
|
|
1231
|
+
create_args=fork["createArgs"], fork_anchor=fork,
|
|
1232
|
+
) != record["createSha256"]
|
|
1233
|
+
or not isinstance(lineage, dict)
|
|
1234
|
+
or lineage != codex_fork_lineage(fork, child_id=worker_id)):
|
|
1235
|
+
raise NativeEffectIndeterminate("original Codex frozen fork claim invalid")
|
|
1236
|
+
store._check_original_create_intent(
|
|
1237
|
+
owner_entry, engine="codex", worker_id=worker_id,
|
|
1238
|
+
fingerprint=record["createSha256"], native={}, anchor=fork,
|
|
1239
|
+
)
|
|
1240
|
+
if (creation_claim.get("createSha256") != record["createSha256"]
|
|
1241
|
+
or creation_claim.get("nativeBinding") != {}
|
|
1242
|
+
or creation_claim.get("fingerprintVersion") != 2
|
|
1243
|
+
or creation_claim.get("forkAnchor") != fork):
|
|
1244
|
+
raise NativeEffectIndeterminate("original Codex create hash changed")
|
|
1245
|
+
# Absolute elapsed budget from the CLI's original <=2-second
|
|
1246
|
+
# admission. Never reset it after this daemon's generation/effect
|
|
1247
|
+
# lease work. Older internal callers receive a bounded local
|
|
1248
|
+
# budget; no proof of no-submission is inferred from a timeout.
|
|
1249
|
+
admission_deadline = message.get("nativeAdmissionDeadline")
|
|
1250
|
+
if admission_deadline is not None:
|
|
1251
|
+
import math
|
|
1252
|
+
if (type(admission_deadline) not in (int, float)
|
|
1253
|
+
or not math.isfinite(admission_deadline)
|
|
1254
|
+
or admission_deadline - time.monotonic() > 2.0):
|
|
1255
|
+
raise NativeEffectIndeterminate("invalid Codex admission deadline")
|
|
1256
|
+
return daemon._submit_original_codex_thread(
|
|
1257
|
+
worker_id, record, source, token,
|
|
1258
|
+
admission_deadline=admission_deadline,
|
|
1259
|
+
)
|
|
1260
|
+
elif action == "native-send":
|
|
1261
|
+
from ccx.codex.delivery import intent_spec, requested_mode, mark_owned, mark_submitted
|
|
1262
|
+
from ccx.codex.provenance import parse_client_message_id
|
|
1263
|
+
from ccx.claude.usage import _merge_caps
|
|
1264
|
+
from ccx.workers import _check_caps
|
|
1265
|
+
|
|
1266
|
+
caps = message.get("cap")
|
|
1267
|
+
if caps is not None:
|
|
1268
|
+
_check_caps(caps, engine="codex")
|
|
1269
|
+
|
|
1270
|
+
store.authorize_native_action(owner.worker, "send")
|
|
1271
|
+
thread_id = native.get("threadId")
|
|
1272
|
+
if (not isinstance(thread_id, str) or not thread_id
|
|
1273
|
+
or native.get("threadSource") != source
|
|
1274
|
+
or thread_id != message.get("threadId")):
|
|
1275
|
+
raise RuntimeError("original Codex native thread differs")
|
|
1276
|
+
token = message.get("deliveryId")
|
|
1277
|
+
if not isinstance(token, str) or not token:
|
|
1278
|
+
raise RuntimeError("original delivery token required")
|
|
1279
|
+
receipt = semantic_receipt(owner.worker, token)
|
|
1280
|
+
effect = store.read_receipt(owner.worker, "codex-send", token)
|
|
1281
|
+
if not isinstance(receipt, dict):
|
|
1282
|
+
raise RuntimeError("original Codex semantic receipt absent")
|
|
1283
|
+
if receipt.get("cap") != caps:
|
|
1284
|
+
raise RuntimeError("original Codex cap differs from immutable delivery")
|
|
1285
|
+
mode = requested_mode(receipt)
|
|
1286
|
+
if (receipt.get("state") != (
|
|
1287
|
+
"submitted" if mode == "new_turn_only" else "prepared")
|
|
1288
|
+
or receipt.get("clientUserMessageId")
|
|
1289
|
+
!= message.get("clientUserMessageId")
|
|
1290
|
+
or not isinstance(effect, dict)
|
|
1291
|
+
or effect.get("state") != "submitted-unknown"
|
|
1292
|
+
or effect.get("action") != "send"
|
|
1293
|
+
or effect.get("workerId") != worker_id
|
|
1294
|
+
or effect.get("generation") != store.generation
|
|
1295
|
+
or effect.get("intentSha256") != _digest(intent_spec(receipt))):
|
|
1296
|
+
raise RuntimeError("original Codex send effect/semantic receipt unproven")
|
|
1297
|
+
if (not isinstance(message.get("text"), str)
|
|
1298
|
+
or hashlib.sha256(message["text"].encode()).hexdigest()
|
|
1299
|
+
!= receipt.get("promptSha256")):
|
|
1300
|
+
raise RuntimeError("original Codex send payload changed")
|
|
1301
|
+
original_worker = daemon._adopt_original_native_worker(owner.worker)
|
|
1302
|
+
if daemon.client is None or not daemon.ready.is_set():
|
|
1303
|
+
raise RuntimeError("Codex app-server not ready")
|
|
1304
|
+
|
|
1305
|
+
def refused(code: str, detail: str, *,
|
|
1306
|
+
current: dict | None = None) -> dict:
|
|
1307
|
+
frozen = current if current is not None else receipt
|
|
1308
|
+
chosen = send_receipt_route(frozen)
|
|
1309
|
+
target = frozen.get("targetTurnId")
|
|
1310
|
+
client_id = frozen.get("clientUserMessageId")
|
|
1311
|
+
return {
|
|
1312
|
+
"accepted": False, "nativeStatus": "rejected",
|
|
1313
|
+
"rejectionCode": code, "rejectionMessage": detail,
|
|
1314
|
+
"workerId": worker_id, "engine": "codex",
|
|
1315
|
+
"nativeSource": source, "threadId": thread_id,
|
|
1316
|
+
"deliveryId": token, "route": chosen,
|
|
1317
|
+
"targetTurnId": target, "clientUserMessageId": client_id,
|
|
1318
|
+
"nativeEvidenceSha256": _digest({
|
|
1319
|
+
"source": source, "threadId": thread_id,
|
|
1320
|
+
"deliveryId": token, "code": code,
|
|
1321
|
+
"route": chosen, "targetTurnId": target,
|
|
1322
|
+
"clientUserMessageId": client_id,
|
|
1323
|
+
}),
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
# Same operation lock as cancel/kill/compact, before store.lease.
|
|
1327
|
+
if original_worker.killed.is_set() or original_worker.kill_intent.is_set():
|
|
1328
|
+
return refused("worker-killed", "original worker is being killed")
|
|
1329
|
+
with original_worker.operation_lock:
|
|
1330
|
+
if not daemon._still_registered(original_worker):
|
|
1331
|
+
return refused("not-registered", "original worker is no longer registered")
|
|
1332
|
+
store.revalidate()
|
|
1333
|
+
latest = store.record(owner.worker)
|
|
1334
|
+
bound = latest.get("native") or {}
|
|
1335
|
+
if (bound.get("threadId") != thread_id
|
|
1336
|
+
or bound.get("threadSource") != source):
|
|
1337
|
+
raise RuntimeError("original Codex native binding changed")
|
|
1338
|
+
with original_worker.lock:
|
|
1339
|
+
lifecycle = original_worker.record.get("lifecycle") or {}
|
|
1340
|
+
state = lifecycle.get("state")
|
|
1341
|
+
live = (original_worker.loaded
|
|
1342
|
+
and not original_worker.killed.is_set()
|
|
1343
|
+
and not original_worker.kill_intent.is_set()
|
|
1344
|
+
and not original_worker.teardown_claimed)
|
|
1345
|
+
busy_compact = bool(original_worker.record.get("activeCompact"))
|
|
1346
|
+
current_turn = lifecycle.get("activeTurnId")
|
|
1347
|
+
launch_pending = original_worker.launch is not None
|
|
1348
|
+
route_kind, target_turn = None, None
|
|
1349
|
+
if live and not busy_compact and not launch_pending:
|
|
1350
|
+
if state == "idle" and mode != "steer_only":
|
|
1351
|
+
route_kind = "new_turn"
|
|
1352
|
+
elif state == "busy" and mode != "new_turn_only":
|
|
1353
|
+
if isinstance(current_turn, str) and current_turn:
|
|
1354
|
+
route_kind, target_turn = "steer", current_turn
|
|
1355
|
+
if route_kind is None:
|
|
1356
|
+
if (live and not busy_compact and not launch_pending
|
|
1357
|
+
and state == "busy" and mode == "new_turn_only"):
|
|
1358
|
+
# Positive, lock-held proof: the live original owns a
|
|
1359
|
+
# turn, so a strict new turn is refused before any RPC.
|
|
1360
|
+
return refused(
|
|
1361
|
+
"busy", f"original worker cannot accept {mode} in state=busy",
|
|
1362
|
+
)
|
|
1363
|
+
return refused(
|
|
1364
|
+
"compaction-busy" if busy_compact else "worker-state",
|
|
1365
|
+
("compaction owns the input surface" if busy_compact else
|
|
1366
|
+
f"original worker cannot accept {mode} in state={state}"),
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
# Preflight before the provider turn, using a cached window or
|
|
1370
|
+
# one bounded account read when no fresh signal exists.
|
|
1371
|
+
if caps:
|
|
1372
|
+
trip = daemon._cap_at_admission(caps)
|
|
1373
|
+
if trip:
|
|
1374
|
+
key, used, cap = trip
|
|
1375
|
+
return refused(
|
|
1376
|
+
"budget-stopped", f"usage cap {key} already exceeded "
|
|
1377
|
+
f"({used:g}% >= {cap:g}%); no "
|
|
1378
|
+
f"{'message' if route_kind == 'steer' else 'turn'} delivered",
|
|
1379
|
+
)
|
|
1380
|
+
|
|
1381
|
+
if mode != "new_turn_only":
|
|
1382
|
+
receipt = mark_owned(
|
|
1383
|
+
owner.worker, token, route_kind=route_kind,
|
|
1384
|
+
target_turn_id=target_turn,
|
|
1385
|
+
)
|
|
1386
|
+
receipt = mark_submitted(
|
|
1387
|
+
owner.worker, token, runtime_id=daemon.instance_id,
|
|
1388
|
+
)
|
|
1389
|
+
else:
|
|
1390
|
+
receipt = semantic_receipt(owner.worker, token)
|
|
1391
|
+
if (receipt.get("state") != "submitted"
|
|
1392
|
+
or send_receipt_route(receipt) != route_kind
|
|
1393
|
+
or receipt.get("targetTurnId") != target_turn):
|
|
1394
|
+
raise RuntimeError("native Codex route not frozen in original receipt")
|
|
1395
|
+
client_id = receipt.get("clientUserMessageId")
|
|
1396
|
+
identity = parse_client_message_id(client_id)
|
|
1397
|
+
if (not isinstance(identity, dict)
|
|
1398
|
+
or identity.get("workerId") != worker_id
|
|
1399
|
+
or identity.get("kind") != (
|
|
1400
|
+
"steer" if route_kind == "steer" else "start")):
|
|
1401
|
+
raise RuntimeError("original Codex native client message provenance invalid")
|
|
1402
|
+
|
|
1403
|
+
# A later asynchronous turn notification can still move R;
|
|
1404
|
+
# expectedTurnId makes such a native steer a positive refusal.
|
|
1405
|
+
with original_worker.lock:
|
|
1406
|
+
current = original_worker.record.get("lifecycle") or {}
|
|
1407
|
+
if (original_worker.killed.is_set()
|
|
1408
|
+
or original_worker.kill_intent.is_set()
|
|
1409
|
+
or not original_worker.loaded
|
|
1410
|
+
or original_worker.record.get("activeCompact")
|
|
1411
|
+
or current.get("state") != (
|
|
1412
|
+
"busy" if route_kind == "steer" else "idle")
|
|
1413
|
+
or (route_kind == "steer"
|
|
1414
|
+
and current.get("activeTurnId") != target_turn)):
|
|
1415
|
+
return refused(
|
|
1416
|
+
"worker-changed",
|
|
1417
|
+
"original native turn or input owner changed before request",
|
|
1418
|
+
current=receipt,
|
|
1419
|
+
)
|
|
1420
|
+
|
|
1421
|
+
key = hashlib.sha256(("send" + "\x00" + token).encode()).hexdigest()
|
|
1422
|
+
with store.lease():
|
|
1423
|
+
with store._gen(
|
|
1424
|
+
"runtime-metadata", "codex-native-effects", worker_id,
|
|
1425
|
+
create=True,
|
|
1426
|
+
) as directory:
|
|
1427
|
+
with _flock_at(
|
|
1428
|
+
directory, key+".lock", exclusive=True,
|
|
1429
|
+
create=True, timeout=.5,
|
|
1430
|
+
):
|
|
1431
|
+
if _read_json(directory, key+".json") is not None:
|
|
1432
|
+
raise NativeEffectIndeterminate(
|
|
1433
|
+
"this original native Codex request was already "
|
|
1434
|
+
"submitted, provider result unknown; no replay"
|
|
1435
|
+
)
|
|
1436
|
+
_write_json(directory, key+".json", {
|
|
1437
|
+
"workerId": worker_id, "engine": "codex",
|
|
1438
|
+
"generation": store.generation, "kind": "send",
|
|
1439
|
+
"token": token, "requestedMode": mode,
|
|
1440
|
+
"route": route_kind, "targetTurnId": target_turn,
|
|
1441
|
+
"clientUserMessageId": client_id,
|
|
1442
|
+
"intentSha256": effect["intentSha256"],
|
|
1443
|
+
"state": "submitted-unknown",
|
|
1444
|
+
})
|
|
1445
|
+
store.revalidate()
|
|
1446
|
+
if route_kind == "steer":
|
|
1447
|
+
try:
|
|
1448
|
+
daemon.client.request("turn/steer", {
|
|
1449
|
+
"threadId": thread_id,
|
|
1450
|
+
"expectedTurnId": target_turn,
|
|
1451
|
+
"input": [{"type": "text", "text": message["text"]}],
|
|
1452
|
+
"clientUserMessageId": client_id,
|
|
1453
|
+
}, timeout=2)
|
|
1454
|
+
except ProtocolError as error:
|
|
1455
|
+
if (not isinstance(error, ProtocolTransportError)
|
|
1456
|
+
and error.request_id is not None):
|
|
1457
|
+
return refused(
|
|
1458
|
+
"native-rejected", str(error), current=receipt,
|
|
1459
|
+
)
|
|
1460
|
+
raise
|
|
1461
|
+
turn_id = target_turn
|
|
1462
|
+
else:
|
|
1463
|
+
turn_params = {
|
|
1464
|
+
"threadId": thread_id,
|
|
1465
|
+
"input": [{"type": "text", "text": message["text"]}],
|
|
1466
|
+
"clientUserMessageId": client_id,
|
|
1467
|
+
}
|
|
1468
|
+
for setting in ("model", "effort"):
|
|
1469
|
+
value = (latest.get("createArgs") or {}).get(setting)
|
|
1470
|
+
if value:
|
|
1471
|
+
turn_params[setting] = value
|
|
1472
|
+
with original_worker.lock:
|
|
1473
|
+
if (original_worker.killed.is_set()
|
|
1474
|
+
or original_worker.kill_intent.is_set()):
|
|
1475
|
+
raise RuntimeError(
|
|
1476
|
+
"original worker is being killed; no native turn/start"
|
|
1477
|
+
)
|
|
1478
|
+
launch = _TurnLaunch()
|
|
1479
|
+
original_worker.launch = launch
|
|
1480
|
+
lifecycle = original_worker.record["lifecycle"]
|
|
1481
|
+
pre_launch_lifecycle = dict(lifecycle)
|
|
1482
|
+
lifecycle["state"] = "busy"
|
|
1483
|
+
lifecycle["since"] = now_ms()
|
|
1484
|
+
lifecycle["activeTurnId"] = None
|
|
1485
|
+
original_worker.record["nativeTurnStart"] = (
|
|
1486
|
+
original_worker._native_turn_start_marker(
|
|
1487
|
+
kind="send", client_message_id=client_id,
|
|
1488
|
+
delivery_id=token,
|
|
1489
|
+
)
|
|
1490
|
+
)
|
|
1491
|
+
original_worker._save()
|
|
1492
|
+
turn_id = None
|
|
1493
|
+
try:
|
|
1494
|
+
response = daemon.client.request(
|
|
1495
|
+
"turn/start", turn_params, timeout=2,
|
|
1496
|
+
)
|
|
1497
|
+
turn = response.get("turn") if isinstance(response, dict) else None
|
|
1498
|
+
turn_id = turn.get("id") if isinstance(turn, dict) else None
|
|
1499
|
+
if not isinstance(turn_id, str) or not turn_id:
|
|
1500
|
+
raise NativeEffectIndeterminate(
|
|
1501
|
+
"Codex turn/start reply lacks exact native turn"
|
|
1502
|
+
)
|
|
1503
|
+
except Exception as error:
|
|
1504
|
+
with original_worker.lock:
|
|
1505
|
+
if (isinstance(error, ProtocolError)
|
|
1506
|
+
and not isinstance(error, ProtocolTransportError)
|
|
1507
|
+
and error.request_id is not None):
|
|
1508
|
+
original_worker.record["nativeTurnStart"] = None
|
|
1509
|
+
original_worker.record["lifecycle"] = pre_launch_lifecycle
|
|
1510
|
+
return refused(
|
|
1511
|
+
"native-rejected", str(error), current=receipt,
|
|
1512
|
+
)
|
|
1513
|
+
original_worker._mark_native_turn_start_unknown_locked(error)
|
|
1514
|
+
raise
|
|
1515
|
+
finally:
|
|
1516
|
+
with original_worker.lock:
|
|
1517
|
+
if original_worker.launch is launch:
|
|
1518
|
+
original_worker.launch = None
|
|
1519
|
+
launch.turn_id = turn_id
|
|
1520
|
+
if turn_id:
|
|
1521
|
+
original_worker.record["nativeTurnStart"] = None
|
|
1522
|
+
original_worker.record["historyKnownEmpty"] = False
|
|
1523
|
+
if original_worker.record["lifecycle"].get("state") == "busy":
|
|
1524
|
+
original_worker.record["lifecycle"]["activeTurnId"] = (
|
|
1525
|
+
original_worker.record["lifecycle"].get("activeTurnId")
|
|
1526
|
+
or turn_id
|
|
1527
|
+
)
|
|
1528
|
+
try:
|
|
1529
|
+
original_worker._save()
|
|
1530
|
+
finally:
|
|
1531
|
+
launch.done.set()
|
|
1532
|
+
if original_worker.killed.is_set():
|
|
1533
|
+
original_worker._interrupt_turn(turn_id)
|
|
1534
|
+
daemon_release._abandon_killed_load(daemon, original_worker)
|
|
1535
|
+
raise RuntimeError(
|
|
1536
|
+
"original worker killed during native turn/start"
|
|
1537
|
+
)
|
|
1538
|
+
if caps:
|
|
1539
|
+
with original_worker.lock:
|
|
1540
|
+
if (original_worker.record["lifecycle"].get("activeTurnId")
|
|
1541
|
+
== turn_id
|
|
1542
|
+
and (original_worker.record.get("quotaStop") or {}).get("turnId")
|
|
1543
|
+
!= turn_id):
|
|
1544
|
+
original_worker.cap_turn_id = turn_id
|
|
1545
|
+
original_worker.active_caps = _merge_caps([
|
|
1546
|
+
original_worker.active_caps
|
|
1547
|
+
if route_kind == "steer" else None,
|
|
1548
|
+
caps,
|
|
1549
|
+
])
|
|
1550
|
+
original_worker.check_active_caps(
|
|
1551
|
+
daemon._cap_data_from_cache())
|
|
1552
|
+
return {
|
|
1553
|
+
"accepted": True, "workerId": worker_id,
|
|
1554
|
+
"engine": "codex", "nativeSource": source,
|
|
1555
|
+
"threadId": thread_id, "deliveryId": token,
|
|
1556
|
+
"clientUserMessageId": client_id,
|
|
1557
|
+
"route": route_kind, "targetTurnId": target_turn,
|
|
1558
|
+
"turnId": turn_id,
|
|
1559
|
+
"nativeEvidenceSha256": _digest({
|
|
1560
|
+
"source": source, "threadId": thread_id,
|
|
1561
|
+
"turnId": turn_id, "deliveryId": token,
|
|
1562
|
+
"route": route_kind, "targetTurnId": target_turn,
|
|
1563
|
+
"clientUserMessageId": client_id,
|
|
1564
|
+
}),
|
|
1565
|
+
}
|
|
1566
|
+
else:
|
|
1567
|
+
raise RuntimeError("unknown native Codex effect")
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
def _original_history_binding(daemon, message: dict[str, Any]):
|
|
1571
|
+
"""Resolve one immutable original Codex worker/thread for read-only RPC."""
|
|
1572
|
+
ctx = daemon._uuid_store
|
|
1573
|
+
owner = ctx.resolve(message.get("workerId"))
|
|
1574
|
+
if owner.engine != "codex":
|
|
1575
|
+
raise ValueError("Codex history needs original full Codex worker UUID")
|
|
1576
|
+
original = ctx.record(owner.worker)
|
|
1577
|
+
native = original.get("native") or {}
|
|
1578
|
+
tid = native.get("threadId") if isinstance(native, dict) else None
|
|
1579
|
+
source = ctx.codex_source(owner.worker)
|
|
1580
|
+
claim = ctx.native_activation(owner.worker)
|
|
1581
|
+
binding = claim.get("nativeBinding")
|
|
1582
|
+
if (not isinstance(tid, str) or not tid
|
|
1583
|
+
or native.get("threadSource") != source
|
|
1584
|
+
or claim.get("state") != "confirmed"
|
|
1585
|
+
or claim.get("source") != source
|
|
1586
|
+
or not isinstance(binding, dict)
|
|
1587
|
+
or binding.get("threadId") != tid
|
|
1588
|
+
or message.get("threadId", tid) != tid):
|
|
1589
|
+
raise ValueError("native thread not positively bound to original full UUID")
|
|
1590
|
+
return owner, tid, source
|