ccx-native 0.0.0-stage → 0.1.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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -2
  3. package/bin/ccx +56 -0
  4. package/ccx/__init__.py +163 -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 +762 -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 +250 -0
  16. package/ccx/claude/host/session.py +5184 -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 +2281 -0
  26. package/ccx/claude/usage.py +837 -0
  27. package/ccx/claude/usage_control.py +152 -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/read.py +1020 -0
  35. package/ccx/cli/commands/turns.py +1081 -0
  36. package/ccx/cli/controller.py +757 -0
  37. package/ccx/cli/main.py +301 -0
  38. package/ccx/cli/parser.py +1324 -0
  39. package/ccx/cli/skills.py +215 -0
  40. package/ccx/cli/statusline.py +160 -0
  41. package/ccx/client.py +1900 -0
  42. package/ccx/codex/__init__.py +0 -0
  43. package/ccx/codex/attach_tty.py +267 -0
  44. package/ccx/codex/client.py +732 -0
  45. package/ccx/codex/completion.py +121 -0
  46. package/ccx/codex/control.py +288 -0
  47. package/ccx/codex/daemon/__init__.py +0 -0
  48. package/ccx/codex/daemon/__main__.py +4 -0
  49. package/ccx/codex/daemon/daemon.py +1556 -0
  50. package/ccx/codex/daemon/models.py +140 -0
  51. package/ccx/codex/daemon/native.py +1590 -0
  52. package/ccx/codex/daemon/release.py +367 -0
  53. package/ccx/codex/daemon/worker.py +2396 -0
  54. package/ccx/codex/delivery.py +758 -0
  55. package/ccx/codex/delivery_recovery.py +232 -0
  56. package/ccx/codex/evidence.py +827 -0
  57. package/ccx/codex/history.py +158 -0
  58. package/ccx/codex/notify_history.py +140 -0
  59. package/ccx/codex/ops.py +666 -0
  60. package/ccx/codex/paths.py +70 -0
  61. package/ccx/codex/protocol.py +805 -0
  62. package/ccx/codex/provenance.py +105 -0
  63. package/ccx/codex/read.py +29 -0
  64. package/ccx/codex/records.py +51 -0
  65. package/ccx/codex/reports.py +177 -0
  66. package/ccx/codex/runtime.py +1412 -0
  67. package/ccx/codex/settle.py +363 -0
  68. package/ccx/codex/telemetry.py +792 -0
  69. package/ccx/codex/types.py +659 -0
  70. package/ccx/codex/worker_environment.py +25 -0
  71. package/ccx/constants.py +288 -0
  72. package/ccx/controllers/__init__.py +0 -0
  73. package/ccx/controllers/controller.py +1449 -0
  74. package/ccx/controllers/live_vars.py +1706 -0
  75. package/ccx/controllers/live_vars_runner.py +216 -0
  76. package/ccx/controllers/notify.py +213 -0
  77. package/ccx/controllers/orchestrator.py +193 -0
  78. package/ccx/controllers/policy.py +57 -0
  79. package/ccx/controllers/store.py +681 -0
  80. package/ccx/errors.py +634 -0
  81. package/ccx/notify/__init__.py +0 -0
  82. package/ccx/notify/__main__.py +5 -0
  83. package/ccx/notify/client.py +116 -0
  84. package/ccx/notify/daemon.py +480 -0
  85. package/ccx/notify/deliver.py +304 -0
  86. package/ccx/notify/paths.py +51 -0
  87. package/ccx/notify/registry.py +649 -0
  88. package/ccx/notify/select.py +750 -0
  89. package/ccx/notify/settle.py +684 -0
  90. package/ccx/ops/__init__.py +0 -0
  91. package/ccx/ops/background.py +115 -0
  92. package/ccx/ops/cancel.py +289 -0
  93. package/ccx/ops/codex_import.py +96 -0
  94. package/ccx/ops/common.py +397 -0
  95. package/ccx/ops/compact.py +337 -0
  96. package/ccx/ops/compact_ops.py +550 -0
  97. package/ccx/ops/context_events.py +48 -0
  98. package/ccx/ops/conversation.py +1540 -0
  99. package/ccx/ops/conversation_paging.py +1338 -0
  100. package/ccx/ops/create.py +56 -0
  101. package/ccx/ops/creation.py +952 -0
  102. package/ccx/ops/creation_steps.py +579 -0
  103. package/ccx/ops/event_feed.py +710 -0
  104. package/ccx/ops/forking.py +711 -0
  105. package/ccx/ops/input.py +556 -0
  106. package/ccx/ops/maintenance.py +295 -0
  107. package/ccx/ops/report.py +770 -0
  108. package/ccx/ops/semantics.py +490 -0
  109. package/ccx/ops/send.py +961 -0
  110. package/ccx/ops/steer_ops.py +867 -0
  111. package/ccx/ops/terminate.py +383 -0
  112. package/ccx/ops/turn_reports.py +306 -0
  113. package/ccx/ops/wait.py +762 -0
  114. package/ccx/ops/worker_events.py +636 -0
  115. package/ccx/rpc/__init__.py +0 -0
  116. package/ccx/rpc/local_rpc.py +1602 -0
  117. package/ccx/rpc/rpc.py +559 -0
  118. package/ccx/rpc/serve.py +374 -0
  119. package/ccx/rpc/transport.py +409 -0
  120. package/ccx/rpc/view.py +125 -0
  121. package/ccx/store/__init__.py +0 -0
  122. package/ccx/store/discovery.py +170 -0
  123. package/ccx/store/identity.py +105 -0
  124. package/ccx/store/journal_io.py +79 -0
  125. package/ccx/store/liveness.py +194 -0
  126. package/ccx/store/paths.py +254 -0
  127. package/ccx/store/record_cache.py +40 -0
  128. package/ccx/store/records.py +309 -0
  129. package/ccx/store/root.py +804 -0
  130. package/ccx/store/runtime_presence.py +137 -0
  131. package/ccx/store/store.py +2332 -0
  132. package/ccx/store/worker_state.py +86 -0
  133. package/ccx/system/__init__.py +0 -0
  134. package/ccx/system/bounded.py +296 -0
  135. package/ccx/system/cwd_paths.py +25 -0
  136. package/ccx/system/engine_binary.py +59 -0
  137. package/ccx/system/executable.py +50 -0
  138. package/ccx/system/oslayer.py +667 -0
  139. package/ccx/system/pipes.py +46 -0
  140. package/ccx/system/sanitize.py +43 -0
  141. package/ccx/types.py +705 -0
  142. package/ccx/version.py +29 -0
  143. package/ccx/workers.py +2081 -0
  144. package/ccx.py +9 -0
  145. package/package.json +40 -4
  146. package/pyproject.toml +87 -0
  147. package/skills/ccx/SKILL.md +155 -0
  148. package/skills/ccx/references/controllers.md +244 -0
  149. package/skills/ccx/references/troubleshooting.md +94 -0
  150. package/skills/ccx/references/variables.md +62 -0
@@ -0,0 +1,1009 @@
1
+ from __future__ import annotations
2
+ import ccx.ops.input as ops_input
3
+
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import uuid
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from ccx.codex.ops import caller_status, configure_worker
13
+ from ccx.codex.client import CodexClient
14
+ from ccx.codex.types import rate_limit_windows, window_label
15
+ from ccx.claude.usage import _caps_from_args
16
+ from ccx.codex import runtime as runtime
17
+ from ccx.controllers import live_vars
18
+ from ccx.codex.settle import (
19
+ DEFAULT_WAKE_GRACE_S,
20
+ wait_for_settlement,
21
+ )
22
+ from ccx.constants import (
23
+ EXIT_ERROR,
24
+ EXIT_NOT_FOUND,
25
+ EXIT_REJECTED,
26
+ EXIT_TURN_INCOMPLETE,
27
+ EXIT_UNAVAILABLE,
28
+ EXIT_WAITING,
29
+ )
30
+ from ccx.errors import BudgetStopped, CcxError, DeliveryRejected, EngineCliNotFound, InvalidRequest, SendIndeterminate
31
+ from ccx.controllers.orchestrator import require_notify_parent
32
+ from ccx.notify.daemon import prepare_notify_parent
33
+ from ccx.notify.registry import public_registration as public_parent_notification, set_codex_turn_id
34
+
35
+ STABLE_STATES = {"idle", "waiting", "idle-killed", "crashed", "dead"}
36
+
37
+
38
+ def cmd_attach(args) -> None:
39
+ """Run the real Codex remote TUI on codexd's live native thread."""
40
+ from ccx.system.engine_binary import resolve_engine_binary
41
+ from ccx.store.root import GenerationError
42
+
43
+ try:
44
+ if not sys.stdin.isatty() or not sys.stdout.isatty():
45
+ raise InvalidRequest("attach requires terminal stdin and stdout")
46
+ with runtime.attached_worker(args.target) as session:
47
+ argv = [resolve_engine_binary("codex"), "resume", session["threadId"]]
48
+ # Codex's startup update prompt defaults to running an installer.
49
+ argv += ["-c", "check_for_update_on_startup=false"]
50
+ # Turns typed in the TUI use the worker's settings, as CCX sends do.
51
+ if session.get("model"):
52
+ argv += ["-m", session["model"]]
53
+ if session.get("effort"):
54
+ argv += ["-c", f'model_reasoning_effort="{session["effort"]}"']
55
+ argv += ["--remote", "unix://" + session["socket"]]
56
+ from ccx.codex.attach_tty import run_status_tui
57
+ exit_code = run_status_tui(
58
+ argv, cwd=session["cwd"], worker_id=session["workerId"],
59
+ status=not getattr(args, "no_status", False),
60
+ )
61
+ if exit_code:
62
+ if not session.get("hasTurn"):
63
+ # Codex writes a thread's rollout with its first turn.
64
+ print("[ccx] attach: if Codex could not resume this thread, it may "
65
+ "have no turn yet; send it a first message with `ccx send`, "
66
+ "then attach", file=sys.stderr)
67
+ raise SystemExit(exit_code)
68
+ except (CcxError, GenerationError, OSError, RuntimeError) as error:
69
+ print(f"[ccx] attach: {error}", file=sys.stderr)
70
+ raise SystemExit(getattr(error, "exit_code", EXIT_UNAVAILABLE)) from error
71
+
72
+
73
+ def _fail(message: str, code: int = EXIT_ERROR) -> None:
74
+ detail = message.removeprefix("ccx: ")
75
+ if detail.startswith("ccx ") and ": " in detail:
76
+ heading, body = detail.split(": ", 1)
77
+ verb = heading.removeprefix("ccx ")
78
+ while body.startswith((f"{verb}: ", f"{heading}: ")):
79
+ body = body.split(": ", 1)[1]
80
+ detail = f"{heading}: {body}"
81
+ else:
82
+ detail = f"ccx: {detail}"
83
+ print(detail, file=sys.stderr)
84
+ raise SystemExit(code)
85
+
86
+
87
+ def _fail_send_indeterminate(error, args) -> None:
88
+ delivery_id = getattr(error, "delivery_id", None)
89
+ status = getattr(error, "delivery_status", None)
90
+ if getattr(args, "json", False):
91
+ print(json.dumps({
92
+ "accepted": False,
93
+ "reconcile_required": True,
94
+ "delivery_id": delivery_id,
95
+ "delivery_status": status,
96
+ "error": str(error),
97
+ }, indent=2))
98
+ raise SystemExit(EXIT_TURN_INCOMPLETE)
99
+ suffix = f"; delivery_id={delivery_id}" if delivery_id else ""
100
+ _fail(
101
+ f"{error}{suffix}; reconcile this delivery and do not resend it",
102
+ EXIT_TURN_INCOMPLETE,
103
+ )
104
+
105
+
106
+ def _fail_send_rejected(error, short: str) -> None:
107
+ """A definite send refusal: nothing started, so exit with its own code
108
+ (EXIT_REJECTED for busy/waiting) and say how to deliver the text later."""
109
+ message = _send_rejection_guidance(short, {
110
+ "error": getattr(error, "reason", None) or str(error),
111
+ "state": getattr(error, "state", None),
112
+ })
113
+ delivery_id = getattr(error, "delivery_id", None)
114
+ if delivery_id and getattr(error, "delivery_status", None) == "prepared":
115
+ # Refused before submission: the same id is safe to retry later.
116
+ message = (
117
+ f"{message}; nothing was submitted; delivery_id={delivery_id}. To "
118
+ f"send this text once the worker is idle (`ccx wait {short}`), "
119
+ f"retry it with `--delivery-id {delivery_id}`"
120
+ )
121
+ _fail(message, error.exit_code)
122
+
123
+
124
+ def _prompt(args, *, required: bool) -> str | None:
125
+ sources = sum([
126
+ getattr(args, "text", None) is not None
127
+ or getattr(args, "prompt", None) is not None,
128
+ bool(getattr(args, "stdin", False)),
129
+ getattr(args, "file", None) is not None,
130
+ ])
131
+ if sources > 1:
132
+ _fail("use exactly one of positional text, --stdin, or --file")
133
+ positional = getattr(args, "text", None)
134
+ if positional is None:
135
+ positional = getattr(args, "prompt", None)
136
+ if getattr(args, "stdin", False) or getattr(args, "file", None) == "-":
137
+ value = sys.stdin.read()
138
+ elif getattr(args, "file", None):
139
+ try:
140
+ value = Path(args.file).read_text()
141
+ except OSError as error:
142
+ _fail(f"cannot read prompt file: {error}")
143
+ else:
144
+ value = positional
145
+ if required and (value is None or not value.strip()):
146
+ _fail("prompt text is required")
147
+ return value
148
+
149
+
150
+ def _rpc(short: str, message: dict[str, Any], timeout: float = 10) -> dict[str, Any]:
151
+ return runtime.rpc(short, message, timeout)
152
+
153
+
154
+ def _record(target: str) -> dict[str, Any]:
155
+ try:
156
+ return runtime.record(target)
157
+ except CcxError as error:
158
+ _fail(str(error), error.exit_code)
159
+
160
+
161
+ def _require_workspace(record: dict[str, Any]) -> None:
162
+ """Refuse to start project work in a workspace that no longer exists."""
163
+ try:
164
+ runtime.require_workspace(record)
165
+ except CcxError as error:
166
+ _fail(str(error), error.exit_code)
167
+
168
+
169
+ def _identity(record: dict[str, Any]) -> dict[str, Any]:
170
+ return runtime.identity(record)
171
+
172
+
173
+ def _emit(value: dict[str, Any], args, *, plain: str | None = None) -> None:
174
+ if getattr(args, "json", False):
175
+ print(json.dumps(value, indent=2))
176
+ elif plain is not None:
177
+ print(plain)
178
+ else:
179
+ print(value.get("short", ""))
180
+
181
+
182
+ def _wait(short: str, timeout: float | None) -> dict[str, Any]:
183
+ return runtime.wait(short, timeout)
184
+
185
+
186
+ def _emit_wait(
187
+ result: dict[str, Any],
188
+ args,
189
+ *,
190
+ include_report: bool = True,
191
+ expected_turn_id: str | None = None,
192
+ ) -> None:
193
+ state = result.get("state")
194
+ last_turn = result.get("lastTurn") or result.get("last_turn")
195
+ if not isinstance(last_turn, dict):
196
+ last_turn = None
197
+ # A non-success turn is this command's failure only when the command was
198
+ # waiting for that turn. Bare `ccx wait` asks about worker health, so
199
+ # an older failed or interrupted turn stays visible without making a
200
+ # healthy idle worker exit nonzero.
201
+ outcome_matches = expected_turn_id is not None and (
202
+ last_turn is not None and last_turn.get("turnId") == expected_turn_id
203
+ )
204
+ settled_non_success = (
205
+ last_turn is not None
206
+ and last_turn.get("status") in {"failed", "interrupted"}
207
+ )
208
+ # A wait that ran out of budget, or found an active record the daemon no
209
+ # longer serves, is not a stable worker, whatever the recorded state says.
210
+ timed_out = result.get("ok") is False and result.get("error") == "wait timed out"
211
+ orphaned = result.get("ok") is False and result.get("loaded") is False
212
+ output = {
213
+ "engine": "codex",
214
+ "short": result.get("short"),
215
+ "thread_id": result.get("threadId") or result.get("thread_id"),
216
+ "name": result.get("name"),
217
+ "cwd": result.get("cwd"),
218
+ "state": state,
219
+ "active_turn_id": result.get("activeTurnId")
220
+ or result.get("active_turn_id"),
221
+ "pending_approvals": result.get("pendingApprovals") or [],
222
+ "last_turn": last_turn,
223
+ "active_turn_error": (
224
+ result.get("activeTurnError") or result.get("active_turn_error")
225
+ ),
226
+ # Evidence health, beside worker health rather than inside it: a
227
+ # degraded journal never changes the state or the exit code.
228
+ "evidence": result.get("evidence"),
229
+ # The status of the turn this command waited for. Null when no
230
+ # specific turn was awaited (bare `wait`) or when the settled outcome
231
+ # belongs to another turn; `last_turn` stays visible either way.
232
+ "turn_status": (
233
+ last_turn.get("status") if outcome_matches else None
234
+ ),
235
+ "timed_out": timed_out,
236
+ }
237
+ if timed_out or orphaned:
238
+ output["error"] = result.get("error")
239
+ report = result.get("lastReport")
240
+ if not isinstance(report, dict):
241
+ report = None
242
+ # Scopes 02/04 deliberately preserve an older valid report when a newer
243
+ # turn produces none, so the worker's history keeps it. That history is not
244
+ # this operation's answer: a command that waited for one turn may only
245
+ # present a report that came from that turn and settled with it.
246
+ if expected_turn_id is not None and not (
247
+ outcome_matches and report is not None
248
+ and report.get("turnId") == expected_turn_id
249
+ ):
250
+ report = None
251
+ if include_report and report:
252
+ output["report"] = report
253
+ if result.get("settle"):
254
+ output.update({
255
+ "worker": result.get("worker"), "settle": True,
256
+ "settlement_verdict": result.get("settlementVerdict"),
257
+ "settled": bool(result.get("settled")),
258
+ "outstanding_count": int(result.get("outstandingCount") or 0),
259
+ "outstanding": list(result.get("outstanding") or []),
260
+ "outstanding_truncated": bool(result.get("outstandingTruncated")),
261
+ "pending_wake_count": int(result.get("pendingWakeCount") or 0),
262
+ "pending_wake": list(result.get("pendingWake") or []),
263
+ "pending_wake_truncated": bool(result.get("pendingWakeTruncated")),
264
+ "registration_count": int(result.get("registrationCount") or 0),
265
+ "wake_grace_seconds": result.get("wakeGraceSeconds"),
266
+ "last_message": result.get("lastMessage"),
267
+ "error": result.get("error"),
268
+ })
269
+ if getattr(args, "json", False):
270
+ print(json.dumps(output, indent=2))
271
+ elif result.get("settle"):
272
+ verdict = result.get("settlementVerdict") or "unknown"
273
+ print(f"{output['worker']} {verdict}")
274
+ elif timed_out or orphaned:
275
+ print(f"{output['short']} {state} ({result.get('error')})")
276
+ elif state == "waiting":
277
+ print(json.dumps(output["pending_approvals"], indent=2))
278
+ elif settled_non_success:
279
+ # Never print the preserved older report here: it did not come from the
280
+ # turn that failed or was interrupted.
281
+ print(f"{output['short']} {state} (last turn {last_turn['status']})")
282
+ elif report and include_report:
283
+ print(report.get("text", ""))
284
+ else:
285
+ print(f"{output['short']} {state}")
286
+ if result.get("settle"):
287
+ verdict = result.get("settlementVerdict")
288
+ if verdict == "settled":
289
+ return
290
+ if verdict == "approval_waiting":
291
+ raise SystemExit(EXIT_WAITING)
292
+ if verdict in {"crashed", "dead"}:
293
+ raise SystemExit(EXIT_UNAVAILABLE)
294
+ raise SystemExit(EXIT_TURN_INCOMPLETE)
295
+ if timed_out:
296
+ raise SystemExit(EXIT_TURN_INCOMPLETE)
297
+ if orphaned:
298
+ raise SystemExit(EXIT_UNAVAILABLE)
299
+ if state == "waiting":
300
+ raise SystemExit(EXIT_WAITING)
301
+ if state in {"crashed", "dead"}:
302
+ raise SystemExit(EXIT_UNAVAILABLE)
303
+ if expected_turn_id is None:
304
+ return
305
+ # The waited turn owns this exit status: it must be the settled turn and it
306
+ # must have completed. An unknown native status is never success.
307
+ if not outcome_matches or last_turn.get("status") != "completed":
308
+ raise SystemExit(EXIT_TURN_INCOMPLETE)
309
+
310
+
311
+ def _require_parent_notification():
312
+ try:
313
+ parent = require_notify_parent()
314
+ prepare_notify_parent(parent)
315
+ return parent
316
+ except CcxError as error:
317
+ _fail(f"notify: {error}", error.exit_code)
318
+
319
+
320
+ def _prepare_parent_notification(args):
321
+ settle = bool(getattr(args, "settle", False))
322
+ if settle and not getattr(args, "notify_parent", False):
323
+ _fail("--settle requires --notify-parent")
324
+ if not getattr(args, "notify_parent", False):
325
+ return None
326
+ return _require_parent_notification()
327
+
328
+
329
+ def _emit_parent_notification_armed(
330
+ record: dict[str, Any],
331
+ registration: dict[str, Any],
332
+ args,
333
+ *,
334
+ extra: dict[str, Any] | None = None,
335
+ ) -> None:
336
+ output = {**_identity(record), **(extra or {})}
337
+ output["notify_parent"] = public_parent_notification(registration)
338
+ _emit(output, args, plain=record["short"] if not getattr(args, "json", False) else None)
339
+ if not getattr(args, "json", False):
340
+ public = output["notify_parent"]
341
+ print(f"notify: armed {public['event_id']} -> Codex parent {public['parent']}")
342
+ print(f"notify: {public['instruction']}")
343
+
344
+
345
+ def cmd_set(args) -> None:
346
+ """Edit exact original Codex future-turn config; rename remains metadata."""
347
+ name_given = args.name is not None
348
+ model_given = args.model is not None
349
+ effort_given = args.effort is not None
350
+ if not (name_given or model_given or effort_given):
351
+ _fail("set: nothing to change (pass --name, --model and/or --effort)")
352
+ try:
353
+ client = CodexClient()
354
+ if model_given or effort_given:
355
+ updates = {}
356
+ if model_given:
357
+ updates["model"] = None if args.model == "default" else args.model
358
+ if effort_given:
359
+ updates["effort"] = None if args.effort == "default" else args.effort
360
+ # one shared core patch for both settings, before a rename;
361
+ # a native busy refusal must not leave a new display name.
362
+ record = configure_worker(
363
+ args.target, expected_generation=client._uuid_store.generation,
364
+ **updates,
365
+ )
366
+ from ccx.codex.types import CodexSessionInfo
367
+ info = CodexSessionInfo.from_record(record)
368
+ else:
369
+ # No daemon config RPC for pure display metadata.
370
+ info = None
371
+ if name_given:
372
+ info = client.set_name(args.target, args.name)
373
+ assert info is not None
374
+ except CcxError as error:
375
+ _fail(str(error), error.exit_code)
376
+ result = {
377
+ "worker": f"codex:{info.worker_id}", "worker_id": info.worker_id,
378
+ "short": info.short, "engine": "codex",
379
+ "action": ("set+rename" if name_given else "set")
380
+ if (model_given or effort_given) else "rename",
381
+ }
382
+ if name_given:
383
+ result["name"] = info.name
384
+ if model_given:
385
+ result["model"] = info.model
386
+ if effort_given:
387
+ result["effort"] = info.effort
388
+ _emit(result, args, plain=result["worker"])
389
+
390
+
391
+ def cmd_send(args) -> None:
392
+ """Original Codex CLI sends one full-ID native turn through codexd.
393
+
394
+ The one globally owned full UUID and immutable delivery ID select the
395
+ original semantic receipt; Codex native threadId remains independently
396
+ bound. No implicit thread/start, no native-busy steering, and no old
397
+ short/name or external source is authorized.
398
+ """
399
+ from ccx import constants
400
+ import time
401
+ from ccx.ops.send import send_turn as full_send
402
+ from ccx.ops.wait import reconcile_turn
403
+ from ccx.ops.report import report_worker
404
+ from ccx.store.root import GenerationError, require_ccx_active
405
+ from ccx.store.identity import IdentityError, EngineMismatch
406
+ from ccx.codex.delivery import DeliveryReceiptError
407
+ from uuid import uuid4
408
+ if getattr(args, "settle", False) and not args.notify_parent:
409
+ _fail("--settle requires --notify-parent")
410
+ if args.timeout is not None and args.timeout <= 0:
411
+ _fail("positive timeout required")
412
+ prompt = _prompt(args, required=True)
413
+ token = args.delivery_id or str(uuid4())
414
+ try:
415
+ store=require_ccx_active(constants.CCX_ROOT)
416
+ owner=store.resolve(args.target)
417
+ if owner.engine != "codex":
418
+ raise EngineMismatch("Send requires a Codex worker handle")
419
+ if args.var is not None or args.vars_from is not None:
420
+ from ccx.cli.commands.turns import _publish_cli_send_variables
421
+ prompt = _publish_cli_send_variables(store, owner, args, prompt, token)
422
+ first=full_send(owner.worker,prompt,delivery_id=token,
423
+ cap=_caps_from_args(args),
424
+ mode="new_turn_only", notify_parent=bool(args.notify_parent),
425
+ settle=bool(getattr(args, "settle", False)))
426
+ # Parser makes --notify-parent / --wait / --no-wait exclusive;
427
+ # the parent gets its durable event, then yields rather than polling.
428
+ should_wait=not (args.no_wait or args.notify_parent)
429
+ started=time.monotonic()
430
+ observation=None
431
+ failure=None
432
+ if should_wait:
433
+ from ccx.codex.settle import send_settle_failure
434
+ while True:
435
+ # native-observe can hold for its entire budget: poll in short
436
+ # slices so a failed turn cannot consume a 120s send timeout.
437
+ observation=reconcile_turn(
438
+ owner.worker, delivery_id=token,
439
+ timeout=(0.5 if args.timeout is None else
440
+ max(0.001, min(0.5, args.timeout-(time.monotonic()-started)))),
441
+ )
442
+ if observation["status"] in ("settled","rejected"):
443
+ break
444
+ failure=send_settle_failure(owner.worker, observation)
445
+ if failure is not None:
446
+ break
447
+ if args.timeout is not None and time.monotonic()-started>=args.timeout:
448
+ break
449
+ time.sleep(.1)
450
+ complete = (first.get("settled") if observation is None
451
+ else observation["status"]=="settled")
452
+ result={
453
+ "worker":owner.worker, "worker_id":owner.worker_id,
454
+ "engine":"codex","short":owner.short,
455
+ "delivery_id":token,
456
+ "status":(failure["status"] if failure else
457
+ observation["status"] if observation else first["status"]),
458
+ "settled":bool(complete),
459
+ "delivery":(observation.get("delivery") if observation
460
+ else first.get("delivery")),
461
+ }
462
+ if failure is not None:
463
+ result["error"]=failure.get("error")
464
+ if args.notify_parent:
465
+ result["notify_parent"]=first.get("notification")
466
+ if complete and not args.notify_parent:
467
+ # Report this delivery's original R, never the worker's latest.
468
+ turn_ref=(result["delivery"] or {}).get("turn_ref")
469
+ if not turn_ref:
470
+ from ccx.errors import SessionUnavailable
471
+ raise SessionUnavailable(
472
+ "Completed delivery has no turn ID; "
473
+ "reconcile its delivery ID"
474
+ )
475
+ result["report"]=report_worker(owner.worker,turn_ref=turn_ref)
476
+ if args.json:
477
+ _emit(result,args)
478
+ else:
479
+ print(owner.worker)
480
+ print(f"delivery_id: {token}")
481
+ if args.notify_parent:
482
+ notification = result["notify_parent"] or {}
483
+ print(f"notification: {notification.get('event_id')} ({notification.get('state')})")
484
+ elif complete:
485
+ print(result["report"]["text"])
486
+ else:
487
+ print(f"status: {result['status']}" +
488
+ (f"; error: {result['error']}" if result.get("error") else ""))
489
+ if result["status"]=="rejected":
490
+ raise SystemExit(EXIT_REJECTED)
491
+ if should_wait and not complete:
492
+ from ccx.cli.commands._common import _nbwait_exit_code
493
+ raise SystemExit(
494
+ (EXIT_UNAVAILABLE if failure["status"] == "unavailable" else
495
+ _nbwait_exit_code(failure["status"], failure.get("apiError")))
496
+ if failure is not None else EXIT_TURN_INCOMPLETE
497
+ )
498
+ except SendIndeterminate as error:
499
+ _fail_send_indeterminate(error, args)
500
+ except BudgetStopped as error:
501
+ if args.json:
502
+ _emit({
503
+ "worker": owner.worker, "worker_id": owner.worker_id,
504
+ "engine": "codex", "short": owner.short,
505
+ "delivery_id": token, "status": "budget-stopped",
506
+ "code": "budget_stopped", "reason": error.reason or str(error),
507
+ "settled": False,
508
+ }, args)
509
+ raise SystemExit(error.exit_code) from None
510
+ _fail("ccx send: " + str(error), error.exit_code)
511
+ except CcxError as error:
512
+ # Definite busy refusal is exit 5; immutable input/parent conflicts
513
+ # remain typed, and no uncertain native write is reported as unavailable.
514
+ _fail("ccx send: "+str(error), error.exit_code)
515
+ except (GenerationError, IdentityError, DeliveryReceiptError) as error:
516
+ _fail("ccx send: native delivery unavailable: "+str(error),3)
517
+
518
+
519
+ def cmd_reconcile(args) -> None:
520
+ """Read-only reconciliation of one original Codex delivery or turn receipt.
521
+
522
+ The exact owned full UUID's semantic receipt is the only authority; a
523
+ native turn CCX holds no receipt for is not_found, never accepted.
524
+ """
525
+ from ccx import constants
526
+ from ccx.codex.delivery import DeliveryReceiptError
527
+ from ccx.codex.ops import _semantic_status
528
+ from ccx.ops.wait import reconcile_turn
529
+ from ccx.store.root import GenerationError, require_ccx_active
530
+ from ccx.store.identity import EngineMismatch, IdentityError
531
+ try:
532
+ store = require_ccx_active(constants.CCX_ROOT)
533
+ owner = store.resolve(args.target)
534
+ if owner.engine != "codex":
535
+ raise EngineMismatch("Reconcile requires a Codex worker handle")
536
+ result = reconcile_turn(
537
+ owner.worker,
538
+ delivery_id=getattr(args, "delivery_id", None),
539
+ turn_id=getattr(args, "turn_id", None),
540
+ )
541
+ record = store.record(owner.worker)
542
+ except CcxError as error:
543
+ _fail(str(error), error.exit_code)
544
+ except (GenerationError, IdentityError, DeliveryReceiptError) as error:
545
+ _fail("ccx reconcile unavailable: " + str(error), 3)
546
+
547
+ # Project from the semantic receipt, not the shared raw status: a prepared
548
+ # receipt whose send effect advanced recommends "wait" (pending, never
549
+ # retryable), and accepted stays delivered (exit 0) whether settled or not.
550
+ delivery = result.get("delivery")
551
+ semantic = _semantic_status(delivery)
552
+ status, pending = caller_status({
553
+ "status": semantic, "pending": semantic == "pending", "delivery": delivery,
554
+ })
555
+ output = {
556
+ **_identity(record), "worker": owner.worker, "worker_id": owner.worker_id,
557
+ "engine": "codex", "thread_id": (record.get("native") or {}).get("threadId"),
558
+ "reconcile_status": status,
559
+ "pending": pending,
560
+ "retryable": status == "not_delivered",
561
+ "delivery": delivery,
562
+ }
563
+ turn = (delivery or {}).get("turn_id") or getattr(args, "turn_id", None) or "-"
564
+ delivery_id = (delivery or {}).get("delivery_id") or getattr(args, "delivery_id", None) or "-"
565
+ _emit(output, args, plain=(
566
+ f"{owner.short} {status} delivery_id: {delivery_id} turn: {turn}"
567
+ ))
568
+
569
+ if status == "not_found":
570
+ raise SystemExit(EXIT_NOT_FOUND)
571
+ # Definitively not delivered shares the refused send's exit code.
572
+ if status in {"rejected", "not_delivered"}:
573
+ raise SystemExit(EXIT_REJECTED)
574
+ if pending:
575
+ raise SystemExit(EXIT_TURN_INCOMPLETE)
576
+
577
+
578
+ def _send_rejection_guidance(short: str, reply: dict[str, Any]) -> str:
579
+ """A refused strict send, named with the verb the caller actually wanted."""
580
+ error = str(reply.get("error") or "send rejected")
581
+ state = reply.get("state")
582
+ if state == "waiting":
583
+ return (f"{error}; the worker is blocked on an approval — resolve it "
584
+ f"with `ccx approve {short}` or `ccx reject {short}`")
585
+ if state == "busy":
586
+ return (f"{error}; `send` starts a new turn on an idle worker. To add "
587
+ f"input to the turn already running, use "
588
+ f"`ccx steer {short} \"...\"`")
589
+ return error
590
+
591
+
592
+ def cmd_notify(args) -> None:
593
+ """Arm an independent read-only observation of one original Codex turn."""
594
+ from ccx import constants
595
+ from ccx.errors import CcxError, InvalidRequest, SessionNotFound, SessionUnavailable
596
+ from ccx.controllers.orchestrator import require_notify_parent
597
+ from ccx.notify.registry import create_registration, public_registration
598
+ from ccx.notify.daemon import ensure_notify_daemon
599
+ from ccx.notify.select import select_current_turn
600
+ from ccx.store.root import GenerationError, require_ccx_active
601
+ from ccx.store.identity import IdentityError, UnknownWorker
602
+
603
+ try:
604
+ try:
605
+ store = require_ccx_active(constants.CCX_ROOT)
606
+ owner = store.resolve(args.target)
607
+ if owner.engine != "codex":
608
+ raise InvalidRequest("Notification requires a Codex worker handle")
609
+ parent = require_notify_parent()
610
+ selection = select_current_turn(owner.worker)
611
+ reg = create_registration(
612
+ parent=parent, worker_engine="codex", worker_id=owner.worker_id,
613
+ worker_name=selection["name"], operation=selection["operation"],
614
+ source="notify", settle=bool(getattr(args, "settle", False)),
615
+ )
616
+ except UnknownWorker as exc:
617
+ raise SessionNotFound(f"ccx notify: no such managed worker {args.target}") from exc
618
+ except IdentityError as exc:
619
+ raise InvalidRequest(f"Invalid worker handle for notification: {exc}") from exc
620
+ except (GenerationError, OSError) as exc:
621
+ # Selection or publication failed; never reported as an armed event.
622
+ raise SessionUnavailable(f"Cannot select a Codex turn to notify about; check status: {exc}") from exc
623
+ available = True
624
+ try:
625
+ ensure_notify_daemon()
626
+ except (CcxError, GenerationError, OSError) as exc:
627
+ available = False
628
+ print(f"ccx notify: event durably armed; supervisor unavailable: {exc}",
629
+ file=sys.stderr)
630
+ result = {**public_registration(reg), "armed": True,
631
+ "selected_state": selection["state"],
632
+ "notifier_available": available}
633
+ except CcxError as exc:
634
+ _fail(str(exc), exc.exit_code)
635
+ _emit(result, args, plain=f"{result['worker']} event_id={result['event_id']}")
636
+
637
+
638
+
639
+ def cmd_steer(args) -> None:
640
+ """One original full-UUID Codex native steer, accepted placement only."""
641
+ from uuid import uuid4
642
+
643
+ text = _prompt(args, required=True)
644
+ token = getattr(args, "delivery_id", None)
645
+ if token is None:
646
+ token = str(uuid4())
647
+ try:
648
+ accepted = CodexClient().steer(
649
+ args.target, text, delivery_id=token,
650
+ caps=_caps_from_args(args),
651
+ )
652
+ except SendIndeterminate as error:
653
+ _fail_send_indeterminate(error, args)
654
+ except DeliveryRejected as error:
655
+ if getattr(error, "delivery_status", None) is None:
656
+ _fail(f"steer: {error}; delivery_id={token}; nothing recorded; "
657
+ "this ID may be used when a turn is running", error.exit_code)
658
+ _fail(f"steer: {error}; delivery_id={token}; "
659
+ "this rejected delivery is terminal; choose a new ID for a "
660
+ "new instruction", error.exit_code)
661
+ except CcxError as error:
662
+ _fail(f"steer: {error}; delivery_id={token}", error.exit_code)
663
+ value = accepted.as_dict()
664
+ _emit(value, args, plain=f"{value['worker']} steer accepted-current-turn "
665
+ f"delivery_id={accepted.delivery_id}")
666
+
667
+
668
+ def cmd_stop(args) -> None:
669
+ """Request only this full-UUID Codex worker's active native turn interrupt."""
670
+ from ccx.ops.cancel import stop_worker
671
+ try:
672
+ result = stop_worker(args.target)
673
+ except CcxError as error:
674
+ if getattr(args, "json", False):
675
+ print(json.dumps({
676
+ "ok": False, "error": str(error),
677
+ "status": getattr(error, "turn_status", "rejected"
678
+ if isinstance(error, DeliveryRejected) else "unavailable"),
679
+ "cancellation_id": getattr(error, "cancellation_id", None),
680
+ "turn_ref": getattr(error, "turn_ref", None),
681
+ }))
682
+ _fail(f"stop: {error}", error.exit_code)
683
+ value = {
684
+ **result, "ok": True, "short": result["worker"]["short"],
685
+ "requested": True,
686
+ "cancelled": result["confirmed"] and result["outcome"] == "interrupted",
687
+ "interrupted": result["confirmed"] and result["outcome"] == "interrupted",
688
+ "turn_id": result["turn_ref"].rsplit(":", 1)[-1],
689
+ }
690
+ _emit(value, args, plain=result["worker"]["worker"])
691
+
692
+ def cmd_kill(args) -> None:
693
+ """Unload only the exact full-UUID Codex worker, never the shared daemon."""
694
+ from ccx.ops.wait import park_worker
695
+ try:
696
+ result = park_worker(args.target, timeout=getattr(args, "timeout", 90))
697
+ except CcxError as error:
698
+ _fail(f"kill: {error}", error.exit_code)
699
+ _emit(result, args, plain=result["worker"])
700
+
701
+ def cmd_wait(args) -> None:
702
+ from ccx.cli.commands.turns import _emit_full_uuid_wait
703
+ from ccx.ops.wait import wait_worker
704
+ try:
705
+ if getattr(args, "settle", False):
706
+ verdict = wait_for_settlement(
707
+ args.target, timeout=getattr(args, "timeout", None),
708
+ wake_grace_s=getattr(args, "wake_grace", DEFAULT_WAKE_GRACE_S))
709
+ else:
710
+ verdict = wait_worker(args.target, timeout=getattr(args, "timeout", None))
711
+ except CcxError as error:
712
+ _fail(f"wait: {error}", error.exit_code)
713
+ if verdict.get("settle"):
714
+ _emit_wait(verdict, args)
715
+ else:
716
+ _emit_full_uuid_wait(args, verdict)
717
+
718
+ def _decision(args, operation: str) -> None:
719
+ # Freeze one current CCX gateId before parent preparation or native effect.
720
+ # Supplied IDs pass through unchanged; the backend fences the original gate.
721
+ if getattr(args, "settle", False) and not getattr(args, "notify_parent", False):
722
+ _fail("--settle requires --notify-parent")
723
+ try:
724
+ request_id = args.request_id
725
+ record = None
726
+ if request_id is None:
727
+ record = _record(args.target)
728
+ gates = [gate for gate in record.get("pendingApprovals") or []
729
+ if isinstance(gate, dict)]
730
+ if not gates:
731
+ raise DeliveryRejected(f"Codex {operation}: no pending approval; check ccx peek.")
732
+ if len(gates) != 1:
733
+ raise InvalidRequest(
734
+ "Several approvals are pending; specify --request-id from ccx peek."
735
+ )
736
+ request_id = gates[0].get("gateId")
737
+ if not isinstance(request_id, str) or not request_id:
738
+ raise DeliveryRejected("Cannot identify a pending approval; check ccx peek.")
739
+ _prepare_parent_notification(args)
740
+ if getattr(args, "notify_parent", False):
741
+ from ccx import constants
742
+ from ccx.ops import common as protocol_ops
743
+ from ccx.store.root import require_ccx_active
744
+ if record is None:
745
+ record = _record(args.target)
746
+ store = require_ccx_active(constants.CCX_ROOT)
747
+ owner = store.resolve(f"codex:{record['workerId']}")
748
+ # Deterministic gate response token, never the native requestId.
749
+ token = str(uuid.uuid5(
750
+ uuid.NAMESPACE_URL,
751
+ f"ccx-input-decision:{store.generation}:{owner.worker_id}:{request_id}",
752
+ ))
753
+ result = ops_input.decide_input(
754
+ owner.worker, request_id=request_id, response_id=token,
755
+ decision=operation, notify_parent=True,
756
+ settle=bool(getattr(args, "settle", False)),
757
+ )
758
+ public = {
759
+ "worker": owner.worker, "worker_id": owner.worker_id,
760
+ "request_id": request_id, "response_id": token,
761
+ "decision": "accept" if operation == "approve" else "decline",
762
+ "status": result["status"], "owned": result["owned"],
763
+ "recommended_action": result["recommended_action"],
764
+ "decision_sent": result["status"] == "accepted",
765
+ "notify_parent": result["notification"],
766
+ }
767
+ _emit(public, args, plain=f"{operation} {result['status']} for Codex request "
768
+ f"{request_id}")
769
+ if result["status"] in {"submitted", "unknown"}:
770
+ raise SystemExit(EXIT_TURN_INCOMPLETE)
771
+ if result["status"] == "rejected":
772
+ raise SystemExit(EXIT_REJECTED)
773
+ return
774
+ client = CodexClient()
775
+ native = (client.approve if operation == "approve" else client.reject)(
776
+ args.target, request_id=request_id)
777
+ except CcxError as error:
778
+ _fail(str(error), error.exit_code)
779
+ result = native.as_dict()
780
+ result["decision_sent"] = True # Native gate response; no model completion.
781
+ _emit(result, args, plain=f"{operation} accepted for Codex request {request_id}")
782
+
783
+ def cmd_approve(args) -> None:
784
+ _decision(args, "approve")
785
+
786
+
787
+ def cmd_reject(args) -> None:
788
+ _decision(args, "reject")
789
+
790
+
791
+ def cmd_compact(args) -> None:
792
+ """Compact the exact owned Codex thread through the shared operation core."""
793
+ from ccx.codex.ops import compact_worker
794
+ from ccx.constants import CCX_ROOT
795
+ from ccx.errors import (
796
+ CompactFailed, CompactIndeterminate,
797
+ )
798
+ from ccx.store.root import GenerationError, NativeEffectIndeterminate, require_ccx_active
799
+ from ccx.store.identity import EngineMismatch, IdentityError, UnknownWorker
800
+
801
+ operation_id = str(uuid.uuid4()) # Internal; Codex CLI has no ID flag.
802
+ if getattr(args, "notify_parent", False) or getattr(args, "settle", False):
803
+ from ccx.notify.registry import compact_notification_failure, create_registration, public_registration
804
+ notification = None
805
+ try:
806
+ if not getattr(args, "notify_parent", False):
807
+ raise InvalidRequest("--settle requires --notify-parent")
808
+ store = require_ccx_active(CCX_ROOT)
809
+ owner = store.resolve(args.target)
810
+ if owner.engine != "codex":
811
+ raise InvalidRequest("Codex compact notification needs a Codex worker")
812
+ record = store.record(owner.worker)
813
+ parent = require_notify_parent()
814
+ prepare_notify_parent(parent)
815
+ notification = create_registration(
816
+ parent=parent, worker_engine="codex", worker_id=owner.worker_id,
817
+ worker_name=record.get("name"),
818
+ operation={"kind": "codex-compact", "operationId": operation_id,
819
+ "nativeSourceId": (record.get("native") or {}).get("threadId")},
820
+ source="compact", settle=bool(getattr(args, "settle", False)),
821
+ )
822
+ # One native call after the durable parent event.
823
+ result = compact_worker(
824
+ owner.worker, operation_id=operation_id, wait=False,
825
+ timeout=args.timeout)
826
+ if result["status"] in {"prepared", "rejected", "not_found"}:
827
+ raise DeliveryRejected(
828
+ result.get("reason") or "Codex did not start compaction; check worker status",
829
+ refusal="native-rejected", operation_id=operation_id)
830
+ if result["status"] == "failed":
831
+ raise CompactFailed(result.get("reason") or
832
+ "Codex compaction failed; check worker status",
833
+ operation_id=operation_id)
834
+ if (result["status"] == "unknown"
835
+ or (result["status"] == "submitted"
836
+ and result.get("receipt_status") != "accepted")):
837
+ raise CompactIndeterminate(
838
+ "Codex compaction outcome is unknown; "
839
+ "keep the notification and operation ID to check later",
840
+ operation_id=operation_id, operation_status=result["status"])
841
+ except (CcxError, GenerationError, NativeEffectIndeterminate,
842
+ IdentityError, EngineMismatch, UnknownWorker, OSError) as error:
843
+ if notification is not None:
844
+ try:
845
+ uncertain, state = compact_notification_failure(notification, str(error))
846
+ except (ValueError, OSError) as failure:
847
+ uncertain, state = True, f"unreadable ({failure})"
848
+ code = EXIT_TURN_INCOMPLETE if uncertain else getattr(
849
+ error, "exit_code", EXIT_UNAVAILABLE)
850
+ _fail(f"{error}; operation_id={operation_id}; "
851
+ f"event_id={notification['eventId']}; notification_state={state}"
852
+ + ("; effect uncertain, no resubmission" if uncertain
853
+ else "; no native compact was submitted"), code)
854
+ _fail(f"{error}; operation_id={operation_id}",
855
+ getattr(error, "exit_code", EXIT_UNAVAILABLE))
856
+ record = store.record(owner.worker)
857
+ status = result["status"]
858
+ proven = status == "succeeded" and result.get("compacted") is True
859
+ output = {
860
+ **_identity(record), "worker": owner.worker, "worker_id": owner.worker_id,
861
+ "engine": "codex", "thread_id": (record.get("native") or {}).get("threadId"),
862
+ "operation_id": operation_id, "operation_status": status,
863
+ "accepted": result.get("receipt_status") == "accepted" or proven,
864
+ "compacted": proven, "settled": False,
865
+ "compact_turn_id": result.get("compactTurnId"),
866
+ "context_item_id": result.get("contextItemId"),
867
+ "native_turn_status": result.get("nativeTurnStatus"),
868
+ "context_before": result.get("contextBefore"),
869
+ "context_after": result.get("contextAfter") if proven else None,
870
+ "notify_parent": public_registration(notification),
871
+ }
872
+ _emit(output, args, plain=(f"Codex compact {status} (parent not yet notified): "
873
+ f"{owner.worker}; operation_id={operation_id}; "
874
+ f"event_id={notification['eventId']}"))
875
+ return
876
+ try:
877
+ result = compact_worker(
878
+ args.target, operation_id=operation_id,
879
+ wait=not args.no_wait, timeout=args.timeout,
880
+ )
881
+ status = result["status"]
882
+ if status == "failed":
883
+ raise CompactFailed(
884
+ result.get("reason") or "Codex compaction failed; check worker status",
885
+ operation_id=operation_id,
886
+ )
887
+ if status == "rejected":
888
+ raise DeliveryRejected(
889
+ result.get("reason") or "Codex refused compaction; check worker status",
890
+ refusal="native-rejected", operation_id=operation_id,
891
+ )
892
+ if not args.no_wait and status != "succeeded":
893
+ raise CompactIndeterminate(
894
+ f"Codex compaction outcome is unknown; inspect its operation ({status})",
895
+ operation_id=operation_id, operation_status=status,
896
+ )
897
+ store = require_ccx_active(CCX_ROOT)
898
+ owner = store.resolve(args.target)
899
+ record = store.record(owner.worker)
900
+ identity = _identity(record)
901
+ native = record.get("native") or {}
902
+ proven = status == "succeeded" and result.get("compacted") is True
903
+ output = {
904
+ **identity, "worker": owner.worker, "worker_id": owner.worker_id,
905
+ "engine": "codex", "thread_id": native.get("threadId"),
906
+ "operation_id": operation_id, "operation_status": status,
907
+ "accepted": result.get("receipt_status") == "accepted" or proven,
908
+ "compacted": proven,
909
+ "context_before": result.get("contextBefore"),
910
+ "context_after": result.get("contextAfter") if proven else None,
911
+ "compact_turn_id": result.get("compactTurnId"),
912
+ "context_item_id": result.get("contextItemId"),
913
+ "native_turn_status": result.get("nativeTurnStatus"),
914
+ "reason": result.get("reason"),
915
+ }
916
+ except NativeEffectIndeterminate as error:
917
+ error = CompactIndeterminate(
918
+ f"Codex native compact admission unknown: {error}",
919
+ operation_id=operation_id, operation_status="unknown",
920
+ )
921
+ _fail(f"{error}; operation_id={operation_id}", error.exit_code)
922
+ except UnknownWorker as error:
923
+ _fail(f"{error}; operation_id={operation_id}", EXIT_NOT_FOUND)
924
+ except (IdentityError, EngineMismatch) as error:
925
+ _fail(f"{error}; operation_id={operation_id}", EXIT_ERROR)
926
+ except GenerationError as error:
927
+ _fail(f"Cannot compact Codex; inspect its status: {error}; "
928
+ f"operation_id={operation_id}", EXIT_UNAVAILABLE)
929
+ except CcxError as error:
930
+ _fail(f"{error}; operation_id={operation_id}", error.exit_code)
931
+
932
+ if output["compacted"]:
933
+ plain = f"Codex compact succeeded: {output['worker']}; operation_id={operation_id}"
934
+ else:
935
+ plain = (f"Codex compact {status} (not proven complete): "
936
+ f"{output['worker']}; operation_id={operation_id}")
937
+ _emit(output, args, plain=plain)
938
+
939
+
940
+ def _reset_time(value: Any) -> str:
941
+ from ccx.cli.commands._common import local_reset_time
942
+ return local_reset_time(value) if isinstance(value, (int, float)) else ""
943
+
944
+
945
+ def _quota_rows(data: dict[str, Any]) -> list[tuple[str, int, str]]:
946
+ rows: list[tuple[str, int, str]] = []
947
+ for limit_id, window in sorted(
948
+ rate_limit_windows(data),
949
+ key=lambda item: (item[0].casefold() != "codex", item[0].casefold()),
950
+ ):
951
+ by_id = data.get("rateLimitsByLimitId") or {}
952
+ bucket = (by_id.get(limit_id) if isinstance(by_id, dict) and by_id
953
+ else data.get("rateLimits"))
954
+ bucket_name = (bucket.get("limitName") if isinstance(bucket, dict) else None) or limit_id
955
+ suffix = (
956
+ ""
957
+ if not bucket_name or str(bucket_name).casefold() == "codex"
958
+ else f" {bucket_name}"
959
+ )
960
+ used = window.get("usedPercent")
961
+ if type(used) not in (int, float):
962
+ continue
963
+ rows.append((
964
+ window_label(window) + suffix,
965
+ int(used),
966
+ _reset_time(window.get("resetsAt")),
967
+ ))
968
+ return rows
969
+
970
+
971
+ def cmd_usage(args) -> None:
972
+ """Print live Codex account quota utilization or token activity."""
973
+ tokens = getattr(args, "tokens", False)
974
+ try:
975
+ data = runtime.read_account_usage(tokens=tokens)
976
+ except CcxError as error:
977
+ if args.json and isinstance(error, EngineCliNotFound):
978
+ print(json.dumps({"ok": False, "code": "runtime_unavailable",
979
+ "message": str(error)}, sort_keys=True))
980
+ raise SystemExit(error.exit_code) from error
981
+ _fail(str(error), error.exit_code)
982
+ if args.json:
983
+ print(json.dumps(data, indent=2))
984
+ return
985
+ if tokens:
986
+ summary = data.get("summary") or {}
987
+ print("usage (tokens):")
988
+ fields = (
989
+ ("lifetime", "lifetimeTokens"),
990
+ ("peak/day", "peakDailyTokens"),
991
+ ("streak", "currentStreakDays"),
992
+ )
993
+ emitted = False
994
+ for label, field in fields:
995
+ value = summary.get(field)
996
+ if isinstance(value, int):
997
+ print(f" {label:<10} {value:>12,}")
998
+ emitted = True
999
+ if not emitted:
1000
+ print(" unavailable")
1001
+ return
1002
+ print("usage (live):")
1003
+ rows = _quota_rows(data)
1004
+ if not rows:
1005
+ print(" unavailable")
1006
+ return
1007
+ for label, used, resets in rows:
1008
+ reset_text = f" resets {resets}" if resets else ""
1009
+ print(f" {label:<10} {used:>5.1f}%{reset_text}")