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