ccx-native 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +240 -4
- package/bin/ccx +55 -2
- package/ccx/__init__.py +167 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +807 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +269 -0
- package/ccx/claude/host/session.py +5244 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2535 -0
- package/ccx/claude/usage.py +796 -0
- package/ccx/claude/usage_control.py +204 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/models.py +53 -0
- package/ccx/cli/commands/read.py +1104 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +302 -0
- package/ccx/cli/parser.py +1439 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1904 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +738 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1558 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2424 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/history_store.py +131 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1454 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/models.py +225 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +296 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1593 -0
- package/ccx/ops/conversation_paging.py +1339 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +575 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/history.py +1041 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +773 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +348 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +640 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +250 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +789 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/private_json.py +54 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +809 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2093 -0
- package/ccx.py +9 -0
- package/package.json +39 -7
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +166 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/history.md +50 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- package/skills/ccx/references/variables.md +62 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
"""Python client for CCX-owned Codex workers.
|
|
2
|
+
|
|
3
|
+
Operations require an active owner store and exact worker UUIDs. Native
|
|
4
|
+
thread IDs are provider bindings, not worker targets. Construction verifies
|
|
5
|
+
the store without launching a provider process.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from ccx.codex.ops import configure_worker, telemetry_worker
|
|
15
|
+
from ccx.codex.paths import ensure_dirs
|
|
16
|
+
from ccx.codex.runtime import (
|
|
17
|
+
record,
|
|
18
|
+
remaining,
|
|
19
|
+
read_account_usage,
|
|
20
|
+
require_workspace,
|
|
21
|
+
rpc,
|
|
22
|
+
validate_model,
|
|
23
|
+
wait as wait_for_stable,
|
|
24
|
+
)
|
|
25
|
+
from ccx.codex.settle import (
|
|
26
|
+
DEFAULT_WAKE_GRACE_S,
|
|
27
|
+
wait_for_settlement,
|
|
28
|
+
)
|
|
29
|
+
from ccx.codex.types import (
|
|
30
|
+
CodexApprovalResult,
|
|
31
|
+
CodexCompactResult,
|
|
32
|
+
CodexContext,
|
|
33
|
+
CodexControlResult,
|
|
34
|
+
CodexReport,
|
|
35
|
+
CodexReconcileResult,
|
|
36
|
+
CodexResumeResult,
|
|
37
|
+
CodexSessionInfo,
|
|
38
|
+
CodexTelemetryPage,
|
|
39
|
+
CodexTurnResult,
|
|
40
|
+
CodexUsage,
|
|
41
|
+
CodexWaitResult,
|
|
42
|
+
)
|
|
43
|
+
from ccx.constants import (
|
|
44
|
+
COMPACT_WAIT_DEFAULT_S,
|
|
45
|
+
DEFAULT_IDLE_TIMEOUT_S,
|
|
46
|
+
)
|
|
47
|
+
from ccx.types import ModelCatalog
|
|
48
|
+
from ccx.errors import (
|
|
49
|
+
DeliveryRejected,
|
|
50
|
+
InvalidRequest,
|
|
51
|
+
SessionUnavailable,
|
|
52
|
+
SessionWaiting,
|
|
53
|
+
SendIndeterminate,
|
|
54
|
+
TurnIncomplete,
|
|
55
|
+
worker_not_found,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
_SANDBOXES = {"read-only", "workspace-write", "danger-full-access"}
|
|
59
|
+
_APPROVAL_POLICIES = {"untrusted", "on-request", "never"}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CodexClient:
|
|
63
|
+
"""Codex operations over the same owner store and receipts as the CLI."""
|
|
64
|
+
|
|
65
|
+
def __init__(self) -> None:
|
|
66
|
+
"""Require the active owner store before any operation can write paths."""
|
|
67
|
+
from ccx.constants import CCX_ROOT
|
|
68
|
+
from ccx.store.root import GenerationError, open_or_initialize
|
|
69
|
+
try:
|
|
70
|
+
self._uuid_store, _created = open_or_initialize(CCX_ROOT)
|
|
71
|
+
except (GenerationError, OSError) as exc:
|
|
72
|
+
raise SessionUnavailable(
|
|
73
|
+
f"CCX worker records are unavailable; check CCX_ROOT: {exc}"
|
|
74
|
+
) from exc
|
|
75
|
+
def _owned_codex(self, target: str) -> tuple[str, dict]:
|
|
76
|
+
"""Positive global full UUID owner, never native thread/name/short."""
|
|
77
|
+
from ccx.store.identity import IdentityError,UnknownWorker
|
|
78
|
+
from ccx.store.root import GenerationError
|
|
79
|
+
from ccx.errors import SessionNotFound
|
|
80
|
+
try:
|
|
81
|
+
self._uuid_store.revalidate()
|
|
82
|
+
owner=self._uuid_store.resolve(target)
|
|
83
|
+
if owner.engine!="codex":
|
|
84
|
+
raise InvalidRequest("CodexClient requires Codex full UUID")
|
|
85
|
+
return owner.worker,self._uuid_store.record(owner.worker)
|
|
86
|
+
except UnknownWorker as exc:
|
|
87
|
+
raise worker_not_found(target) from exc
|
|
88
|
+
except IdentityError as exc:
|
|
89
|
+
raise InvalidRequest(f"Invalid Codex worker handle: {exc}") from exc
|
|
90
|
+
except GenerationError as exc:
|
|
91
|
+
raise SessionUnavailable(f"CCX records changed; reopen the worker: {exc}") from exc
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def create(
|
|
95
|
+
self, prompt: str | None = None, *, cwd: str | None = None,
|
|
96
|
+
name: str | None = None, worker_id: str | None = None,
|
|
97
|
+
model: str | None = None, effort: str | None = None,
|
|
98
|
+
idle_timeout: int = DEFAULT_IDLE_TIMEOUT_S,
|
|
99
|
+
sandbox: str = "danger-full-access", approval_policy: str = "never",
|
|
100
|
+
wait: bool = False, timeout: float | None = None,
|
|
101
|
+
delivery_id: str | None = None,
|
|
102
|
+
start_timeout: float | None = None,
|
|
103
|
+
caps: dict | None = None,
|
|
104
|
+
) -> CodexSessionInfo | CodexTurnResult:
|
|
105
|
+
"""Create one verified original Codex thread, never a speculative resume."""
|
|
106
|
+
from ccx.ops.creation import create
|
|
107
|
+
result = create(
|
|
108
|
+
engine="codex",worker_id=worker_id,cwd=cwd or os.getcwd(),
|
|
109
|
+
prompt=prompt,delivery_id=delivery_id,wait=wait,caps=caps,
|
|
110
|
+
name=name,model=model,effort=effort,
|
|
111
|
+
idle_timeout=idle_timeout,sandbox=sandbox,
|
|
112
|
+
approval_policy=approval_policy,created_via="python-api",
|
|
113
|
+
timeout=timeout,start_timeout=start_timeout,
|
|
114
|
+
expected_generation=self._uuid_store.generation,
|
|
115
|
+
)
|
|
116
|
+
if wait:
|
|
117
|
+
report = result["report"]
|
|
118
|
+
turn_id = report["turn_id"]
|
|
119
|
+
return CodexTurnResult.from_record(
|
|
120
|
+
result["record"], turn_id=turn_id,
|
|
121
|
+
report={"turnId": turn_id, "phase": "completed",
|
|
122
|
+
"text": report["text"],
|
|
123
|
+
"completedAt": report.get("settled_at_ms")},
|
|
124
|
+
last_turn={"turnId": turn_id, "status": "completed"},
|
|
125
|
+
client_user_message_id=result["delivery"].get("client_user_message_id"),
|
|
126
|
+
delivery_id=result["delivery_id"],
|
|
127
|
+
)
|
|
128
|
+
# This result was verified by the shared application operation.
|
|
129
|
+
# Avoid a second fallible store read after first-turn submission.
|
|
130
|
+
if prompt is None:
|
|
131
|
+
self._uuid_store.revalidate()
|
|
132
|
+
return CodexSessionInfo.from_record(result["record"])
|
|
133
|
+
|
|
134
|
+
def fork(
|
|
135
|
+
self, target: str, prompt: str | None = None, *,
|
|
136
|
+
name: str | None = None, worker_id: str | None = None,
|
|
137
|
+
model: str | None = None, effort: str | None = None,
|
|
138
|
+
wait: bool = False, timeout: float | None = None,
|
|
139
|
+
delivery_id: str | None = None,
|
|
140
|
+
caps: dict | None = None,
|
|
141
|
+
) -> CodexSessionInfo | CodexTurnResult:
|
|
142
|
+
"""Fork a Codex worker into a new worker with its history.
|
|
143
|
+
|
|
144
|
+
Native ``thread/fork`` through the source's last settled turn; the
|
|
145
|
+
fork keeps the source's cwd, sandbox, approval policy, model and
|
|
146
|
+
effort unless overridden. Returns like :meth:`create`. Retrying with
|
|
147
|
+
the same ``worker_id`` reuses the original fork point.
|
|
148
|
+
"""
|
|
149
|
+
from math import isfinite
|
|
150
|
+
from ccx.ops.forking import fork
|
|
151
|
+
from ccx.store.root import GenerationError
|
|
152
|
+
from ccx.store.identity import IdentityError, UnknownWorker, parse_managed_target
|
|
153
|
+
from ccx.errors import SessionNotFound
|
|
154
|
+
|
|
155
|
+
if timeout is not None and (
|
|
156
|
+
type(timeout) not in (int, float) or not isfinite(timeout) or timeout <= 0
|
|
157
|
+
):
|
|
158
|
+
raise InvalidRequest("fork timeout must be positive finite seconds or None")
|
|
159
|
+
deadline = time.monotonic() + timeout if timeout is not None else None
|
|
160
|
+
try:
|
|
161
|
+
source = parse_managed_target(target).qualified("codex")
|
|
162
|
+
result = fork(
|
|
163
|
+
source, worker_id=worker_id, name=name, model=model, effort=effort,
|
|
164
|
+
created_via="python-api", expected_generation=self._uuid_store.generation,
|
|
165
|
+
deadline=deadline, prompt=prompt, wait=wait, delivery_id=delivery_id,
|
|
166
|
+
caps=caps,
|
|
167
|
+
)
|
|
168
|
+
if wait:
|
|
169
|
+
report = result["report"]
|
|
170
|
+
turn_id = report["turn_id"]
|
|
171
|
+
return CodexTurnResult.from_record(
|
|
172
|
+
result["record"], turn_id=turn_id,
|
|
173
|
+
report={"turnId": turn_id, "phase": "completed",
|
|
174
|
+
"text": report["text"],
|
|
175
|
+
"completedAt": report.get("settled_at_ms")},
|
|
176
|
+
last_turn={"turnId": turn_id, "status": "completed"},
|
|
177
|
+
client_user_message_id=result["delivery"].get("client_user_message_id"),
|
|
178
|
+
delivery_id=result["delivery_id"],
|
|
179
|
+
)
|
|
180
|
+
if prompt is None:
|
|
181
|
+
self._uuid_store.revalidate()
|
|
182
|
+
return CodexSessionInfo.from_record(result["record"])
|
|
183
|
+
except UnknownWorker as exc:
|
|
184
|
+
raise worker_not_found(target) from exc
|
|
185
|
+
except IdentityError as exc:
|
|
186
|
+
raise InvalidRequest(f"fork: Invalid Codex worker handle: {exc}") from exc
|
|
187
|
+
except GenerationError as exc:
|
|
188
|
+
raise SessionUnavailable(f"fork: Codex ACTIVE generation unavailable: {exc}") from exc
|
|
189
|
+
|
|
190
|
+
def resume(
|
|
191
|
+
self,
|
|
192
|
+
target: str,
|
|
193
|
+
*,
|
|
194
|
+
timeout: float | None = 90,
|
|
195
|
+
) -> CodexResumeResult:
|
|
196
|
+
"""Re-subscribe only the original full-UUID native Codex thread.
|
|
197
|
+
|
|
198
|
+
No model turn is started by explicit resume. A confirmed idle native
|
|
199
|
+
subscription, not read-only thread history, is the success condition.
|
|
200
|
+
"""
|
|
201
|
+
from ccx.ops.send import ensure_ready
|
|
202
|
+
handle, _record = self._owned_codex(target)
|
|
203
|
+
ready = ensure_ready(
|
|
204
|
+
handle, timeout=timeout,
|
|
205
|
+
expected_generation=self._uuid_store.generation,
|
|
206
|
+
)
|
|
207
|
+
return CodexResumeResult.from_record(
|
|
208
|
+
self._uuid_store.record(handle), resumed=ready["resumed"],
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def set_name(self, target: str, name: str | None) -> CodexSessionInfo:
|
|
212
|
+
"""Rename an exact Codex full UUID with one fresh semantic record CAS."""
|
|
213
|
+
from ccx.store.identity import EngineMismatch, IdentityError, UnknownWorker
|
|
214
|
+
from ccx.store.root import GenerationError
|
|
215
|
+
try:
|
|
216
|
+
owner = self._uuid_store.resolve(target)
|
|
217
|
+
if owner.engine != "codex":
|
|
218
|
+
raise EngineMismatch("CodexClient requires a Codex full-UUID owner")
|
|
219
|
+
rec = self._uuid_store.rename_worker(
|
|
220
|
+
owner.worker, engine="codex", name=name,
|
|
221
|
+
)
|
|
222
|
+
return CodexSessionInfo.from_record(rec)
|
|
223
|
+
except UnknownWorker as exc:
|
|
224
|
+
from ccx.errors import SessionNotFound
|
|
225
|
+
raise worker_not_found(target) from exc
|
|
226
|
+
except IdentityError as exc:
|
|
227
|
+
raise InvalidRequest(f"invalid Codex worker full UUID: {exc}") from exc
|
|
228
|
+
except GenerationError as exc:
|
|
229
|
+
raise SessionUnavailable(f"Worker records changed; retry with the saved handle: {exc}") from exc
|
|
230
|
+
|
|
231
|
+
def set_model(
|
|
232
|
+
self,
|
|
233
|
+
target: str,
|
|
234
|
+
model: str | None,
|
|
235
|
+
*,
|
|
236
|
+
timeout: float = 5,
|
|
237
|
+
) -> CodexSessionInfo:
|
|
238
|
+
"""Set the model used by future turns without starting a turn."""
|
|
239
|
+
worker, _record = self._owned_codex(target)
|
|
240
|
+
value = configure_worker(
|
|
241
|
+
worker, model=model, timeout=timeout,
|
|
242
|
+
expected_generation=self._uuid_store.generation,
|
|
243
|
+
)
|
|
244
|
+
self._uuid_store.revalidate()
|
|
245
|
+
return CodexSessionInfo.from_record(value)
|
|
246
|
+
|
|
247
|
+
def set_effort(
|
|
248
|
+
self,
|
|
249
|
+
target: str,
|
|
250
|
+
effort: str | None,
|
|
251
|
+
*,
|
|
252
|
+
timeout: float = 5,
|
|
253
|
+
) -> CodexSessionInfo:
|
|
254
|
+
"""Set future-turn reasoning effort without starting a turn."""
|
|
255
|
+
worker, _record = self._owned_codex(target)
|
|
256
|
+
value = configure_worker(
|
|
257
|
+
worker, effort=effort, timeout=timeout,
|
|
258
|
+
expected_generation=self._uuid_store.generation,
|
|
259
|
+
)
|
|
260
|
+
self._uuid_store.revalidate()
|
|
261
|
+
return CodexSessionInfo.from_record(value)
|
|
262
|
+
|
|
263
|
+
def send(
|
|
264
|
+
self, target: str, text: str, *, delivery_id: str | None = None,
|
|
265
|
+
wait: bool = True, timeout: float | None = None, caps: dict | None = None,
|
|
266
|
+
) -> CodexTurnResult:
|
|
267
|
+
"""One original native Codex app-server turn, full-UUID only.
|
|
268
|
+
|
|
269
|
+
The original delivery_id and clientUserMessageId are immutable.
|
|
270
|
+
Timeout or a lost app-server reply retains the exact native effect
|
|
271
|
+
fence; never guess a missing turn/start was not submitted.
|
|
272
|
+
"""
|
|
273
|
+
from ccx.ops.send import send_turn as _send
|
|
274
|
+
from ccx.ops.wait import reconcile_turn
|
|
275
|
+
from ccx.ops.report import report_worker
|
|
276
|
+
from ccx.store.root import NativeEffectIndeterminate, GenerationError
|
|
277
|
+
from ccx.codex.delivery import DeliveryReceiptConflict
|
|
278
|
+
from ccx.errors import SendIndeterminate,OperationConflict,DeliveryRejected
|
|
279
|
+
from uuid import uuid4
|
|
280
|
+
if timeout is not None and (not isinstance(timeout,(int,float))
|
|
281
|
+
or timeout<=0):
|
|
282
|
+
raise InvalidRequest("Codex send timeout must be positive or None")
|
|
283
|
+
worker,record=self._owned_codex(target)
|
|
284
|
+
token=delivery_id if delivery_id is not None else str(uuid4())
|
|
285
|
+
deadline=None if timeout is None else time.monotonic()+timeout
|
|
286
|
+
try:
|
|
287
|
+
accepted=_send(worker,text,delivery_id=token,mode="new_turn_only",cap=caps)
|
|
288
|
+
except DeliveryReceiptConflict as exc:
|
|
289
|
+
raise OperationConflict(
|
|
290
|
+
f"Codex original delivery {token!r} conflicts: {exc}",
|
|
291
|
+
short=worker[6:14],worker_id=worker.split(":",1)[1],
|
|
292
|
+
) from exc
|
|
293
|
+
except NativeEffectIndeterminate as exc:
|
|
294
|
+
raise SendIndeterminate(
|
|
295
|
+
"native Codex turn effect unknown; original delivery must "
|
|
296
|
+
f"be reconciled without replay: {exc}",
|
|
297
|
+
short=worker.split(":",1)[1][:8],
|
|
298
|
+
delivery_id=token,delivery_status="submitted-unknown",
|
|
299
|
+
) from exc
|
|
300
|
+
except GenerationError as exc:
|
|
301
|
+
raise SessionUnavailable(
|
|
302
|
+
f"full-UUID Codex native send unavailable: {exc}"
|
|
303
|
+
) from exc
|
|
304
|
+
result=accepted
|
|
305
|
+
if wait:
|
|
306
|
+
while True:
|
|
307
|
+
self._uuid_store.revalidate()
|
|
308
|
+
result=reconcile_turn(
|
|
309
|
+
worker, delivery_id=token,
|
|
310
|
+
timeout=(0.5 if deadline is None else
|
|
311
|
+
max(0.001, min(0.5, deadline-time.monotonic()))),
|
|
312
|
+
)
|
|
313
|
+
if result["status"] in ("settled","rejected"):
|
|
314
|
+
break
|
|
315
|
+
from ccx.codex.settle import send_settle_failure
|
|
316
|
+
failure = send_settle_failure(worker, result)
|
|
317
|
+
if failure is not None:
|
|
318
|
+
detail = failure.get("error") or failure["status"]
|
|
319
|
+
if failure["status"] == "budget-stopped":
|
|
320
|
+
from ccx.errors import BudgetStopped
|
|
321
|
+
raise BudgetStopped(
|
|
322
|
+
f"budget stop: {detail}", short=worker.split(":", 1)[1][:8],
|
|
323
|
+
reason=str(detail), phase="turn",
|
|
324
|
+
)
|
|
325
|
+
if failure["status"] == "waiting":
|
|
326
|
+
preview = failure.get("pending_input")
|
|
327
|
+
raise SessionWaiting(
|
|
328
|
+
str(detail), short=worker.split(":", 1)[1][:8],
|
|
329
|
+
waiting_for=({**preview, "type": preview.get("kind"),
|
|
330
|
+
"id": preview.get("request_id")}
|
|
331
|
+
if preview is not None else None),
|
|
332
|
+
)
|
|
333
|
+
delivery = result.get("delivery") or {}
|
|
334
|
+
raise TurnIncomplete(
|
|
335
|
+
str(detail), short=worker.split(":", 1)[1][:8],
|
|
336
|
+
state=failure["status"], turn_id=delivery.get("turn_id"),
|
|
337
|
+
turn_status=(failure["status"]
|
|
338
|
+
if failure["status"] in {"failed", "interrupted", "cancelled"}
|
|
339
|
+
else result.get("native_status")),
|
|
340
|
+
turn_error=failure.get("error"),
|
|
341
|
+
)
|
|
342
|
+
if deadline is not None and time.monotonic()>=deadline:
|
|
343
|
+
raise SendIndeterminate(
|
|
344
|
+
"Codex turn not positively completed within caller "
|
|
345
|
+
f"deadline; reconcile original delivery {token!r}",
|
|
346
|
+
short=worker.split(":",1)[1][:8],
|
|
347
|
+
delivery_id=token,delivery_status=result["status"],
|
|
348
|
+
)
|
|
349
|
+
time.sleep(.1)
|
|
350
|
+
delivery=result.get("delivery") or {}
|
|
351
|
+
if result["status"]=="rejected":
|
|
352
|
+
raise DeliveryRejected(
|
|
353
|
+
delivery.get("rejection_message") or "native Codex refused",
|
|
354
|
+
short=worker.split(":",1)[1][:8],
|
|
355
|
+
refusal=delivery.get("rejection_code"),
|
|
356
|
+
delivery_id=token,delivery_status="rejected",
|
|
357
|
+
)
|
|
358
|
+
turn_id=delivery.get("turn_id")
|
|
359
|
+
client_id=delivery.get("client_user_message_id")
|
|
360
|
+
report=None
|
|
361
|
+
final_turn=None
|
|
362
|
+
already_completed=(bool(accepted.get("settled")) if not wait
|
|
363
|
+
else result["status"]=="settled")
|
|
364
|
+
if already_completed:
|
|
365
|
+
if wait:
|
|
366
|
+
native_report=report_worker(
|
|
367
|
+
worker,turn_ref=delivery.get("turn_ref"))
|
|
368
|
+
positive_text=native_report["text"]
|
|
369
|
+
else:
|
|
370
|
+
original=delivery.get("settlement")
|
|
371
|
+
if (not isinstance(original,dict)
|
|
372
|
+
or original.get("status")!="completed"
|
|
373
|
+
or original.get("turnId")!=turn_id
|
|
374
|
+
or not isinstance(original.get("text"),str)):
|
|
375
|
+
raise SessionUnavailable(
|
|
376
|
+
"original Codex accepted receipt lacks positive "
|
|
377
|
+
"completed-turn evidence"
|
|
378
|
+
)
|
|
379
|
+
positive_text=original["text"]
|
|
380
|
+
report={"turnId":turn_id,"phase":"completed",
|
|
381
|
+
"text":positive_text}
|
|
382
|
+
final_turn={"turnId":turn_id,"status":"completed"}
|
|
383
|
+
rec=self._uuid_store.record(worker)
|
|
384
|
+
return CodexTurnResult.from_record(
|
|
385
|
+
rec,turn_id=turn_id,report=report,last_turn=final_turn,
|
|
386
|
+
client_user_message_id=client_id,delivery_id=token,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
def reconcile(
|
|
390
|
+
self, target: str, *, delivery_id: str | None = None,
|
|
391
|
+
turn_id: str | None = None, timeout: float = 30,
|
|
392
|
+
) -> CodexReconcileResult:
|
|
393
|
+
"""Observe original native thread/turn, no second native mutation."""
|
|
394
|
+
from ccx.ops.wait import reconcile_turn
|
|
395
|
+
from ccx.store.root import GenerationError
|
|
396
|
+
if timeout<=0:
|
|
397
|
+
raise InvalidRequest("Codex reconciliation timeout must be positive")
|
|
398
|
+
worker,record=self._owned_codex(target)
|
|
399
|
+
try:
|
|
400
|
+
result=reconcile_turn(
|
|
401
|
+
worker,delivery_id=delivery_id,turn_id=turn_id,timeout=timeout
|
|
402
|
+
)
|
|
403
|
+
except GenerationError as exc:
|
|
404
|
+
raise SessionUnavailable(
|
|
405
|
+
f"Codex delivery record unavailable; inspect the worker: {exc}"
|
|
406
|
+
) from exc
|
|
407
|
+
return CodexReconcileResult.from_operation(
|
|
408
|
+
self._uuid_store.record(worker),
|
|
409
|
+
result.get("delivery"),
|
|
410
|
+
result["status"],
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def telemetry(
|
|
414
|
+
self,
|
|
415
|
+
target: str,
|
|
416
|
+
*,
|
|
417
|
+
after_cursor: str | None = None,
|
|
418
|
+
limit: int = 200,
|
|
419
|
+
timeout: float = 120,
|
|
420
|
+
) -> CodexTelemetryPage:
|
|
421
|
+
"""Read original source-bound Codex telemetry without starting a turn."""
|
|
422
|
+
from ccx.store.root import GenerationError
|
|
423
|
+
|
|
424
|
+
worker, _record = self._owned_codex(target)
|
|
425
|
+
try:
|
|
426
|
+
result = telemetry_worker(
|
|
427
|
+
worker, after_cursor=after_cursor, limit=limit, timeout=timeout,
|
|
428
|
+
)
|
|
429
|
+
except GenerationError as exc:
|
|
430
|
+
raise SessionUnavailable(
|
|
431
|
+
f"Cannot read Codex activity; check worker status: {exc}"
|
|
432
|
+
) from exc
|
|
433
|
+
return CodexTelemetryPage.from_operation(result)
|
|
434
|
+
|
|
435
|
+
def steer(
|
|
436
|
+
self, target: str, text: str, *, delivery_id: str | None = None,
|
|
437
|
+
caps: dict | None = None,
|
|
438
|
+
) -> CodexControlResult:
|
|
439
|
+
"""Place one input in the exact active Codex turn, never start or wait."""
|
|
440
|
+
from uuid import uuid4
|
|
441
|
+
from ccx.codex import delivery as codex_delivery
|
|
442
|
+
from ccx.codex.runtime import settled_daemon_status
|
|
443
|
+
from ccx.ops.send import send_turn
|
|
444
|
+
from ccx.store.root import GenerationError
|
|
445
|
+
from ccx.store.worker_state import RUNTIME_ABSENT
|
|
446
|
+
|
|
447
|
+
handle, record = self._owned_codex(target)
|
|
448
|
+
if not isinstance(text, str) or not text.strip():
|
|
449
|
+
raise InvalidRequest("steer: prompt text is required")
|
|
450
|
+
token = delivery_id if delivery_id is not None else str(uuid4())
|
|
451
|
+
if not codex_delivery.valid_delivery_id(token):
|
|
452
|
+
raise InvalidRequest("steer: invalid delivery_id")
|
|
453
|
+
try:
|
|
454
|
+
original = codex_delivery.read_receipt(handle, token)
|
|
455
|
+
if original is None:
|
|
456
|
+
# Never turn a replayed/uncertain original token into a fresh
|
|
457
|
+
# absent-daemon refusal: the shared sender owns every replay.
|
|
458
|
+
self._uuid_store.authorize_native_action(handle, "send")
|
|
459
|
+
status = settled_daemon_status(time.monotonic() + 1.0)
|
|
460
|
+
if status.get("transport") == RUNTIME_ABSENT:
|
|
461
|
+
raise DeliveryRejected(
|
|
462
|
+
"Codex worker has no running turn (daemon absent); "
|
|
463
|
+
"no delivery was recorded, so this token is unclaimed "
|
|
464
|
+
"and may be used when the worker is running",
|
|
465
|
+
short=handle.split(":", 1)[1][:8],
|
|
466
|
+
state=(record.get("lifecycle") or {}).get("state"),
|
|
467
|
+
refusal="worker-state", delivery_id=token,
|
|
468
|
+
)
|
|
469
|
+
except (codex_delivery.DeliveryReceiptError, GenerationError, OSError) as exc:
|
|
470
|
+
raise SessionUnavailable(f"Codex message record is unavailable; inspect worker status: {exc}") from exc
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
accepted = send_turn(handle, text, delivery_id=token,
|
|
474
|
+
mode="steer_only", cap=caps)
|
|
475
|
+
except (codex_delivery.DeliveryReceiptError, GenerationError, OSError) as exc:
|
|
476
|
+
raise SessionUnavailable(f"Cannot add this message to the running turn; check status: {exc}") from exc
|
|
477
|
+
delivery = accepted["delivery"]
|
|
478
|
+
if accepted["status"] == "rejected":
|
|
479
|
+
raise DeliveryRejected(
|
|
480
|
+
delivery.get("rejection_message") or "Codex steer refused",
|
|
481
|
+
short=handle.split(":", 1)[1][:8],
|
|
482
|
+
refusal=delivery.get("rejection_code"),
|
|
483
|
+
delivery_id=token, delivery_status="rejected",
|
|
484
|
+
)
|
|
485
|
+
if (accepted["status"] != "accepted"
|
|
486
|
+
or delivery.get("route") != "steer"
|
|
487
|
+
or delivery.get("placement") != "current_turn"):
|
|
488
|
+
raise SendIndeterminate(
|
|
489
|
+
"Steering was not confirmed; "
|
|
490
|
+
"reconcile the delivery instead of resending",
|
|
491
|
+
short=handle.split(":", 1)[1][:8],
|
|
492
|
+
delivery_id=token, delivery_status=accepted["status"],
|
|
493
|
+
)
|
|
494
|
+
return CodexControlResult.from_record(
|
|
495
|
+
record, action="steer", turn_id=delivery["turn_id"],
|
|
496
|
+
client_user_message_id=delivery["client_user_message_id"],
|
|
497
|
+
delivery_id=token,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
def interrupt(
|
|
501
|
+
self,
|
|
502
|
+
target: str,
|
|
503
|
+
*,
|
|
504
|
+
expected_turn_id: str | None = None,
|
|
505
|
+
) -> CodexControlResult:
|
|
506
|
+
"""Request an exact full-UUID native interrupt; return ACK, not completion."""
|
|
507
|
+
from ccx.ops.cancel import stop_worker
|
|
508
|
+
handle, value = self._owned_codex(target)
|
|
509
|
+
expected_ref = None
|
|
510
|
+
if expected_turn_id is not None:
|
|
511
|
+
if not isinstance(expected_turn_id, str) or not expected_turn_id:
|
|
512
|
+
raise InvalidRequest("expected_turn_id must be a nonempty native turn ID")
|
|
513
|
+
expected_ref = "ccx-turn:codex:v1:" + expected_turn_id
|
|
514
|
+
result = stop_worker(
|
|
515
|
+
handle, expected_turn_ref=expected_ref,
|
|
516
|
+
expected_generation=self._uuid_store.generation,
|
|
517
|
+
)
|
|
518
|
+
# Existing CodexControlResult is an acceptance-only type. An unknown
|
|
519
|
+
# native reply is raised by stop_worker, never accepted=True here.
|
|
520
|
+
return CodexControlResult.from_record(
|
|
521
|
+
value, action="interrupt", turn_id=result["turn_ref"].rsplit(":", 1)[-1],
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
def wait(
|
|
525
|
+
self,
|
|
526
|
+
target: str,
|
|
527
|
+
*,
|
|
528
|
+
timeout: float | None = None,
|
|
529
|
+
settle: bool = False,
|
|
530
|
+
wake_grace: float = DEFAULT_WAKE_GRACE_S,
|
|
531
|
+
) -> CodexWaitResult:
|
|
532
|
+
"""Wait for original Codex stability or recursive notification quiescence."""
|
|
533
|
+
from ccx.ops.wait import wait_worker
|
|
534
|
+
handle, record = self._owned_codex(target)
|
|
535
|
+
result = (wait_for_settlement(
|
|
536
|
+
handle, timeout=timeout, wake_grace_s=wake_grace,
|
|
537
|
+
expected_generation=self._uuid_store.generation)
|
|
538
|
+
if settle else wait_worker(
|
|
539
|
+
handle, timeout=timeout,
|
|
540
|
+
expected_generation=self._uuid_store.generation))
|
|
541
|
+
return CodexWaitResult.from_result(record, result)
|
|
542
|
+
|
|
543
|
+
def report(
|
|
544
|
+
self, target: str, *, timeout: float | None = 90,
|
|
545
|
+
) -> CodexReport:
|
|
546
|
+
"""Read Codex's latest turn reply or last confirmed reply while busy.
|
|
547
|
+
|
|
548
|
+
An empty final answer stays empty; note explains newer uncertain
|
|
549
|
+
messages, running work, failed workers or native history failures.
|
|
550
|
+
"""
|
|
551
|
+
from ccx.ops.report import report_worker
|
|
552
|
+
from ccx.store.root import GenerationError
|
|
553
|
+
if timeout is not None and timeout<=0:
|
|
554
|
+
raise InvalidRequest("Codex report timeout must be positive")
|
|
555
|
+
worker,rec=self._owned_codex(target)
|
|
556
|
+
try:
|
|
557
|
+
report=report_worker(
|
|
558
|
+
worker,timeout=90 if timeout is None else timeout,
|
|
559
|
+
max_chars=None,
|
|
560
|
+
)
|
|
561
|
+
except GenerationError as exc:
|
|
562
|
+
raise SessionUnavailable(
|
|
563
|
+
f"Cannot read Codex reply; inspect worker status: {exc}"
|
|
564
|
+
) from exc
|
|
565
|
+
return CodexReport.from_record(self._uuid_store.record(worker),{
|
|
566
|
+
"turnId":report.get("turn_id"),
|
|
567
|
+
"itemId":None,"phase":report.get("phase"),
|
|
568
|
+
"text":report["text"],
|
|
569
|
+
"completedAt":report.get("settled_at_ms"),
|
|
570
|
+
"note":report.get("note"),
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
def approve(
|
|
574
|
+
self,
|
|
575
|
+
target: str,
|
|
576
|
+
*,
|
|
577
|
+
request_id: str,
|
|
578
|
+
timeout: float | None = None,
|
|
579
|
+
) -> CodexApprovalResult:
|
|
580
|
+
"""Accept one pending command or ordinary file-change request.
|
|
581
|
+
|
|
582
|
+
``timeout`` bounds only the decision RPC itself (the host resolves the
|
|
583
|
+
approval synchronously); pass it when the decision is one step of a
|
|
584
|
+
larger budgeted operation. ``None`` keeps the default host timeout.
|
|
585
|
+
"""
|
|
586
|
+
return self._decision(target, "approve", request_id, timeout)
|
|
587
|
+
|
|
588
|
+
def reject(
|
|
589
|
+
self,
|
|
590
|
+
target: str,
|
|
591
|
+
*,
|
|
592
|
+
request_id: str,
|
|
593
|
+
timeout: float | None = None,
|
|
594
|
+
) -> CodexApprovalResult:
|
|
595
|
+
"""Decline one pending command or ordinary file-change request.
|
|
596
|
+
|
|
597
|
+
Codex approvals carry no denial rationale, so there is no ``reason``
|
|
598
|
+
here: the app-server accepts a decision, not a message.
|
|
599
|
+
"""
|
|
600
|
+
return self._decision(target, "reject", request_id, timeout)
|
|
601
|
+
|
|
602
|
+
def _decision(
|
|
603
|
+
self,
|
|
604
|
+
target: str,
|
|
605
|
+
operation: str,
|
|
606
|
+
request_id: str,
|
|
607
|
+
timeout: float | None = None,
|
|
608
|
+
*, response_id: str | None = None,
|
|
609
|
+
expected_turn_id: str | None = None,
|
|
610
|
+
) -> CodexApprovalResult:
|
|
611
|
+
# Native requestIds can repeat after an app-server restart. The caller
|
|
612
|
+
# must supply the unique CCX gateId shown by recorded status/peek.
|
|
613
|
+
if not isinstance(request_id, str) or not request_id or len(request_id) > 128:
|
|
614
|
+
raise InvalidRequest("Codex approval requires an exact bounded CCX gateId")
|
|
615
|
+
if timeout is not None and (type(timeout) not in (int, float) or timeout <= 0):
|
|
616
|
+
raise InvalidRequest("Codex approval timeout must be positive")
|
|
617
|
+
_owner, value = self._owned_codex(target)
|
|
618
|
+
worker = value["workerId"] # Canonical bare global UUID, not qualified handle.
|
|
619
|
+
message = {"op": operation, "gateId": request_id}
|
|
620
|
+
if response_id is not None:
|
|
621
|
+
message["responseId"] = response_id
|
|
622
|
+
reply = rpc(worker, message, timeout=10 if timeout is None else timeout)
|
|
623
|
+
# The request could have reached codexd even if its ACK was lost.
|
|
624
|
+
# Codex app-server server-request responses have no provider ACK.
|
|
625
|
+
transport = reply.get("transport")
|
|
626
|
+
if reply.get("ok"):
|
|
627
|
+
native = value.get("native") or {}
|
|
628
|
+
expected_decision = "accept" if operation == "approve" else "decline"
|
|
629
|
+
if (reply.get("gateId") != request_id
|
|
630
|
+
or reply.get("workerId") != worker
|
|
631
|
+
or reply.get("threadId") != native.get("threadId")
|
|
632
|
+
or not isinstance(reply.get("turnId"), str)
|
|
633
|
+
or not reply["turnId"]
|
|
634
|
+
or (expected_turn_id is not None
|
|
635
|
+
and reply["turnId"] != expected_turn_id)
|
|
636
|
+
or reply.get("decision") != expected_decision):
|
|
637
|
+
raise SendIndeterminate(
|
|
638
|
+
f"Codex {operation} ACK mismatched original worker/gate; "
|
|
639
|
+
f"worker={worker}, gateId={request_id}", short=worker)
|
|
640
|
+
return CodexApprovalResult.from_record(
|
|
641
|
+
value, request_id=request_id, decision=expected_decision)
|
|
642
|
+
if transport == "absent" or reply.get("loaded") is False:
|
|
643
|
+
raise SessionUnavailable(f"Codex {operation}: worker is stopped; resume it before this action",
|
|
644
|
+
short=worker)
|
|
645
|
+
if transport == "inconclusive" or not reply.get("definitive"):
|
|
646
|
+
raise SendIndeterminate(
|
|
647
|
+
f"Codex {operation}: approval response indeterminate; "
|
|
648
|
+
f"worker={worker}, gateId={request_id}", short=worker)
|
|
649
|
+
raise DeliveryRejected(
|
|
650
|
+
str(reply.get("error") or f"Codex {operation} refused original gate"),
|
|
651
|
+
short=worker, reason=reply.get("error"), state=reply.get("state"))
|
|
652
|
+
|
|
653
|
+
def compact(
|
|
654
|
+
self,
|
|
655
|
+
target: str,
|
|
656
|
+
*,
|
|
657
|
+
timeout: float | None = COMPACT_WAIT_DEFAULT_S,
|
|
658
|
+
) -> CodexCompactResult:
|
|
659
|
+
"""Wait for one original, source-proven native Codex compaction."""
|
|
660
|
+
from uuid import uuid4
|
|
661
|
+
from ccx.codex.ops import compact_worker
|
|
662
|
+
from ccx.errors import CompactFailed, CompactIndeterminate
|
|
663
|
+
|
|
664
|
+
from ccx.store.root import GenerationError, NativeEffectIndeterminate
|
|
665
|
+
|
|
666
|
+
handle, _record = self._owned_codex(target)
|
|
667
|
+
operation_id = str(uuid4()) # Internal only; public Codex has no ID option.
|
|
668
|
+
try:
|
|
669
|
+
result = compact_worker(
|
|
670
|
+
handle, operation_id=operation_id, wait=True, timeout=timeout,
|
|
671
|
+
)
|
|
672
|
+
except NativeEffectIndeterminate as exc:
|
|
673
|
+
raise CompactIndeterminate(
|
|
674
|
+
f"Codex compaction outcome is unknown; check status before retrying: {exc}",
|
|
675
|
+
short=handle.split(":", 1)[1][:8], operation_id=operation_id,
|
|
676
|
+
operation_status="unknown",
|
|
677
|
+
) from exc
|
|
678
|
+
except GenerationError as exc:
|
|
679
|
+
raise SessionUnavailable(
|
|
680
|
+
f"full-UUID Codex native compact unavailable: {exc}"
|
|
681
|
+
) from exc
|
|
682
|
+
current = self._uuid_store.record(handle)
|
|
683
|
+
native_state = (current.get("lifecycle") or {}).get("state")
|
|
684
|
+
status = result["status"]
|
|
685
|
+
if status == "failed":
|
|
686
|
+
raise CompactFailed(
|
|
687
|
+
result.get("reason") or "Codex native compact turn failed",
|
|
688
|
+
short=current["workerId"][:8], state=native_state,
|
|
689
|
+
operation_id=operation_id,
|
|
690
|
+
)
|
|
691
|
+
if status == "rejected":
|
|
692
|
+
raise DeliveryRejected(
|
|
693
|
+
result.get("reason") or "Codex compact refused before native admission",
|
|
694
|
+
short=current["workerId"][:8], state=native_state,
|
|
695
|
+
refusal="native-rejected", operation_id=operation_id,
|
|
696
|
+
)
|
|
697
|
+
if status != "succeeded" or result.get("compacted") is not True:
|
|
698
|
+
raise CompactIndeterminate(
|
|
699
|
+
f"Codex compaction result {status!r} is unconfirmed; inspect its operation",
|
|
700
|
+
short=current["workerId"][:8], state=native_state,
|
|
701
|
+
operation_id=operation_id, operation_status=status,
|
|
702
|
+
)
|
|
703
|
+
return CodexCompactResult.from_record(
|
|
704
|
+
current, compacted=True,
|
|
705
|
+
context_before=result.get("contextBefore"),
|
|
706
|
+
context_after=result.get("contextAfter"),
|
|
707
|
+
operation_id=operation_id, operation_status=status,
|
|
708
|
+
compact_turn_id=result.get("compactTurnId"),
|
|
709
|
+
reason=result.get("reason"),
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
def context(self, target: str) -> CodexContext:
|
|
713
|
+
"""Return the latest cached carried-context measurement."""
|
|
714
|
+
_worker, rec = self._owned_codex(target)
|
|
715
|
+
return CodexContext.from_record(rec)
|
|
716
|
+
|
|
717
|
+
def models(self, *, refresh: bool = False) -> ModelCatalog:
|
|
718
|
+
"""The models Codex offers, as ``ccx models --engine codex``."""
|
|
719
|
+
from ccx.models import read_models
|
|
720
|
+
return read_models(engine="codex", refresh=refresh)[0]
|
|
721
|
+
|
|
722
|
+
def usage(
|
|
723
|
+
self,
|
|
724
|
+
*,
|
|
725
|
+
tokens: bool = False,
|
|
726
|
+
timeout: float = 30,
|
|
727
|
+
) -> CodexUsage:
|
|
728
|
+
"""Return live Codex account quota utilization or token activity."""
|
|
729
|
+
if timeout <= 0:
|
|
730
|
+
raise InvalidRequest("timeout must be greater than zero")
|
|
731
|
+
data = read_account_usage(tokens=tokens, timeout=timeout)
|
|
732
|
+
return CodexUsage(
|
|
733
|
+
raw=data,
|
|
734
|
+
kind="token_activity" if tokens else "rate_limits",
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
__all__ = ["CodexClient"]
|