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.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -4
  3. package/bin/ccx +55 -2
  4. package/ccx/__init__.py +167 -0
  5. package/ccx/__main__.py +28 -0
  6. package/ccx/claude/__init__.py +0 -0
  7. package/ccx/claude/analytics.py +466 -0
  8. package/ccx/claude/attach.py +807 -0
  9. package/ccx/claude/context.py +465 -0
  10. package/ccx/claude/delivery.py +806 -0
  11. package/ccx/claude/external.py +253 -0
  12. package/ccx/claude/host/__init__.py +0 -0
  13. package/ccx/claude/host/events.py +297 -0
  14. package/ccx/claude/host/hooks.py +438 -0
  15. package/ccx/claude/host/pty.py +269 -0
  16. package/ccx/claude/host/session.py +5244 -0
  17. package/ccx/claude/host/subagents.py +169 -0
  18. package/ccx/claude/host/successor.py +267 -0
  19. package/ccx/claude/managed_turns.py +240 -0
  20. package/ccx/claude/oauth.py +176 -0
  21. package/ccx/claude/settings.py +339 -0
  22. package/ccx/claude/spawn.py +557 -0
  23. package/ccx/claude/tasks.py +72 -0
  24. package/ccx/claude/telemetry.py +282 -0
  25. package/ccx/claude/transcript.py +2535 -0
  26. package/ccx/claude/usage.py +796 -0
  27. package/ccx/claude/usage_control.py +204 -0
  28. package/ccx/cli/__init__.py +0 -0
  29. package/ccx/cli/codex.py +1009 -0
  30. package/ccx/cli/commands/__init__.py +0 -0
  31. package/ccx/cli/commands/_common.py +407 -0
  32. package/ccx/cli/commands/control.py +161 -0
  33. package/ccx/cli/commands/create.py +669 -0
  34. package/ccx/cli/commands/models.py +53 -0
  35. package/ccx/cli/commands/read.py +1104 -0
  36. package/ccx/cli/commands/turns.py +1081 -0
  37. package/ccx/cli/controller.py +757 -0
  38. package/ccx/cli/main.py +302 -0
  39. package/ccx/cli/parser.py +1439 -0
  40. package/ccx/cli/skills.py +215 -0
  41. package/ccx/cli/statusline.py +160 -0
  42. package/ccx/client.py +1904 -0
  43. package/ccx/codex/__init__.py +0 -0
  44. package/ccx/codex/attach_tty.py +267 -0
  45. package/ccx/codex/client.py +738 -0
  46. package/ccx/codex/completion.py +121 -0
  47. package/ccx/codex/control.py +288 -0
  48. package/ccx/codex/daemon/__init__.py +0 -0
  49. package/ccx/codex/daemon/__main__.py +4 -0
  50. package/ccx/codex/daemon/daemon.py +1558 -0
  51. package/ccx/codex/daemon/models.py +140 -0
  52. package/ccx/codex/daemon/native.py +1590 -0
  53. package/ccx/codex/daemon/release.py +367 -0
  54. package/ccx/codex/daemon/worker.py +2424 -0
  55. package/ccx/codex/delivery.py +758 -0
  56. package/ccx/codex/delivery_recovery.py +232 -0
  57. package/ccx/codex/evidence.py +827 -0
  58. package/ccx/codex/history.py +158 -0
  59. package/ccx/codex/history_store.py +131 -0
  60. package/ccx/codex/notify_history.py +140 -0
  61. package/ccx/codex/ops.py +666 -0
  62. package/ccx/codex/paths.py +70 -0
  63. package/ccx/codex/protocol.py +805 -0
  64. package/ccx/codex/provenance.py +105 -0
  65. package/ccx/codex/read.py +29 -0
  66. package/ccx/codex/records.py +51 -0
  67. package/ccx/codex/reports.py +177 -0
  68. package/ccx/codex/runtime.py +1454 -0
  69. package/ccx/codex/settle.py +363 -0
  70. package/ccx/codex/telemetry.py +792 -0
  71. package/ccx/codex/types.py +659 -0
  72. package/ccx/codex/worker_environment.py +25 -0
  73. package/ccx/constants.py +288 -0
  74. package/ccx/controllers/__init__.py +0 -0
  75. package/ccx/controllers/controller.py +1449 -0
  76. package/ccx/controllers/live_vars.py +1706 -0
  77. package/ccx/controllers/live_vars_runner.py +216 -0
  78. package/ccx/controllers/notify.py +213 -0
  79. package/ccx/controllers/orchestrator.py +193 -0
  80. package/ccx/controllers/policy.py +57 -0
  81. package/ccx/controllers/store.py +681 -0
  82. package/ccx/errors.py +634 -0
  83. package/ccx/models.py +225 -0
  84. package/ccx/notify/__init__.py +0 -0
  85. package/ccx/notify/__main__.py +5 -0
  86. package/ccx/notify/client.py +116 -0
  87. package/ccx/notify/daemon.py +480 -0
  88. package/ccx/notify/deliver.py +304 -0
  89. package/ccx/notify/paths.py +51 -0
  90. package/ccx/notify/registry.py +649 -0
  91. package/ccx/notify/select.py +750 -0
  92. package/ccx/notify/settle.py +684 -0
  93. package/ccx/ops/__init__.py +0 -0
  94. package/ccx/ops/background.py +115 -0
  95. package/ccx/ops/cancel.py +296 -0
  96. package/ccx/ops/codex_import.py +96 -0
  97. package/ccx/ops/common.py +397 -0
  98. package/ccx/ops/compact.py +337 -0
  99. package/ccx/ops/compact_ops.py +550 -0
  100. package/ccx/ops/context_events.py +48 -0
  101. package/ccx/ops/conversation.py +1593 -0
  102. package/ccx/ops/conversation_paging.py +1339 -0
  103. package/ccx/ops/create.py +56 -0
  104. package/ccx/ops/creation.py +952 -0
  105. package/ccx/ops/creation_steps.py +575 -0
  106. package/ccx/ops/event_feed.py +710 -0
  107. package/ccx/ops/forking.py +711 -0
  108. package/ccx/ops/history.py +1041 -0
  109. package/ccx/ops/input.py +556 -0
  110. package/ccx/ops/maintenance.py +295 -0
  111. package/ccx/ops/report.py +773 -0
  112. package/ccx/ops/semantics.py +490 -0
  113. package/ccx/ops/send.py +961 -0
  114. package/ccx/ops/steer_ops.py +867 -0
  115. package/ccx/ops/terminate.py +383 -0
  116. package/ccx/ops/turn_reports.py +348 -0
  117. package/ccx/ops/wait.py +762 -0
  118. package/ccx/ops/worker_events.py +640 -0
  119. package/ccx/rpc/__init__.py +0 -0
  120. package/ccx/rpc/local_rpc.py +1602 -0
  121. package/ccx/rpc/rpc.py +559 -0
  122. package/ccx/rpc/serve.py +374 -0
  123. package/ccx/rpc/transport.py +409 -0
  124. package/ccx/rpc/view.py +125 -0
  125. package/ccx/store/__init__.py +0 -0
  126. package/ccx/store/discovery.py +170 -0
  127. package/ccx/store/identity.py +105 -0
  128. package/ccx/store/journal_io.py +79 -0
  129. package/ccx/store/liveness.py +194 -0
  130. package/ccx/store/paths.py +250 -0
  131. package/ccx/store/record_cache.py +40 -0
  132. package/ccx/store/records.py +309 -0
  133. package/ccx/store/root.py +789 -0
  134. package/ccx/store/runtime_presence.py +137 -0
  135. package/ccx/store/store.py +2332 -0
  136. package/ccx/store/worker_state.py +86 -0
  137. package/ccx/system/__init__.py +0 -0
  138. package/ccx/system/bounded.py +296 -0
  139. package/ccx/system/cwd_paths.py +25 -0
  140. package/ccx/system/engine_binary.py +59 -0
  141. package/ccx/system/executable.py +50 -0
  142. package/ccx/system/oslayer.py +667 -0
  143. package/ccx/system/pipes.py +46 -0
  144. package/ccx/system/private_json.py +54 -0
  145. package/ccx/system/sanitize.py +43 -0
  146. package/ccx/types.py +809 -0
  147. package/ccx/version.py +29 -0
  148. package/ccx/workers.py +2093 -0
  149. package/ccx.py +9 -0
  150. package/package.json +39 -7
  151. package/pyproject.toml +87 -0
  152. package/skills/ccx/SKILL.md +166 -0
  153. package/skills/ccx/references/controllers.md +244 -0
  154. package/skills/ccx/references/history.md +50 -0
  155. package/skills/ccx/references/troubleshooting.md +94 -0
  156. 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