ccx-native 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +240 -4
- package/bin/ccx +55 -2
- package/ccx/__init__.py +167 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +807 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +269 -0
- package/ccx/claude/host/session.py +5244 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2535 -0
- package/ccx/claude/usage.py +796 -0
- package/ccx/claude/usage_control.py +204 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/models.py +53 -0
- package/ccx/cli/commands/read.py +1104 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +302 -0
- package/ccx/cli/parser.py +1439 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1904 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +738 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1558 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2424 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/history_store.py +131 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1454 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/models.py +225 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +296 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1593 -0
- package/ccx/ops/conversation_paging.py +1339 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +575 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/history.py +1041 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +773 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +348 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +640 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +250 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +789 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/private_json.py +54 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +809 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2093 -0
- package/ccx.py +9 -0
- package/package.json +39 -7
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +166 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/history.md +50 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
package/ccx/codex/ops.py
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
"""Codex configuration, CLI reconciliation, compact, telemetry and account reads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ccx.codex import runtime as runtime
|
|
10
|
+
from ccx.codex.telemetry import read_telemetry
|
|
11
|
+
from ccx.errors import DeliveryRejected, InvalidRequest, SessionNotFound, SessionUnavailable, worker_not_found
|
|
12
|
+
from ccx.store.worker_state import RUNTIME_ABSENT, RUNTIME_INCONCLUSIVE, RUNTIME_SERVED
|
|
13
|
+
|
|
14
|
+
_CONFIG_UNSET = object()
|
|
15
|
+
|
|
16
|
+
def _deadline(timeout: float | None) -> float | None:
|
|
17
|
+
if timeout is not None and timeout < 0:
|
|
18
|
+
raise InvalidRequest("timeout must be zero or greater")
|
|
19
|
+
return None if timeout is None else time.monotonic() + timeout
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def deliver_machine_wake(
|
|
23
|
+
owner, payload: dict[str, Any], *, namespace: str, name: str,
|
|
24
|
+
reconcile: bool, timeout: float = 65,
|
|
25
|
+
expected_thread_id: str | None = None,
|
|
26
|
+
expected_generation: str | None = None,
|
|
27
|
+
) -> dict[str, Any]:
|
|
28
|
+
"""Private original Codex machine output: one owned writer, never a worker send."""
|
|
29
|
+
from ccx import constants
|
|
30
|
+
from ccx.store.root import GenerationError, require_ccx_active
|
|
31
|
+
from ccx.store.identity import WorkerOwner, IdentityError
|
|
32
|
+
from ccx.ops.send import ensure_ready
|
|
33
|
+
|
|
34
|
+
if (not isinstance(owner, WorkerOwner) or owner.engine != "codex"
|
|
35
|
+
or not isinstance(payload, dict)
|
|
36
|
+
or not isinstance(payload.get("eventId"), str) or not payload["eventId"]
|
|
37
|
+
or not isinstance(payload.get("deliveryId"), str) or not payload["deliveryId"]
|
|
38
|
+
or not isinstance(namespace, str) or not namespace or namespace == "ccx"
|
|
39
|
+
or not isinstance(name, str) or not name
|
|
40
|
+
or type(reconcile) is not bool
|
|
41
|
+
or type(timeout) not in (int, float) or not 0 < timeout <= 65):
|
|
42
|
+
raise InvalidRequest("Invalid Codex notification; check the recipient")
|
|
43
|
+
event_id, delivery_id = payload["eventId"], payload["deliveryId"]
|
|
44
|
+
deadline = time.monotonic() + timeout
|
|
45
|
+
|
|
46
|
+
def no_effect(reason: str) -> dict[str, Any]:
|
|
47
|
+
return {"ok": False, "definitive": True,
|
|
48
|
+
**({"notAttempted": True} if not reconcile else {}),
|
|
49
|
+
"error": reason}
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
store = require_ccx_active(
|
|
53
|
+
constants.CCX_ROOT, expected_generation=expected_generation,
|
|
54
|
+
)
|
|
55
|
+
current = store.resolve(owner.worker)
|
|
56
|
+
if current != owner:
|
|
57
|
+
return no_effect("Codex machine wake owner changed")
|
|
58
|
+
source = store.codex_source(owner.worker)
|
|
59
|
+
record = store.record(owner.worker)
|
|
60
|
+
native = record.get("native") or {}
|
|
61
|
+
thread_id = native.get("threadId")
|
|
62
|
+
if (not isinstance(thread_id, str) or not thread_id
|
|
63
|
+
or native.get("threadSource") != source
|
|
64
|
+
or (expected_thread_id is not None
|
|
65
|
+
and thread_id != expected_thread_id)):
|
|
66
|
+
return no_effect("Codex machine wake original thread/source differs")
|
|
67
|
+
state = (record.get("lifecycle") or {}).get("state")
|
|
68
|
+
if state in {"dead", "budget-stopped"}:
|
|
69
|
+
return no_effect("explicitly stopped original Codex parent stays pending")
|
|
70
|
+
if state not in {"idle", "idle-killed", "crashed", "busy", "waiting"}:
|
|
71
|
+
return no_effect("original Codex parent is not ready for machine wake")
|
|
72
|
+
reload = state in {"idle-killed", "crashed"}
|
|
73
|
+
if state == "idle":
|
|
74
|
+
observed = runtime.observe_worker(owner.worker_id, timeout=min(1., max(.01, deadline-time.monotonic())))
|
|
75
|
+
if observed["presence"] == RUNTIME_INCONCLUSIVE:
|
|
76
|
+
return no_effect("original Codex parent presence inconclusive")
|
|
77
|
+
reload = observed["presence"] == RUNTIME_ABSENT
|
|
78
|
+
if reload:
|
|
79
|
+
remaining = deadline - time.monotonic()
|
|
80
|
+
if remaining <= 0:
|
|
81
|
+
return no_effect("original Codex parent load budget expired")
|
|
82
|
+
ready = ensure_ready(
|
|
83
|
+
owner.worker, expected_generation=store.generation,
|
|
84
|
+
timeout=min(60., remaining),
|
|
85
|
+
)
|
|
86
|
+
if (ready.get("worker_id") != owner.worker_id
|
|
87
|
+
or ready.get("thread_id") != thread_id
|
|
88
|
+
or ready.get("state") != "idle"):
|
|
89
|
+
return no_effect("original Codex parent reload thread/owner unproven")
|
|
90
|
+
store.revalidate()
|
|
91
|
+
except (GenerationError, IdentityError, SessionUnavailable, DeliveryRejected,
|
|
92
|
+
OSError, ValueError) as error:
|
|
93
|
+
return no_effect(f"original Codex machine wake preflight unavailable: {error}")
|
|
94
|
+
|
|
95
|
+
remaining = deadline - time.monotonic()
|
|
96
|
+
if remaining <= 0:
|
|
97
|
+
return no_effect("original Codex machine wake RPC budget expired")
|
|
98
|
+
try:
|
|
99
|
+
reply = runtime.rpc(
|
|
100
|
+
owner.worker_id,
|
|
101
|
+
{"op": "machine-wake", "eventId": event_id,
|
|
102
|
+
"deliveryId": delivery_id, "payload": payload,
|
|
103
|
+
"namespace": namespace, "name": name, "threadId": thread_id,
|
|
104
|
+
"reconcile": reconcile},
|
|
105
|
+
timeout=remaining,
|
|
106
|
+
)
|
|
107
|
+
except Exception as error:
|
|
108
|
+
return {"ok": False, "pending": True,
|
|
109
|
+
"error": f"original Codex machine wake RPC uncertain: {error}"}
|
|
110
|
+
if reply.get("transport") != RUNTIME_SERVED:
|
|
111
|
+
# Absent can follow a written request (daemon died before replying).
|
|
112
|
+
return {"ok": False, "pending": True,
|
|
113
|
+
"error": "original Codex machine wake RPC not served"}
|
|
114
|
+
if reply.get("ok") is not True:
|
|
115
|
+
refused = {key: value for key, value in reply.items() if key != "notAttempted"}
|
|
116
|
+
if not reconcile and reply.get("definitive") is True and not reply.get("pending"):
|
|
117
|
+
refused["notAttempted"] = True
|
|
118
|
+
return refused
|
|
119
|
+
if (reply.get("workerId") != owner.worker_id
|
|
120
|
+
or reply.get("threadId") != thread_id
|
|
121
|
+
or reply.get("deliveryId") != delivery_id
|
|
122
|
+
or not (reply.get("reconciled") is True
|
|
123
|
+
or (isinstance(reply.get("turnId"), str) and reply["turnId"]))):
|
|
124
|
+
return {"ok": False, "pending": True,
|
|
125
|
+
"error": "original Codex machine wake lacks positive native placement proof"}
|
|
126
|
+
try:
|
|
127
|
+
store.revalidate()
|
|
128
|
+
except GenerationError as error:
|
|
129
|
+
return {"ok": False, "pending": True,
|
|
130
|
+
"error": f"Codex machine wake generation changed after native effect: {error}"}
|
|
131
|
+
return reply
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def configure_worker(
|
|
139
|
+
target: str,
|
|
140
|
+
*,
|
|
141
|
+
model: str | None | object = _CONFIG_UNSET,
|
|
142
|
+
effort: str | None | object = _CONFIG_UNSET,
|
|
143
|
+
timeout: float = 5,
|
|
144
|
+
expected_generation: str | None = None,
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
"""Set only *future* turns on one original Codex full-UUID worker.
|
|
147
|
+
|
|
148
|
+
A serving daemon always owns mutation, even if this worker is unloaded.
|
|
149
|
+
A proven absent daemon permits one fresh dotted CAS while holding the
|
|
150
|
+
existing singleton spawn lock. Neither path loads a thread or sends text.
|
|
151
|
+
"""
|
|
152
|
+
from math import isfinite
|
|
153
|
+
from ccx import constants
|
|
154
|
+
from ccx.codex.runtime import daemon_call, daemon_spawn_lock, observe_worker
|
|
155
|
+
from ccx.store.root import GenerationError, RecordConflict, require_ccx_active
|
|
156
|
+
from ccx.store.identity import EngineMismatch, IdentityError, UnknownWorker
|
|
157
|
+
|
|
158
|
+
if model is _CONFIG_UNSET and effort is _CONFIG_UNSET:
|
|
159
|
+
raise InvalidRequest("configure_worker requires model and/or effort")
|
|
160
|
+
updates: dict[str, str | None] = {}
|
|
161
|
+
for key, value in (("model", model), ("effort", effort)):
|
|
162
|
+
if value is _CONFIG_UNSET:
|
|
163
|
+
continue
|
|
164
|
+
if value is not None and (not isinstance(value, str) or not value.strip()):
|
|
165
|
+
raise InvalidRequest(f"{key} must be a non-empty string or None")
|
|
166
|
+
normalized = value.strip() if isinstance(value, str) else None
|
|
167
|
+
if key == "effort" and normalized not in (None, "low", "medium", "high", "xhigh", "max"):
|
|
168
|
+
raise InvalidRequest("Codex effort must be low/medium/high/xhigh/max or None")
|
|
169
|
+
updates[key] = normalized
|
|
170
|
+
if type(timeout) not in (int, float) or not isfinite(timeout) or timeout <= 0:
|
|
171
|
+
raise InvalidRequest("Codex configuration timeout must be positive and finite")
|
|
172
|
+
try:
|
|
173
|
+
store = require_ccx_active(constants.CCX_ROOT, expected_generation=expected_generation)
|
|
174
|
+
owner = store.resolve(target)
|
|
175
|
+
if owner.engine != "codex":
|
|
176
|
+
raise EngineMismatch("Configuring Codex needs a worker handle")
|
|
177
|
+
handle, worker_id = owner.worker, owner.worker_id
|
|
178
|
+
record = store.record(handle) # original owner/record validation
|
|
179
|
+
if updates.get("model") is not None:
|
|
180
|
+
from ccx.codex.runtime import validate_model
|
|
181
|
+
validate_model(updates["model"], cwd=record["cwd"])
|
|
182
|
+
# All persistence paths, including daemon set-config, follow preflight.
|
|
183
|
+
deadline = time.monotonic() + timeout
|
|
184
|
+
|
|
185
|
+
def time_left() -> float:
|
|
186
|
+
remaining = deadline - time.monotonic()
|
|
187
|
+
if remaining <= 0:
|
|
188
|
+
raise SessionUnavailable("Codex configuration deadline expired", short=owner.short)
|
|
189
|
+
return remaining
|
|
190
|
+
|
|
191
|
+
def observe():
|
|
192
|
+
return observe_worker(worker_id, timeout=min(1., time_left()))
|
|
193
|
+
|
|
194
|
+
def daemon_mutation(probe: dict) -> dict | None:
|
|
195
|
+
presence = probe["presence"]
|
|
196
|
+
if presence == RUNTIME_INCONCLUSIVE:
|
|
197
|
+
raise SessionUnavailable("Codex loadedness is inconclusive; no config written", short=owner.short)
|
|
198
|
+
if not probe["daemon_served"]:
|
|
199
|
+
return None
|
|
200
|
+
# A loaded non-idle native turn can never receive a future-turn
|
|
201
|
+
# configuration. codexd checks again under its worker.lock.
|
|
202
|
+
if presence == RUNTIME_SERVED and probe["reply"].get("state") != "idle":
|
|
203
|
+
raise DeliveryRejected(
|
|
204
|
+
f"Worker is {probe['reply'].get('state')}; wait until idle to change settings.",
|
|
205
|
+
short=owner.short, state=probe["reply"].get("state"),
|
|
206
|
+
refusal="busy",
|
|
207
|
+
)
|
|
208
|
+
reply = daemon_call(
|
|
209
|
+
{"op": "set-config", "short": worker_id, "updates": updates},
|
|
210
|
+
timeout=time_left(), short=owner.short,
|
|
211
|
+
)
|
|
212
|
+
if reply.get("transport") != RUNTIME_SERVED:
|
|
213
|
+
raise SessionUnavailable(
|
|
214
|
+
"Codex settings may not have saved; check current settings before retrying",
|
|
215
|
+
short=owner.short,
|
|
216
|
+
)
|
|
217
|
+
if reply.get("ok") is not True:
|
|
218
|
+
raise SessionUnavailable(
|
|
219
|
+
reply.get("error") or "Codex configuration refused",
|
|
220
|
+
short=owner.short,
|
|
221
|
+
)
|
|
222
|
+
store.revalidate()
|
|
223
|
+
return store.record(handle)
|
|
224
|
+
|
|
225
|
+
applied = daemon_mutation(observe())
|
|
226
|
+
if applied is not None:
|
|
227
|
+
return applied
|
|
228
|
+
with daemon_spawn_lock(deadline):
|
|
229
|
+
# No daemon may boot/load while this lock is held. It is not a
|
|
230
|
+
# new configuration lock: this is the original daemon spawn fence.
|
|
231
|
+
second = observe()
|
|
232
|
+
applied = daemon_mutation(second)
|
|
233
|
+
if applied is not None:
|
|
234
|
+
return applied
|
|
235
|
+
if second["presence"] != RUNTIME_ABSENT:
|
|
236
|
+
raise SessionUnavailable(
|
|
237
|
+
"Codex runtime absentness unproven; no config written",
|
|
238
|
+
short=owner.short,
|
|
239
|
+
)
|
|
240
|
+
recorded = store.record(handle)
|
|
241
|
+
state = (recorded.get("lifecycle") or {}).get("state")
|
|
242
|
+
if state in {"busy", "waiting", "booting", "resuming"}:
|
|
243
|
+
raise SessionUnavailable(
|
|
244
|
+
f"Codex worker is {state}; configuration requires idle or unloaded",
|
|
245
|
+
short=owner.short,
|
|
246
|
+
)
|
|
247
|
+
# Re-validate the durable lifecycle *under* the record CAS, too.
|
|
248
|
+
return store.mutate_record(
|
|
249
|
+
handle,
|
|
250
|
+
patches={f"createArgs.{key}": value for key, value in updates.items()},
|
|
251
|
+
expect={"lifecycle.state": state},
|
|
252
|
+
)
|
|
253
|
+
except UnknownWorker as exc:
|
|
254
|
+
raise worker_not_found(target) from exc
|
|
255
|
+
except IdentityError as exc:
|
|
256
|
+
raise InvalidRequest(f"Invalid Codex worker handle: {exc}") from exc
|
|
257
|
+
except RecordConflict as exc:
|
|
258
|
+
raise SessionUnavailable("Codex state changed before saving settings; retry while idle", short=owner.short) from exc
|
|
259
|
+
except GenerationError as exc:
|
|
260
|
+
raise SessionUnavailable(f"Codex ACTIVE owner/config unavailable: {exc}") from exc
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def caller_status(result: dict[str, Any]) -> tuple[str, bool]:
|
|
264
|
+
"""CLI/Python verdict for a reconcile result: ``(status, pending)``.
|
|
265
|
+
|
|
266
|
+
A delivery CCX refused before submission (`prepared`, or `owned` with
|
|
267
|
+
nothing written natively) is pending only in the receipt sense that the
|
|
268
|
+
same id may still be sent. Nothing is in flight and nothing will land
|
|
269
|
+
without a retry, so these adapters report it as ``not_delivered`` rather
|
|
270
|
+
than leave a caller waiting on it. The public protocol keeps its documented
|
|
271
|
+
``pending`` + ``retry_same_id`` + ``last_refusal`` shape.
|
|
272
|
+
"""
|
|
273
|
+
delivery = result.get("delivery")
|
|
274
|
+
if (
|
|
275
|
+
result.get("status") == "pending"
|
|
276
|
+
and isinstance(delivery, dict)
|
|
277
|
+
and delivery.get("recommended_action") == "retry_same_id"
|
|
278
|
+
):
|
|
279
|
+
return "not_delivered", False
|
|
280
|
+
return result["status"], bool(result.get("pending"))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _semantic_status(public: dict[str, Any] | None) -> str:
|
|
284
|
+
if not isinstance(public, dict):
|
|
285
|
+
return "not_found"
|
|
286
|
+
raw = public.get("status")
|
|
287
|
+
if raw == "accepted":
|
|
288
|
+
return "accepted"
|
|
289
|
+
if raw == "rejected":
|
|
290
|
+
return "rejected"
|
|
291
|
+
return "pending"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def compact_intent_digest(
|
|
307
|
+
worker_id: str, generation: str, thread_id: str,
|
|
308
|
+
thread_source: str, operation_id: str,
|
|
309
|
+
) -> str:
|
|
310
|
+
"""One immutable full-UUID compact identity; budgets are not inputs."""
|
|
311
|
+
from ccx.store.store import _digest
|
|
312
|
+
|
|
313
|
+
return _digest({
|
|
314
|
+
"workerId": worker_id, "engine": "codex", "generation": generation,
|
|
315
|
+
"threadId": thread_id, "threadSource": thread_source,
|
|
316
|
+
"operationId": operation_id, "action": "compact",
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _compact_marker(value: dict[str, Any], operation_id: str):
|
|
321
|
+
active = value.get("activeCompact")
|
|
322
|
+
if isinstance(active, dict) and active.get("operationId") == operation_id:
|
|
323
|
+
return "active", active
|
|
324
|
+
last = value.get("lastCompact")
|
|
325
|
+
if isinstance(last, dict) and last.get("operationId") == operation_id:
|
|
326
|
+
return "last", last
|
|
327
|
+
return None, None
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _compact_view(store, owner, operation_id: str, receipt: dict | None) -> dict:
|
|
331
|
+
"""Project exact-ID compaction status without native reads."""
|
|
332
|
+
record = store.record(owner.worker)
|
|
333
|
+
kind, marker = _compact_marker(record, operation_id)
|
|
334
|
+
state = receipt.get("state") if receipt else None
|
|
335
|
+
native = record.get("native") or {}
|
|
336
|
+
digest = compact_intent_digest(
|
|
337
|
+
owner.worker_id, store.generation, native.get("threadId"),
|
|
338
|
+
native.get("threadSource"), operation_id,
|
|
339
|
+
)
|
|
340
|
+
if receipt is not None and (
|
|
341
|
+
receipt.get("workerId") != owner.worker_id
|
|
342
|
+
or receipt.get("engine") != "codex"
|
|
343
|
+
or receipt.get("generation") != store.generation
|
|
344
|
+
or receipt.get("family") != "codex-compact"
|
|
345
|
+
or receipt.get("action") != "compact"
|
|
346
|
+
or receipt.get("intentSha256") != digest
|
|
347
|
+
):
|
|
348
|
+
raise SessionUnavailable("Codex compaction record changed; inspect its operation",
|
|
349
|
+
short=owner.short)
|
|
350
|
+
|
|
351
|
+
result = "not_found" if state is None else "unknown"
|
|
352
|
+
if state == "prepared" and kind is None:
|
|
353
|
+
result = "prepared"
|
|
354
|
+
elif state == "rejected" and kind is None:
|
|
355
|
+
result = "rejected"
|
|
356
|
+
elif state in ("submitted-unknown", "accepted"):
|
|
357
|
+
if kind == "active":
|
|
358
|
+
result = "submitted"
|
|
359
|
+
elif kind == "last":
|
|
360
|
+
settled = (
|
|
361
|
+
marker.get("nativeTurnStarted") is True
|
|
362
|
+
and isinstance(marker.get("turnId"), str) and bool(marker["turnId"])
|
|
363
|
+
and isinstance(marker.get("contextItemId"), str)
|
|
364
|
+
and bool(marker["contextItemId"])
|
|
365
|
+
and marker.get("nativeTurnSettled") is True
|
|
366
|
+
and marker.get("nativeTurnStatus") == "completed"
|
|
367
|
+
and marker.get("status") == "succeeded"
|
|
368
|
+
)
|
|
369
|
+
if settled:
|
|
370
|
+
result = "succeeded"
|
|
371
|
+
elif (marker.get("nativeTurnSettled") is True
|
|
372
|
+
and marker.get("nativeTurnStatus") in (
|
|
373
|
+
"failed", "interrupted", "cancelled"
|
|
374
|
+
)):
|
|
375
|
+
result = "failed"
|
|
376
|
+
else:
|
|
377
|
+
result = "unknown"
|
|
378
|
+
elif kind is not None:
|
|
379
|
+
# A marker without an authenticated original effect is never proof.
|
|
380
|
+
result = "unknown"
|
|
381
|
+
|
|
382
|
+
before = marker.get("contextBefore") if isinstance(marker, dict) else None
|
|
383
|
+
after = marker.get("contextAfter") if isinstance(marker, dict) else None
|
|
384
|
+
completed = marker.get("completedAtMs") if isinstance(marker, dict) else None
|
|
385
|
+
if (result != "succeeded" or not isinstance(after, dict)
|
|
386
|
+
or type(completed) is not int
|
|
387
|
+
or type(after.get("as_of_ms")) is not int
|
|
388
|
+
or after["as_of_ms"] <= completed
|
|
389
|
+
or (isinstance(before, dict)
|
|
390
|
+
and type(before.get("as_of_ms")) is int
|
|
391
|
+
and after["as_of_ms"] <= before["as_of_ms"])):
|
|
392
|
+
after = None
|
|
393
|
+
return {
|
|
394
|
+
"worker": {"worker": owner.worker, "worker_id": owner.worker_id,
|
|
395
|
+
"short": owner.short, "engine": "codex"},
|
|
396
|
+
"operationId": operation_id, "operation_id": operation_id,
|
|
397
|
+
"status": result, "compacted": result == "succeeded",
|
|
398
|
+
"receipt_status": state,
|
|
399
|
+
"recommendedAction": ("wait" if result in ("submitted", "prepared")
|
|
400
|
+
else "inspect" if result == "unknown" else "none"),
|
|
401
|
+
"contextBefore": before if isinstance(before, dict) else None,
|
|
402
|
+
"contextAfter": after,
|
|
403
|
+
"compactTurnId": marker.get("turnId") if isinstance(marker, dict) else None,
|
|
404
|
+
"contextItemId": marker.get("contextItemId") if isinstance(marker, dict) else None,
|
|
405
|
+
"nativeTurnStatus": (marker.get("nativeTurnStatus")
|
|
406
|
+
if isinstance(marker, dict) else None),
|
|
407
|
+
"reason": (marker.get("reason") if isinstance(marker, dict) else None),
|
|
408
|
+
"receipt": ({"operation_id": operation_id, "state": state,
|
|
409
|
+
"native_evidence_sha256": receipt.get("nativeEvidenceSha256")}
|
|
410
|
+
if receipt else None),
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def compact_observation(target: str, operation_id: str) -> dict[str, Any]:
|
|
415
|
+
"""Read only a generation-native effect and exact worker markers."""
|
|
416
|
+
from ccx import constants
|
|
417
|
+
from ccx.store.root import require_ccx_active, NativeActionDenied
|
|
418
|
+
|
|
419
|
+
if (not isinstance(operation_id, str) or not operation_id
|
|
420
|
+
or len(operation_id) > 128 or "\x00" in operation_id):
|
|
421
|
+
raise InvalidRequest("Codex compact operation_id must be 1..128 non-NUL characters")
|
|
422
|
+
store = require_ccx_active(constants.CCX_ROOT)
|
|
423
|
+
owner = store.resolve(target)
|
|
424
|
+
if owner.engine != "codex":
|
|
425
|
+
raise InvalidRequest("Codex compaction needs a worker handle")
|
|
426
|
+
rec = store.record(owner.worker)
|
|
427
|
+
native = rec.get("native") or {}
|
|
428
|
+
binding = store.native_activation(owner.worker)
|
|
429
|
+
source = store.codex_source(owner.worker)
|
|
430
|
+
if (binding.get("state") != "confirmed"
|
|
431
|
+
or binding.get("source") != source
|
|
432
|
+
or (binding.get("nativeBinding") or {}).get("threadId") != native.get("threadId")
|
|
433
|
+
or not isinstance(native.get("threadId"), str)
|
|
434
|
+
or not native["threadId"]
|
|
435
|
+
or native.get("threadSource") != source):
|
|
436
|
+
raise NativeActionDenied("Codex conversation could not be verified; inspect worker status")
|
|
437
|
+
receipt = store.read_receipt(owner.worker, "codex-compact", operation_id)
|
|
438
|
+
row = _compact_view(store, owner, operation_id, receipt)
|
|
439
|
+
store.revalidate()
|
|
440
|
+
return row
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def compact_worker(
|
|
444
|
+
target: str, *, operation_id: str, timeout: float | None = 90,
|
|
445
|
+
wait: bool = True,
|
|
446
|
+
) -> dict[str, Any]:
|
|
447
|
+
"""One original Codex native compaction, never a legacy short-keyed RPC."""
|
|
448
|
+
from ccx import constants
|
|
449
|
+
from ccx.store.root import NativeEffectIndeterminate, NativeActionDenied, require_ccx_active
|
|
450
|
+
from ccx.store.store import _digest
|
|
451
|
+
from ccx.ops.send import ensure_ready
|
|
452
|
+
from ccx.codex.runtime import daemon_call
|
|
453
|
+
|
|
454
|
+
if (not isinstance(operation_id, str) or not operation_id
|
|
455
|
+
or len(operation_id) > 128 or "\x00" in operation_id):
|
|
456
|
+
raise InvalidRequest("Codex compact requires a valid operation_id")
|
|
457
|
+
if timeout is not None and (type(timeout) not in (int, float)
|
|
458
|
+
or timeout < 0 or timeout != timeout
|
|
459
|
+
or timeout == float("inf")):
|
|
460
|
+
raise InvalidRequest("Codex compact timeout must be finite nonnegative seconds")
|
|
461
|
+
deadline = _deadline(timeout)
|
|
462
|
+
store = require_ccx_active(constants.CCX_ROOT)
|
|
463
|
+
owner = store.resolve(target)
|
|
464
|
+
if owner.engine != "codex":
|
|
465
|
+
raise InvalidRequest("Codex compact requires an exact Codex owner")
|
|
466
|
+
handle = owner.worker
|
|
467
|
+
rec = store.record(handle)
|
|
468
|
+
native = rec.get("native") or {}
|
|
469
|
+
source = store.codex_source(owner.worker)
|
|
470
|
+
thread_id = native.get("threadId")
|
|
471
|
+
binding = store.native_activation(handle)
|
|
472
|
+
if (binding.get("state") != "confirmed"
|
|
473
|
+
or binding.get("source") != source
|
|
474
|
+
or (binding.get("nativeBinding") or {}).get("threadId") != thread_id
|
|
475
|
+
or not isinstance(thread_id, str) or not thread_id
|
|
476
|
+
or native.get("threadSource") != source):
|
|
477
|
+
raise NativeActionDenied("Codex conversation is not ready for compaction; check status")
|
|
478
|
+
digest = compact_intent_digest(
|
|
479
|
+
owner.worker_id, store.generation, thread_id, source, operation_id,
|
|
480
|
+
)
|
|
481
|
+
prior = store.read_receipt(handle, "codex-compact", operation_id)
|
|
482
|
+
row = _compact_view(store, owner, operation_id, prior)
|
|
483
|
+
if prior is None:
|
|
484
|
+
# An exact native marker without an original global receipt is
|
|
485
|
+
# untrusted possible work, never permission for a fresh submit.
|
|
486
|
+
if _compact_marker(store.record(handle), operation_id)[0] is not None:
|
|
487
|
+
return _wait_for_compact(row, handle, operation_id, deadline, wait)
|
|
488
|
+
elif prior["state"] != "prepared" or row["status"] == "unknown":
|
|
489
|
+
return _wait_for_compact(row, handle, operation_id, deadline, wait)
|
|
490
|
+
|
|
491
|
+
# Genuine fresh/prepared action may promptlessly load its original thread.
|
|
492
|
+
# Submitted/replayed actions are reconciliation-only, never a load.
|
|
493
|
+
store.authorize_native_action(handle, "compact")
|
|
494
|
+
left = None if deadline is None else deadline - time.monotonic()
|
|
495
|
+
if left is not None and left <= 0:
|
|
496
|
+
raise SessionUnavailable("Codex compaction did not start before timeout; retry when ready",
|
|
497
|
+
short=owner.short)
|
|
498
|
+
ensure_ready(
|
|
499
|
+
handle, expected_generation=store.generation,
|
|
500
|
+
timeout=90 if left is None else min(90, left),
|
|
501
|
+
)
|
|
502
|
+
store.revalidate()
|
|
503
|
+
intent = store.begin_native_intent(
|
|
504
|
+
handle, family="codex-compact", action="compact",
|
|
505
|
+
operation_id=operation_id, intent_sha256=digest,
|
|
506
|
+
)
|
|
507
|
+
if intent.replayed:
|
|
508
|
+
row = compact_observation(handle, operation_id)
|
|
509
|
+
return _wait_for_compact(row, handle, operation_id, deadline, wait)
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
answer = store.dispatch_native_intent_once(
|
|
513
|
+
intent,
|
|
514
|
+
bounded_submit=lambda budget: daemon_call({
|
|
515
|
+
"op": "compact", "short": owner.worker_id,
|
|
516
|
+
"operationId": operation_id, "threadId": thread_id,
|
|
517
|
+
}, timeout=budget, short=owner.short),
|
|
518
|
+
dispatch_budget_s=2.,
|
|
519
|
+
)
|
|
520
|
+
except NativeEffectIndeterminate:
|
|
521
|
+
# IPC may have landed despite caller-side loss of its positive ACK.
|
|
522
|
+
answer = None
|
|
523
|
+
|
|
524
|
+
if (isinstance(answer, dict) and answer.get("transport") == "served"
|
|
525
|
+
and (answer.get("ok") is True or
|
|
526
|
+
(answer.get("noEffect") is True and answer.get("definitive") is True))):
|
|
527
|
+
# Ambiguous served errors carry no positive native admission proof.
|
|
528
|
+
# Read the exact original markers instead of treating them as an ACK.
|
|
529
|
+
identity = {
|
|
530
|
+
"workerId": owner.worker_id, "engine": "codex",
|
|
531
|
+
"generation": store.generation, "nativeSource": source,
|
|
532
|
+
"threadId": thread_id, "operationId": operation_id,
|
|
533
|
+
}
|
|
534
|
+
if (any(answer.get(key) != value for key, value in identity.items())
|
|
535
|
+
or type(answer.get("startedAtMs")) is not int
|
|
536
|
+
or answer.get("nativeEvidenceSha256") != _digest({
|
|
537
|
+
**identity, "startedAtMs": answer.get("startedAtMs"),
|
|
538
|
+
"admission": "thread/compact/start"
|
|
539
|
+
})):
|
|
540
|
+
raise NativeEffectIndeterminate("Codex compaction response does not match this worker; inspect status")
|
|
541
|
+
if answer.get("ok") is True and answer.get("nativeStatus") == "accepted":
|
|
542
|
+
store.settle_native_intent(
|
|
543
|
+
intent, state="accepted",
|
|
544
|
+
native_evidence_sha256=answer["nativeEvidenceSha256"],
|
|
545
|
+
)
|
|
546
|
+
elif (answer.get("ok") is False
|
|
547
|
+
and answer.get("noEffect") is True and answer.get("definitive") is True):
|
|
548
|
+
record = store.record(handle)
|
|
549
|
+
if _compact_marker(record, operation_id)[0] is not None:
|
|
550
|
+
raise NativeEffectIndeterminate("Codex compaction records disagree; inspect operation status")
|
|
551
|
+
store.settle_native_intent(
|
|
552
|
+
intent, state="rejected",
|
|
553
|
+
native_evidence_sha256=answer["nativeEvidenceSha256"],
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
row = compact_observation(handle, operation_id)
|
|
557
|
+
return _wait_for_compact(row, handle, operation_id, deadline, wait)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _wait_for_compact(
|
|
561
|
+
row: dict, handle: str, operation_id: str, deadline: float | None,
|
|
562
|
+
wait: bool,
|
|
563
|
+
) -> dict:
|
|
564
|
+
if not wait:
|
|
565
|
+
# The exact-ID observation is already truthful, including a settled replay.
|
|
566
|
+
return row
|
|
567
|
+
while row["status"] in ("submitted", "prepared") and (
|
|
568
|
+
deadline is None or time.monotonic() < deadline
|
|
569
|
+
):
|
|
570
|
+
time.sleep(.1 if deadline is None else min(.1, max(0, deadline-time.monotonic())))
|
|
571
|
+
row = compact_observation(handle, operation_id)
|
|
572
|
+
return row
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def telemetry_worker(
|
|
580
|
+
target: str,
|
|
581
|
+
*,
|
|
582
|
+
after_cursor: str | None = None,
|
|
583
|
+
limit: int = 200,
|
|
584
|
+
timeout: float = 120,
|
|
585
|
+
) -> dict[str, Any]:
|
|
586
|
+
return read_telemetry(
|
|
587
|
+
target,
|
|
588
|
+
after_cursor=after_cursor,
|
|
589
|
+
limit=limit,
|
|
590
|
+
timeout=timeout,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _iso_reset(value: Any) -> str | None:
|
|
601
|
+
if not isinstance(value, (int, float)) or isinstance(value, bool):
|
|
602
|
+
return None
|
|
603
|
+
return datetime.fromtimestamp(value, timezone.utc).strftime(
|
|
604
|
+
"%Y-%m-%dT%H:%M:%SZ"
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def account_usage_read(*, timeout: float = 30) -> dict[str, Any]:
|
|
609
|
+
if timeout <= 0:
|
|
610
|
+
raise InvalidRequest("usage timeout must be greater than zero")
|
|
611
|
+
raw = runtime.read_account_usage(tokens=False, timeout=timeout)
|
|
612
|
+
by_id = raw.get("rateLimitsByLimitId")
|
|
613
|
+
if isinstance(by_id, dict) and by_id:
|
|
614
|
+
buckets = sorted(
|
|
615
|
+
by_id.items(), key=lambda item: str(item[0]).casefold()
|
|
616
|
+
)
|
|
617
|
+
else:
|
|
618
|
+
buckets = [("codex", raw.get("rateLimits"))]
|
|
619
|
+
|
|
620
|
+
windows: list[dict[str, Any]] = []
|
|
621
|
+
for limit_id, bucket in buckets:
|
|
622
|
+
if not isinstance(bucket, dict):
|
|
623
|
+
continue
|
|
624
|
+
for kind in ("primary", "secondary"):
|
|
625
|
+
window = bucket.get(kind)
|
|
626
|
+
if not isinstance(window, dict):
|
|
627
|
+
continue
|
|
628
|
+
used = window.get("usedPercent")
|
|
629
|
+
used_percent = (
|
|
630
|
+
float(used)
|
|
631
|
+
if isinstance(used, (int, float)) and not isinstance(used, bool)
|
|
632
|
+
else None
|
|
633
|
+
)
|
|
634
|
+
minutes = window.get("windowDurationMins")
|
|
635
|
+
duration_seconds = (
|
|
636
|
+
int(minutes) * 60
|
|
637
|
+
if isinstance(minutes, int)
|
|
638
|
+
and not isinstance(minutes, bool)
|
|
639
|
+
and minutes > 0
|
|
640
|
+
else None
|
|
641
|
+
)
|
|
642
|
+
windows.append({
|
|
643
|
+
"provider": "codex",
|
|
644
|
+
"window_id": f"{limit_id or 'codex'}:{kind}",
|
|
645
|
+
"limit_id": str(limit_id or "codex"),
|
|
646
|
+
"kind": kind,
|
|
647
|
+
"window_duration_seconds": duration_seconds,
|
|
648
|
+
"used_percent": used_percent,
|
|
649
|
+
"remaining_percent": (
|
|
650
|
+
max(0.0, 100.0 - used_percent)
|
|
651
|
+
if used_percent is not None else None
|
|
652
|
+
),
|
|
653
|
+
"resets_at": _iso_reset(window.get("resetsAt")),
|
|
654
|
+
})
|
|
655
|
+
return {
|
|
656
|
+
"provider": "codex",
|
|
657
|
+
"observed_at_ms": int(time.time() * 1000),
|
|
658
|
+
"windows": windows,
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
__all__ = [
|
|
663
|
+
"account_usage_read",
|
|
664
|
+
"compact_worker",
|
|
665
|
+
"telemetry_worker",
|
|
666
|
+
]
|