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,659 @@
|
|
|
1
|
+
"""Frozen return types for the public :class:`ccx.CodexClient`.
|
|
2
|
+
|
|
3
|
+
The Codex client is intentionally separate from the Claude ``CcxClient``:
|
|
4
|
+
Codex has native thread/turn ids, in-turn steering, and no transcript surface.
|
|
5
|
+
Every result still follows the same library contract: attribute access plus an
|
|
6
|
+
``as_dict()`` projection containing only JSON-friendly values.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import asdict, dataclass, field
|
|
12
|
+
from typing import Any
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
WINDOW_MINUTES = {"5h": 300, "7d": 10080}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def rate_limit_windows(data: dict):
|
|
20
|
+
"""Yield (limit ID, native window) by duration, not primary/secondary rank."""
|
|
21
|
+
by_id = data.get("rateLimitsByLimitId")
|
|
22
|
+
buckets = (by_id.items() if isinstance(by_id, dict) and by_id
|
|
23
|
+
else (("codex", data.get("rateLimits")),))
|
|
24
|
+
for limit_id, bucket in buckets:
|
|
25
|
+
if not isinstance(bucket, dict):
|
|
26
|
+
continue
|
|
27
|
+
for field in ("primary", "secondary"):
|
|
28
|
+
window = bucket.get(field)
|
|
29
|
+
if isinstance(window, dict):
|
|
30
|
+
yield str(limit_id), window
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def window_label(window: dict) -> str:
|
|
34
|
+
minutes = window.get("windowDurationMins")
|
|
35
|
+
if type(minutes) is not int or minutes <= 0:
|
|
36
|
+
return "limit"
|
|
37
|
+
if minutes % 1440 == 0:
|
|
38
|
+
return f"{minutes // 1440}d"
|
|
39
|
+
if minutes % 60 == 0:
|
|
40
|
+
return f"{minutes // 60}h"
|
|
41
|
+
return f"{minutes}m"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def rate_limit_window(data: dict, window: str, *, limit_id: str = "codex") -> dict | None:
|
|
45
|
+
"""Find an exact-duration window in a named bucket, even if weekly is primary."""
|
|
46
|
+
minutes = WINDOW_MINUTES[window]
|
|
47
|
+
by_id = data.get("rateLimitsByLimitId")
|
|
48
|
+
bucket = by_id.get(limit_id) if isinstance(by_id, dict) else None
|
|
49
|
+
for source in (bucket, data.get("rateLimits")):
|
|
50
|
+
if not isinstance(source, dict):
|
|
51
|
+
continue
|
|
52
|
+
if source.get("limitId") not in (None, limit_id):
|
|
53
|
+
continue
|
|
54
|
+
for field in ("primary", "secondary"):
|
|
55
|
+
candidate = source.get(field)
|
|
56
|
+
if (isinstance(candidate, dict)
|
|
57
|
+
and candidate.get("windowDurationMins") == minutes):
|
|
58
|
+
return candidate
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def codex_cap_tripped(data: dict | None, caps: dict | None):
|
|
63
|
+
"""The first known reached window; missing, malformed or stale data fails open."""
|
|
64
|
+
if not isinstance(data, dict) or not caps:
|
|
65
|
+
return None
|
|
66
|
+
for name, cap in caps.items():
|
|
67
|
+
if name not in WINDOW_MINUTES:
|
|
68
|
+
continue
|
|
69
|
+
value = rate_limit_window(data, name)
|
|
70
|
+
used = value.get("usedPercent") if isinstance(value, dict) else None
|
|
71
|
+
reset = value.get("resetsAt") if isinstance(value, dict) else None
|
|
72
|
+
if type(reset) in (int, float) and reset <= time.time():
|
|
73
|
+
continue
|
|
74
|
+
if type(used) in (int, float) and 0 <= used <= 100 and used >= cap:
|
|
75
|
+
return name, float(used), float(cap)
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _identity(record: dict) -> dict[str, Any]:
|
|
80
|
+
lifecycle = record.get("lifecycle") or {}
|
|
81
|
+
create_args = record.get("createArgs") or {}
|
|
82
|
+
last_turn = record.get("lastTurn")
|
|
83
|
+
if not isinstance(last_turn, dict):
|
|
84
|
+
last_turn = None
|
|
85
|
+
native = record.get("native")
|
|
86
|
+
native = native if isinstance(native, dict) else {}
|
|
87
|
+
worker_id = record.get("workerId")
|
|
88
|
+
return {
|
|
89
|
+
# The short is for display; derive it from the global full UUID.
|
|
90
|
+
# Never resolve this prefix to a worker authority.
|
|
91
|
+
"short": (worker_id[:8] if isinstance(worker_id, str)
|
|
92
|
+
else record.get("short")),
|
|
93
|
+
"worker_id": worker_id,
|
|
94
|
+
"thread_id": native.get("threadId") or record.get("threadId"),
|
|
95
|
+
"name": record.get("name"),
|
|
96
|
+
"cwd": record.get("cwd"),
|
|
97
|
+
"state": lifecycle.get("state"),
|
|
98
|
+
"active_turn_id": lifecycle.get("activeTurnId"),
|
|
99
|
+
"model": create_args.get("model"),
|
|
100
|
+
"effort": create_args.get("effort"),
|
|
101
|
+
"sandbox": create_args.get("sandbox"),
|
|
102
|
+
"approval_policy": create_args.get("approvalPolicy"),
|
|
103
|
+
"idle_timeout": create_args.get("idleTimeout"),
|
|
104
|
+
"created_at": record.get("createdAt"),
|
|
105
|
+
"last_turn_at": record.get("lastTurnAt"),
|
|
106
|
+
"last_turn": dict(last_turn) if last_turn is not None else None,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class CodexSessionInfo:
|
|
112
|
+
"""Identity and persisted creation settings for one CCX-owned Codex worker."""
|
|
113
|
+
|
|
114
|
+
short: str
|
|
115
|
+
worker_id: str | None = None
|
|
116
|
+
thread_id: str | None = None
|
|
117
|
+
name: str | None = None
|
|
118
|
+
cwd: str | None = None
|
|
119
|
+
state: str | None = None
|
|
120
|
+
active_turn_id: str | None = None
|
|
121
|
+
model: str | None = None
|
|
122
|
+
effort: str | None = None
|
|
123
|
+
sandbox: str | None = None
|
|
124
|
+
approval_policy: str | None = None
|
|
125
|
+
idle_timeout: int | None = None
|
|
126
|
+
created_at: int | None = None
|
|
127
|
+
last_turn_at: int | None = None
|
|
128
|
+
last_turn: dict | None = None
|
|
129
|
+
engine: str = "codex"
|
|
130
|
+
|
|
131
|
+
def as_dict(self) -> dict[str, Any]:
|
|
132
|
+
out = asdict(self)
|
|
133
|
+
if self.worker_id is not None:
|
|
134
|
+
out["worker"] = f"codex:{self.worker_id}"
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def from_record(cls, record: dict) -> "CodexSessionInfo":
|
|
139
|
+
return cls(**_identity(record))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class CodexResumeResult(CodexSessionInfo):
|
|
144
|
+
"""Result of explicitly ensuring that a durable Codex worker is loaded."""
|
|
145
|
+
|
|
146
|
+
resumed: bool = False
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_record(cls, record: dict, *, resumed: bool) -> "CodexResumeResult":
|
|
150
|
+
return cls(**_identity(record), resumed=bool(resumed))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class CodexTurnResult(CodexSessionInfo):
|
|
155
|
+
"""A started or completed Codex turn.
|
|
156
|
+
|
|
157
|
+
``accepted`` means app-server accepted ``turn/start``. With ``wait=False``,
|
|
158
|
+
``reply`` is ``None`` and the turn normally remains busy. With a waited
|
|
159
|
+
turn, ``reply`` comes from the last completed agent message cached by the
|
|
160
|
+
host. ``status``, ``error``, and the timing fields are the matching native
|
|
161
|
+
turn's outcome and remain ``None`` for an acknowledged but not-yet-settled
|
|
162
|
+
turn. ``item_id`` and ``phase`` describe the selected report; the item ID
|
|
163
|
+
is native to the observation surface that supplied it and may differ from
|
|
164
|
+
a corresponding ``thread/read`` item ID. Use ``turn_id`` for turn
|
|
165
|
+
association across observation surfaces.
|
|
166
|
+
|
|
167
|
+
``client_user_message_id`` is the CCX-owned id sent with this turn's user
|
|
168
|
+
message. Native Codex persists it on the ``userMessage`` item, so it is how
|
|
169
|
+
a cold reader identifies the input CCX started this turn with. It is
|
|
170
|
+
``None`` for a result that did not start a turn.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
accepted: bool = True
|
|
174
|
+
turn_id: str | None = None
|
|
175
|
+
reply: str | None = None
|
|
176
|
+
item_id: str | None = None
|
|
177
|
+
phase: str | None = None
|
|
178
|
+
status: str | None = None
|
|
179
|
+
error: Any = None
|
|
180
|
+
started_at: int | None = None
|
|
181
|
+
completed_at: int | None = None
|
|
182
|
+
duration_ms: int | float | None = None
|
|
183
|
+
client_user_message_id: str | None = None
|
|
184
|
+
delivery_id: str | None = None
|
|
185
|
+
|
|
186
|
+
@classmethod
|
|
187
|
+
def from_record(
|
|
188
|
+
cls,
|
|
189
|
+
record: dict,
|
|
190
|
+
*,
|
|
191
|
+
accepted: bool = True,
|
|
192
|
+
turn_id: str | None = None,
|
|
193
|
+
report: dict | None = None,
|
|
194
|
+
last_turn: dict | None = None,
|
|
195
|
+
client_user_message_id: str | None = None,
|
|
196
|
+
delivery_id: str | None = None,
|
|
197
|
+
) -> "CodexTurnResult":
|
|
198
|
+
report = report or {}
|
|
199
|
+
raw_last_turn = last_turn
|
|
200
|
+
if not isinstance(raw_last_turn, dict):
|
|
201
|
+
raw_last_turn = record.get("lastTurn")
|
|
202
|
+
if not isinstance(raw_last_turn, dict):
|
|
203
|
+
raw_last_turn = None
|
|
204
|
+
selected_turn_id = turn_id or report.get("turnId")
|
|
205
|
+
if selected_turn_id is None and raw_last_turn is not None:
|
|
206
|
+
selected_turn_id = raw_last_turn.get("turnId")
|
|
207
|
+
outcome = raw_last_turn
|
|
208
|
+
if (
|
|
209
|
+
selected_turn_id is not None
|
|
210
|
+
and outcome is not None
|
|
211
|
+
and outcome.get("turnId") != selected_turn_id
|
|
212
|
+
):
|
|
213
|
+
# The record may still expose a previous turn while a no-wait
|
|
214
|
+
# result identifies a newly accepted one. Do not attach that old
|
|
215
|
+
# outcome to the new result.
|
|
216
|
+
outcome = None
|
|
217
|
+
report_matches = (
|
|
218
|
+
selected_turn_id is None
|
|
219
|
+
or report.get("turnId") == selected_turn_id
|
|
220
|
+
)
|
|
221
|
+
selected_report = report if report_matches else {}
|
|
222
|
+
identity_record = {**record, "lastTurn": raw_last_turn}
|
|
223
|
+
return cls(
|
|
224
|
+
**_identity(identity_record),
|
|
225
|
+
accepted=accepted,
|
|
226
|
+
turn_id=selected_turn_id,
|
|
227
|
+
reply=selected_report.get("text"),
|
|
228
|
+
item_id=selected_report.get("itemId"),
|
|
229
|
+
phase=selected_report.get("phase"),
|
|
230
|
+
status=outcome.get("status") if outcome else None,
|
|
231
|
+
error=outcome.get("error") if outcome else None,
|
|
232
|
+
started_at=outcome.get("startedAtMs") if outcome else None,
|
|
233
|
+
completed_at=(
|
|
234
|
+
outcome.get("completedAtMs")
|
|
235
|
+
if outcome
|
|
236
|
+
else selected_report.get("completedAt")
|
|
237
|
+
),
|
|
238
|
+
duration_ms=outcome.get("durationMs") if outcome else None,
|
|
239
|
+
client_user_message_id=client_user_message_id,
|
|
240
|
+
delivery_id=delivery_id,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass(frozen=True)
|
|
245
|
+
class CodexReconcileResult(CodexSessionInfo):
|
|
246
|
+
"""Read-only verdict for one caller-known delivery or native turn."""
|
|
247
|
+
|
|
248
|
+
delivery_id: str | None = None
|
|
249
|
+
status: str = "not_found"
|
|
250
|
+
pending: bool = False
|
|
251
|
+
turn_id: str | None = None
|
|
252
|
+
client_user_message_id: str | None = None
|
|
253
|
+
rejection_code: str | None = None
|
|
254
|
+
rejection_message: str | None = None
|
|
255
|
+
# Set with status="not_delivered": why CCX refused the delivery before
|
|
256
|
+
# submitting it. Sending again under the same delivery id is safe.
|
|
257
|
+
refusal_code: str | None = None
|
|
258
|
+
refusal_message: str | None = None
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def retryable(self) -> bool:
|
|
262
|
+
return self.status == "not_delivered"
|
|
263
|
+
|
|
264
|
+
@classmethod
|
|
265
|
+
def from_operation(cls, worker: dict, delivery: dict | None, status: str) -> "CodexReconcileResult":
|
|
266
|
+
delivery = delivery or {}
|
|
267
|
+
refusal = delivery.get("last_refusal") or {}
|
|
268
|
+
return cls(
|
|
269
|
+
**_identity(worker),
|
|
270
|
+
delivery_id=delivery.get("delivery_id"),
|
|
271
|
+
status=status,
|
|
272
|
+
pending=bool(delivery.get("pending")) and status != "not_delivered",
|
|
273
|
+
turn_id=delivery.get("turn_id"),
|
|
274
|
+
client_user_message_id=delivery.get("client_user_message_id"),
|
|
275
|
+
rejection_code=delivery.get("rejection_code"),
|
|
276
|
+
rejection_message=delivery.get("rejection_message"),
|
|
277
|
+
refusal_code=refusal.get("code") if status == "not_delivered" else None,
|
|
278
|
+
refusal_message=(
|
|
279
|
+
refusal.get("message") if status == "not_delivered" else None
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@dataclass(frozen=True)
|
|
285
|
+
class CodexTelemetryPage:
|
|
286
|
+
"""Normalized replay page for one Codex worker's captured telemetry."""
|
|
287
|
+
|
|
288
|
+
short: str
|
|
289
|
+
thread_id: str | None = None
|
|
290
|
+
name: str | None = None
|
|
291
|
+
turns: list[dict] = field(default_factory=list)
|
|
292
|
+
compactions: list[dict] = field(default_factory=list)
|
|
293
|
+
coverage: dict = field(default_factory=dict)
|
|
294
|
+
next_cursor: str | None = None
|
|
295
|
+
has_more: bool = False
|
|
296
|
+
engine: str = "codex"
|
|
297
|
+
|
|
298
|
+
def as_dict(self) -> dict[str, Any]:
|
|
299
|
+
return asdict(self)
|
|
300
|
+
|
|
301
|
+
@classmethod
|
|
302
|
+
def from_operation(cls, result: dict[str, Any]) -> "CodexTelemetryPage":
|
|
303
|
+
worker = result.get("worker")
|
|
304
|
+
worker = worker if isinstance(worker, dict) else {}
|
|
305
|
+
return cls(
|
|
306
|
+
short=str(worker.get("short") or ""),
|
|
307
|
+
thread_id=worker.get("thread_id"),
|
|
308
|
+
name=worker.get("name"),
|
|
309
|
+
turns=list(result.get("turns") or []),
|
|
310
|
+
compactions=list(result.get("compactions") or []),
|
|
311
|
+
coverage=dict(result.get("coverage") or {}),
|
|
312
|
+
next_cursor=result.get("next_cursor"),
|
|
313
|
+
has_more=bool(result.get("has_more")),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@dataclass(frozen=True)
|
|
318
|
+
class CodexControlResult(CodexSessionInfo):
|
|
319
|
+
"""Acknowledgement for ``steer`` or ``interrupt``.
|
|
320
|
+
|
|
321
|
+
``client_user_message_id`` is set for ``steer``, whose input becomes its own
|
|
322
|
+
native user message, and stays ``None`` for ``interrupt``, which sends no
|
|
323
|
+
user message at all. A steer id is never a start id: they are minted
|
|
324
|
+
separately and carry different origins.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
action: str = ""
|
|
328
|
+
accepted: bool = True
|
|
329
|
+
turn_id: str | None = None
|
|
330
|
+
client_user_message_id: str | None = None
|
|
331
|
+
delivery_id: str | None = None
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def from_record(
|
|
335
|
+
cls,
|
|
336
|
+
record: dict,
|
|
337
|
+
*,
|
|
338
|
+
action: str,
|
|
339
|
+
turn_id: str | None = None,
|
|
340
|
+
client_user_message_id: str | None = None,
|
|
341
|
+
delivery_id: str | None = None,
|
|
342
|
+
) -> "CodexControlResult":
|
|
343
|
+
return cls(
|
|
344
|
+
**_identity(record),
|
|
345
|
+
action=action,
|
|
346
|
+
accepted=True,
|
|
347
|
+
turn_id=turn_id,
|
|
348
|
+
client_user_message_id=client_user_message_id,
|
|
349
|
+
delivery_id=delivery_id,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@dataclass(frozen=True)
|
|
354
|
+
class CodexWaitResult(CodexSessionInfo):
|
|
355
|
+
"""Stable-state verdict from :meth:`CodexClient.wait`.
|
|
356
|
+
|
|
357
|
+
``evidence`` is the worker's raw evidence-journal health block, or ``None``
|
|
358
|
+
for a worker that has produced no evidence. It is deliberately independent
|
|
359
|
+
of ``status``: a healthy idle worker whose journal degraded stays idle, and
|
|
360
|
+
a poller sees the degradation without the worker looking broken.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
status: str = ""
|
|
364
|
+
timed_out: bool = False
|
|
365
|
+
last_settled_turn: dict | None = None
|
|
366
|
+
active_turn_ref: str | None = None
|
|
367
|
+
pending_approvals: list[dict] = field(default_factory=list)
|
|
368
|
+
report: dict | None = None
|
|
369
|
+
active_turn_error: dict | None = None
|
|
370
|
+
error: str | None = None
|
|
371
|
+
turn_id: str | None = None
|
|
372
|
+
turn_status: str | None = None
|
|
373
|
+
turn_error: Any = None
|
|
374
|
+
turn_started_at: int | None = None
|
|
375
|
+
turn_completed_at: int | None = None
|
|
376
|
+
turn_duration_ms: int | float | None = None
|
|
377
|
+
evidence: dict | None = None
|
|
378
|
+
settle: bool = False
|
|
379
|
+
settlement_verdict: str | None = None
|
|
380
|
+
settled: bool | None = None
|
|
381
|
+
outstanding_count: int = 0
|
|
382
|
+
outstanding: list[dict] = field(default_factory=list)
|
|
383
|
+
outstanding_truncated: bool = False
|
|
384
|
+
pending_wake_count: int = 0
|
|
385
|
+
pending_wake: list[dict] = field(default_factory=list)
|
|
386
|
+
pending_wake_truncated: bool = False
|
|
387
|
+
registration_count: int = 0
|
|
388
|
+
wake_grace_seconds: float | None = None
|
|
389
|
+
last_message: str | None = None
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def is_waiting(self) -> bool:
|
|
393
|
+
return self.status == "waiting"
|
|
394
|
+
|
|
395
|
+
@classmethod
|
|
396
|
+
def from_result(cls, record: dict, result: dict) -> "CodexWaitResult":
|
|
397
|
+
state = result.get("state") or (record.get("lifecycle") or {}).get("state")
|
|
398
|
+
timed_out = result.get("timed_out") is True or (
|
|
399
|
+
not result.get("ok", False) and result.get("error") == "wait timed out"
|
|
400
|
+
)
|
|
401
|
+
last_turn = result.get("lastTurn")
|
|
402
|
+
if not isinstance(last_turn, dict):
|
|
403
|
+
last_turn = record.get("lastTurn")
|
|
404
|
+
if not isinstance(last_turn, dict):
|
|
405
|
+
last_turn = None
|
|
406
|
+
if "activeTurnError" in result:
|
|
407
|
+
active_turn_error = result["activeTurnError"]
|
|
408
|
+
elif "active_turn_error" in result:
|
|
409
|
+
active_turn_error = result["active_turn_error"]
|
|
410
|
+
else:
|
|
411
|
+
active_turn_error = record.get("activeTurnError")
|
|
412
|
+
active_turn = (record.get("lifecycle") or {}).get("activeTurnId")
|
|
413
|
+
for key in ("activeTurnId", "active_turn_id", "active_turn_ref"):
|
|
414
|
+
if key in result:
|
|
415
|
+
active_turn = result[key]
|
|
416
|
+
if key == "active_turn_ref" and active_turn is not None:
|
|
417
|
+
active_turn = active_turn.removeprefix("ccx-turn:codex:v1:")
|
|
418
|
+
break
|
|
419
|
+
approvals = record.get("pendingApprovals")
|
|
420
|
+
for key in ("pendingApprovals", "pending_approvals"):
|
|
421
|
+
if key in result:
|
|
422
|
+
approvals = result[key]
|
|
423
|
+
break
|
|
424
|
+
identity_record = {**record, "lastTurn": last_turn}
|
|
425
|
+
return cls(
|
|
426
|
+
**_identity({**identity_record, "lifecycle": {
|
|
427
|
+
**(record.get("lifecycle") or {}),
|
|
428
|
+
"state": state,
|
|
429
|
+
"activeTurnId": active_turn,
|
|
430
|
+
}}),
|
|
431
|
+
status=str(result.get("status") or ("timeout" if timed_out else state or "unknown")),
|
|
432
|
+
timed_out=timed_out,
|
|
433
|
+
last_settled_turn=result.get("last_settled_turn"),
|
|
434
|
+
active_turn_ref=result.get("active_turn_ref"),
|
|
435
|
+
pending_approvals=list(approvals or []),
|
|
436
|
+
# A general native wait is worker stability, not a reply request.
|
|
437
|
+
# Do not lift an older cached report into this wait's answer.
|
|
438
|
+
report=(None if "worker" in result else
|
|
439
|
+
result.get("lastReport") or result.get("report")
|
|
440
|
+
or record.get("lastReport")),
|
|
441
|
+
active_turn_error=active_turn_error,
|
|
442
|
+
error=result.get("error"),
|
|
443
|
+
turn_id=last_turn.get("turnId") if last_turn else None,
|
|
444
|
+
turn_status=last_turn.get("status") if last_turn else None,
|
|
445
|
+
turn_error=last_turn.get("error") if last_turn else None,
|
|
446
|
+
turn_started_at=last_turn.get("startedAtMs") if last_turn else None,
|
|
447
|
+
turn_completed_at=last_turn.get("completedAtMs") if last_turn else None,
|
|
448
|
+
turn_duration_ms=last_turn.get("durationMs") if last_turn else None,
|
|
449
|
+
evidence=(
|
|
450
|
+
result.get("evidence")
|
|
451
|
+
if isinstance(result.get("evidence"), dict)
|
|
452
|
+
else (
|
|
453
|
+
record.get("evidence")
|
|
454
|
+
if isinstance(record.get("evidence"), dict) else None
|
|
455
|
+
)
|
|
456
|
+
),
|
|
457
|
+
settle=bool(result.get("settle")),
|
|
458
|
+
settlement_verdict=(
|
|
459
|
+
result.get("settlementVerdict")
|
|
460
|
+
or result.get("settlement_verdict")
|
|
461
|
+
),
|
|
462
|
+
settled=(result.get("settled") if result.get("settle") else None),
|
|
463
|
+
outstanding_count=int(
|
|
464
|
+
result.get("outstandingCount")
|
|
465
|
+
or result.get("outstanding_count")
|
|
466
|
+
or 0
|
|
467
|
+
),
|
|
468
|
+
outstanding=list(result.get("outstanding") or []),
|
|
469
|
+
outstanding_truncated=bool(
|
|
470
|
+
result.get("outstandingTruncated")
|
|
471
|
+
or result.get("outstanding_truncated")
|
|
472
|
+
),
|
|
473
|
+
pending_wake_count=int(
|
|
474
|
+
result.get("pendingWakeCount")
|
|
475
|
+
or result.get("pending_wake_count")
|
|
476
|
+
or 0
|
|
477
|
+
),
|
|
478
|
+
pending_wake=list(
|
|
479
|
+
result.get("pendingWake")
|
|
480
|
+
or result.get("pending_wake")
|
|
481
|
+
or []
|
|
482
|
+
),
|
|
483
|
+
pending_wake_truncated=bool(
|
|
484
|
+
result.get("pendingWakeTruncated")
|
|
485
|
+
or result.get("pending_wake_truncated")
|
|
486
|
+
),
|
|
487
|
+
registration_count=int(
|
|
488
|
+
result.get("registrationCount")
|
|
489
|
+
or result.get("registration_count")
|
|
490
|
+
or 0
|
|
491
|
+
),
|
|
492
|
+
wake_grace_seconds=(
|
|
493
|
+
result.get("wakeGraceSeconds")
|
|
494
|
+
if result.get("wakeGraceSeconds") is not None
|
|
495
|
+
else result.get("wake_grace_seconds")
|
|
496
|
+
),
|
|
497
|
+
last_message=(
|
|
498
|
+
result.get("lastMessage")
|
|
499
|
+
if result.get("lastMessage") is not None
|
|
500
|
+
else result.get("last_message")
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
@dataclass(frozen=True)
|
|
506
|
+
class CodexReport(CodexSessionInfo):
|
|
507
|
+
"""The latest completed Codex agent message.
|
|
508
|
+
|
|
509
|
+
``item_id`` is the native ID from the observation surface that supplied
|
|
510
|
+
this report. It is evidence about that source item, not a durable join key
|
|
511
|
+
across live notifications and reconstructed ``thread/read`` history;
|
|
512
|
+
``turn_id`` is the authoritative cross-surface identity.
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
turn_id: str | None = None
|
|
516
|
+
item_id: str | None = None
|
|
517
|
+
phase: str | None = None
|
|
518
|
+
text: str = ""
|
|
519
|
+
completed_at: int | None = None
|
|
520
|
+
# Set when a newer turn is running or ended without a reply.
|
|
521
|
+
note: str | None = None
|
|
522
|
+
|
|
523
|
+
@classmethod
|
|
524
|
+
def from_record(cls, record: dict, report: dict) -> "CodexReport":
|
|
525
|
+
return cls(
|
|
526
|
+
**_identity(record),
|
|
527
|
+
turn_id=report.get("turnId"),
|
|
528
|
+
item_id=report.get("itemId"),
|
|
529
|
+
phase=report.get("phase"),
|
|
530
|
+
text=str(report.get("text") or ""),
|
|
531
|
+
completed_at=report.get("completedAt"),
|
|
532
|
+
note=report.get("note"),
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
@dataclass(frozen=True)
|
|
537
|
+
class CodexApprovalResult(CodexSessionInfo):
|
|
538
|
+
"""Acknowledgement for one connection-scoped approval decision."""
|
|
539
|
+
|
|
540
|
+
request_id: str | int | None = None
|
|
541
|
+
decision: str = ""
|
|
542
|
+
|
|
543
|
+
@classmethod
|
|
544
|
+
def from_record(
|
|
545
|
+
cls, record: dict, *, request_id: str | int | None, decision: str
|
|
546
|
+
) -> "CodexApprovalResult":
|
|
547
|
+
return cls(
|
|
548
|
+
**_identity(record),
|
|
549
|
+
request_id=request_id,
|
|
550
|
+
decision=decision,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@dataclass(frozen=True)
|
|
555
|
+
class CodexContext(CodexSessionInfo):
|
|
556
|
+
"""Latest carried input tokens and model context-window size, the shape
|
|
557
|
+
``ccx context codex:TARGET --json`` emits. ``state`` is the worker lifecycle;
|
|
558
|
+
``reason`` is ``None`` when ``tokens_used`` is a measurement,
|
|
559
|
+
``"compacting"`` while a compaction runs (the older measurement is
|
|
560
|
+
withheld) and ``"no-signal"`` otherwise. ``as_of_turn_ref`` identifies settled CCX turn context;
|
|
561
|
+
live context has a null turn reference."""
|
|
562
|
+
|
|
563
|
+
tokens_used: int | None = None
|
|
564
|
+
window_size: int | None = None
|
|
565
|
+
percent_used: float | None = None
|
|
566
|
+
as_of_turn_ref: str | None = None
|
|
567
|
+
as_of_ms: int | None = None
|
|
568
|
+
reason: str | None = None
|
|
569
|
+
|
|
570
|
+
@classmethod
|
|
571
|
+
def from_record(cls, record: dict) -> "CodexContext":
|
|
572
|
+
from ccx.claude.context import current_context, public_context
|
|
573
|
+
return cls(
|
|
574
|
+
**_identity(record),
|
|
575
|
+
**public_context(
|
|
576
|
+
current_context(record, engine="codex", live=record.get("context")), engine="codex",
|
|
577
|
+
compacting=isinstance(record.get("activeCompact"), dict),
|
|
578
|
+
),
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
@dataclass(frozen=True)
|
|
583
|
+
class CodexUsage:
|
|
584
|
+
"""Live Codex account quota utilization or token-activity payload."""
|
|
585
|
+
|
|
586
|
+
raw: dict = field(default_factory=dict)
|
|
587
|
+
kind: str = "rate_limits"
|
|
588
|
+
engine: str = "codex"
|
|
589
|
+
|
|
590
|
+
def as_dict(self) -> dict[str, Any]:
|
|
591
|
+
return {
|
|
592
|
+
"engine": self.engine,
|
|
593
|
+
"kind": self.kind,
|
|
594
|
+
"raw": dict(self.raw),
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
def utilization(
|
|
598
|
+
self,
|
|
599
|
+
*,
|
|
600
|
+
limit_id: str = "codex",
|
|
601
|
+
window: str = "5h",
|
|
602
|
+
) -> int | None:
|
|
603
|
+
"""Return a quota window's used percentage when present."""
|
|
604
|
+
if window not in WINDOW_MINUTES:
|
|
605
|
+
raise ValueError("Codex window must be '5h' or '7d'")
|
|
606
|
+
value = rate_limit_window(self.raw, window, limit_id=limit_id)
|
|
607
|
+
used = value.get("usedPercent") if isinstance(value, dict) else None
|
|
608
|
+
return used if type(used) in (int, float) and 0 <= used <= 100 else None
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@dataclass(frozen=True)
|
|
612
|
+
class CodexCompactResult(CodexSessionInfo):
|
|
613
|
+
"""One evidenced native Codex compact, not an idle or an admission ACK."""
|
|
614
|
+
|
|
615
|
+
compacted: bool = False
|
|
616
|
+
context_before: dict | None = None
|
|
617
|
+
context_after: dict | None = None
|
|
618
|
+
operation_id: str | None = None
|
|
619
|
+
operation_status: str | None = None
|
|
620
|
+
compact_turn_id: str | None = None
|
|
621
|
+
reason: str | None = None
|
|
622
|
+
|
|
623
|
+
@classmethod
|
|
624
|
+
def from_record(
|
|
625
|
+
cls,
|
|
626
|
+
record: dict,
|
|
627
|
+
*,
|
|
628
|
+
compacted: bool,
|
|
629
|
+
context_before: dict | None,
|
|
630
|
+
context_after: dict | None,
|
|
631
|
+
operation_id: str | None = None,
|
|
632
|
+
operation_status: str | None = None,
|
|
633
|
+
compact_turn_id: str | None = None,
|
|
634
|
+
reason: str | None = None,
|
|
635
|
+
) -> "CodexCompactResult":
|
|
636
|
+
return cls(
|
|
637
|
+
**_identity(record),
|
|
638
|
+
compacted=compacted,
|
|
639
|
+
context_before=context_before,
|
|
640
|
+
context_after=context_after,
|
|
641
|
+
operation_id=operation_id,
|
|
642
|
+
operation_status=operation_status,
|
|
643
|
+
compact_turn_id=compact_turn_id,
|
|
644
|
+
reason=reason,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
__all__ = [
|
|
649
|
+
"CodexApprovalResult",
|
|
650
|
+
"CodexCompactResult",
|
|
651
|
+
"CodexContext",
|
|
652
|
+
"CodexControlResult",
|
|
653
|
+
"CodexReport",
|
|
654
|
+
"CodexSessionInfo",
|
|
655
|
+
"CodexTelemetryPage",
|
|
656
|
+
"CodexTurnResult",
|
|
657
|
+
"CodexUsage",
|
|
658
|
+
"CodexWaitResult",
|
|
659
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Prevent one launcher's identity from entering a shared Codex app-server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def scrub_shared_codex_identity(env: dict[str, str]) -> dict[str, str]:
|
|
7
|
+
"""Keep user tool/auth settings; remove only worker/session identity."""
|
|
8
|
+
from ccx.claude.host.session import INHERITED_CLAUDE_IDENTITY_VARS
|
|
9
|
+
|
|
10
|
+
stale = {
|
|
11
|
+
*INHERITED_CLAUDE_IDENTITY_VARS,
|
|
12
|
+
"CLAUDE_CODE_FORCE_SESSION_PERSISTENCE",
|
|
13
|
+
"CLAUDECODE",
|
|
14
|
+
"CCX_SPAWN_SCOPE",
|
|
15
|
+
"CODEX_THREAD_ID",
|
|
16
|
+
}
|
|
17
|
+
for key in tuple(env):
|
|
18
|
+
if key.startswith("CCX_SELF_") or key in stale:
|
|
19
|
+
env.pop(key, None)
|
|
20
|
+
# Restore the caller's genuine user shell paths while discarding every
|
|
21
|
+
# parent-owned Bash, zsh and Claude Python shim. A shared daemon never
|
|
22
|
+
# carries a worker's generated shell function into its next thread.
|
|
23
|
+
from ccx.controllers.live_vars import sanitize_inherited_shell_bridge_env
|
|
24
|
+
sanitize_inherited_shell_bridge_env(env)
|
|
25
|
+
return env
|