ccx-native 0.0.1 → 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 -4
  3. package/bin/ccx +55 -2
  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 +39 -7
  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,337 @@
1
+ from __future__ import annotations
2
+ import time
3
+ from typing import Any
4
+ from ccx.client import _compact_failed
5
+ from ccx.constants import COMPACT_WAIT_DEFAULT_S
6
+ from ccx.errors import (
7
+ CompactIndeterminate,
8
+ DeliveryRejected,
9
+ InvalidRequest,
10
+ SessionNotFound,
11
+ SessionUnavailable,
12
+ )
13
+ from ccx.ops.common import split_target
14
+ from ccx.ops.send import ensure_ready
15
+
16
+
17
+ def _original_claude_compact_view(store, owner, rec: dict, operation_id: str,
18
+ *, publish: bool = True) -> dict | None:
19
+ """One source-verified projection for both mutating and pure observations."""
20
+ from ccx.ops.compact_ops import observe_original_operation
21
+
22
+ operation = observe_original_operation(
23
+ store, owner, rec, operation_id, publish=publish,
24
+ )
25
+ if operation is None:
26
+ return None
27
+ result = operation.get("result") or {}
28
+ status = operation.get("status") or "unknown"
29
+ latest = store.record(owner.worker)
30
+ return {
31
+ "short": owner.short, "worker_id": owner.worker_id,
32
+ "state": (latest.get("lifecycle") or {}).get("state"),
33
+ "compacted": status == "succeeded",
34
+ "message": result.get("message"),
35
+ "pre_tokens": result.get("pre_tokens"),
36
+ "post_tokens": result.get("post_tokens"),
37
+ "duration_ms": result.get("duration_ms"),
38
+ "operation_id": operation_id, "operation_status": status,
39
+ "evidence_uuid": operation.get("evidence_uuid"),
40
+ "reason": operation.get("reason"),
41
+ }
42
+
43
+
44
+ def compact_status(target: str, *, operation_id: str, timeout: float = 0.75,
45
+ publish: bool = True) -> dict[str, Any]:
46
+ """Read original Claude compact; pure if publish=False, never native."""
47
+ from ccx import constants
48
+ from ccx.ops.compact_ops import get_operation, compact_intent_digest, CompactJournalError
49
+ from ccx.store.root import require_ccx_active
50
+ from ccx.store.root import GenerationError
51
+
52
+ handle, engine, worker_id = split_target(target)
53
+ if engine != "claude":
54
+ raise SessionUnavailable("Codex compaction is unavailable for this worker")
55
+ if (not isinstance(operation_id, str) or not operation_id
56
+ or len(operation_id) > 128 or "\x00" in operation_id):
57
+ raise InvalidRequest("compact_status needs a nonempty operation_id of at most 128 characters")
58
+ store = require_ccx_active(constants.CCX_ROOT)
59
+ owner = store.resolve(handle)
60
+ rec = store.record(handle)
61
+ native = rec.get("native") or {}
62
+ session_id = native.get("sessionId") if isinstance(native, dict) else None
63
+ try:
64
+ prior = get_operation(owner.worker, operation_id)
65
+ receipt = store.read_receipt(owner.worker, "compact", operation_id)
66
+ if prior is None:
67
+ if receipt is None:
68
+ raise SessionNotFound(f"No compaction found for operation {operation_id!r}", short=owner.short)
69
+ raise CompactIndeterminate(
70
+ "Compaction result cannot be verified; inspect its operation",
71
+ short=owner.short, operation_id=operation_id, operation_status="unknown",
72
+ )
73
+ digest = compact_intent_digest(
74
+ owner.worker_id, generation=store.generation,
75
+ native_session_id=session_id, operation_id=operation_id,
76
+ )
77
+ if (not receipt or receipt.get("state") not in (
78
+ "submitted-unknown", "accepted", "rejected")
79
+ or receipt.get("intentSha256") != digest
80
+ or receipt.get("workerId") != owner.worker_id
81
+ or receipt.get("engine") != "claude"
82
+ or receipt.get("generation") != store.generation
83
+ or receipt.get("family") != "compact"
84
+ or receipt.get("action") != "compact"):
85
+ raise CompactIndeterminate(
86
+ "Compaction confirmation is missing; inspect its operation",
87
+ short=owner.short, operation_id=operation_id, operation_status="unknown",
88
+ )
89
+ viewed = _original_claude_compact_view(
90
+ store, owner, rec, operation_id, publish=publish,
91
+ )
92
+ assert viewed is not None
93
+ return viewed
94
+ except (GenerationError, CompactJournalError, OSError) as exc:
95
+ raise CompactIndeterminate(
96
+ f"Cannot read compaction status; retry: {exc}",
97
+ short=owner.short, operation_id=operation_id, operation_status="unknown",
98
+ ) from exc
99
+
100
+
101
+ def compact_worker(
102
+ target: str,
103
+ *,
104
+ operation_id: str,
105
+ timeout: float | None = 90,
106
+ wait: bool = True,
107
+ execution_timeout: float | None = None,
108
+ ) -> dict[str, Any]:
109
+ """One receipt-fenced native Claude /compact, then same-ID journal observation."""
110
+ from ccx import constants
111
+ from ccx.ops.compact_ops import compact_intent_digest, get_operation, CompactJournalError
112
+ from ccx.constants import COMPACT_POLL_S
113
+ import math
114
+ from ccx.store.root import GenerationError, NativeEffectIndeterminate, require_ccx_active
115
+ from ccx.store.store import _digest
116
+ from ccx.rpc.rpc import _rpc
117
+
118
+ handle, engine, worker_id = split_target(target)
119
+ if engine != "claude":
120
+ raise SessionUnavailable("Codex compaction is unavailable for this worker")
121
+ if (not isinstance(operation_id, str) or not operation_id
122
+ or len(operation_id) > 128 or "\x00" in operation_id):
123
+ raise InvalidRequest("compact operation_id must be a nonempty string of at most 128 characters")
124
+ if timeout is not None and (type(timeout) not in (float, int)
125
+ or not math.isfinite(timeout) or timeout < 0):
126
+ raise InvalidRequest("compact observation timeout must be finite, nonnegative or None")
127
+ if (execution_timeout is not None
128
+ and (type(execution_timeout) not in (int, float)
129
+ or not math.isfinite(execution_timeout) or execution_timeout <= 0)):
130
+ raise InvalidRequest("compact execution_timeout must be finite and positive or None")
131
+ deadline = None if timeout is None else time.monotonic() + timeout
132
+ store = require_ccx_active(constants.CCX_ROOT)
133
+ owner = store.resolve(handle)
134
+ rec = store.record(handle)
135
+ native = rec.get("native") or {}
136
+ session_id = native.get("sessionId") if isinstance(native, dict) else None
137
+ if not isinstance(session_id, str) or not session_id:
138
+ raise SessionUnavailable("original Claude native session ID is unavailable", short=owner.short)
139
+ digest = compact_intent_digest(
140
+ owner.worker_id, generation=store.generation,
141
+ native_session_id=session_id, operation_id=operation_id,
142
+ )
143
+
144
+ def indeterminate(reason: str, *, status: str = "unknown") -> CompactIndeterminate:
145
+ return CompactIndeterminate(
146
+ reason, short=owner.short,
147
+ operation_id=operation_id, operation_status=status,
148
+ )
149
+
150
+ # Original operation reconciliation precedes any host revival, claim or IPC.
151
+ try:
152
+ prior = get_operation(handle, operation_id)
153
+ effect = store.read_receipt(handle, "compact", operation_id)
154
+ except (GenerationError, CompactJournalError, OSError) as exc:
155
+ raise indeterminate(f"cannot inspect original compact receipt/journal: {exc}") from exc
156
+ if effect is not None and (
157
+ effect.get("intentSha256") != digest
158
+ or effect.get("workerId") != owner.worker_id
159
+ or effect.get("engine") != "claude"
160
+ or effect.get("generation") != store.generation
161
+ or effect.get("action") != "compact"
162
+ or effect.get("family") != "compact"
163
+ ):
164
+ raise indeterminate("original compact receipt identity does not match")
165
+ if prior is not None and (effect is None or effect.get("state") == "prepared"):
166
+ raise indeterminate("original compact journal lacks a submitted native effect receipt")
167
+ if effect and effect.get("state") == "rejected":
168
+ raise DeliveryRejected(
169
+ "native compact was rejected; request a new operation_id to try again",
170
+ short=owner.short, operation_id=operation_id,
171
+ refusal="compact-rejected",
172
+ )
173
+ submitted = effect is not None and effect.get("state") in ("submitted-unknown", "accepted")
174
+ if submitted and prior is None:
175
+ raise indeterminate("submitted native compact has no original journal; never resubmit")
176
+ if effect is not None and effect.get("state") not in (
177
+ "prepared", "submitted-unknown", "accepted",
178
+ ):
179
+ raise indeterminate("unsupported original compact intent state")
180
+ if not submitted:
181
+ left = None if deadline is None else max(0., deadline - time.monotonic())
182
+ if left is not None and left <= 0:
183
+ raise indeterminate("compact observation budget expired before native admission")
184
+ ensure_ready(
185
+ handle,
186
+ # Revive has a separate bounded original-native timeout; a
187
+ # longer user observation window must remain legal.
188
+ timeout=min(600., left) if left is not None else 180,
189
+ expected_generation=store.generation,
190
+ )
191
+ rec = store.record(handle)
192
+ store.authorize_native_action(handle, "compact")
193
+ attempt = store.begin_native_intent(
194
+ handle, action="compact", family="compact",
195
+ operation_id=operation_id, intent_sha256=digest,
196
+ )
197
+ if attempt.replayed:
198
+ raise indeterminate("original compact receipt already submitted; no replay")
199
+ budget = 2. if deadline is None else min(2., max(.001, deadline - time.monotonic()))
200
+ if deadline is not None and deadline - time.monotonic() <= 0:
201
+ raise indeterminate("compact observation budget expired before native dispatch")
202
+ native_budget = (
203
+ execution_timeout if execution_timeout is not None
204
+ else COMPACT_WAIT_DEFAULT_S
205
+ )
206
+ try:
207
+ reply = store.dispatch_native_intent_once(
208
+ attempt, dispatch_budget_s=budget,
209
+ bounded_submit=lambda seconds: _rpc(
210
+ handle,
211
+ {"op": "compact", "operation_id": operation_id,
212
+ "timeout": native_budget},
213
+ timeout=seconds,
214
+ ),
215
+ )
216
+ except NativeEffectIndeterminate:
217
+ # A lost reply is NOT a proved rejection, and this token must
218
+ # never be sent to the host a second time.
219
+ reply = None
220
+ if isinstance(reply, dict) and reply.get("ok") is True:
221
+ journal_reply = reply.get("operation")
222
+ if (reply.get("workerId") != owner.worker_id
223
+ or reply.get("engine") != "claude"
224
+ or reply.get("generation") != store.generation
225
+ or reply.get("nativeSessionId") != session_id
226
+ or reply.get("operationId") != operation_id
227
+ or not isinstance(journal_reply, dict)
228
+ or journal_reply.get("operation_id") != operation_id
229
+ or journal_reply.get("status") not in (
230
+ "accepted", "pasted", "submitted", "succeeded", "noop", "failed", "unknown",
231
+ )):
232
+ raise indeterminate("original compact host ACK identity or journal differs")
233
+ store.settle_native_intent(
234
+ attempt, state="accepted",
235
+ native_evidence_sha256=_digest({
236
+ "workerId": owner.worker_id, "generation": store.generation,
237
+ "operationId": operation_id, "journal": journal_reply,
238
+ }),
239
+ )
240
+ else:
241
+ # Only an authenticated host refusal (its own state field)
242
+ # PLUS a fresh trusted absent journal proves nothing reached Ink.
243
+ try:
244
+ prior = get_operation(handle, operation_id)
245
+ except (GenerationError, CompactJournalError, OSError) as exc:
246
+ raise indeterminate("compact journal unreadable after native IPC") from exc
247
+ if (isinstance(reply, dict) and reply.get("ok") is False
248
+ and isinstance(reply.get("state"), str)
249
+ and prior is None):
250
+ store.settle_native_intent(
251
+ attempt, state="rejected",
252
+ native_evidence_sha256=_digest({
253
+ "workerId": owner.worker_id, "operationId": operation_id,
254
+ "nativeRefusal": reply.get("error"),
255
+ "nativeState": reply["state"],
256
+ }),
257
+ )
258
+ raise DeliveryRejected(
259
+ "native compact refused: " + str(reply.get("error"))
260
+ + "; use a new operation_id to retry",
261
+ short=owner.short, state=reply["state"],
262
+ operation_id=operation_id,
263
+ )
264
+ if prior is None:
265
+ raise indeterminate("native compact ACK unavailable; submitted token fenced")
266
+ # Journal presence is physical acceptance, *never* terminal success.
267
+ store.revalidate()
268
+ if not wait:
269
+ # The compaction journal proves durable native acceptance even before the
270
+ # provider has written a readable transcript. It is NOT a completion.
271
+ accepted = get_operation(handle, operation_id)
272
+ if accepted is None:
273
+ raise indeterminate("original native compact acceptance journal missing")
274
+ latest = store.record(handle)
275
+ return {
276
+ "short": owner.short, "worker_id": owner.worker_id,
277
+ "state": (latest.get("lifecycle") or {}).get("state"),
278
+ "operation_id": operation_id,
279
+ "operation_status": accepted.get("status") or "unknown",
280
+ "compacted": False, "pre_tokens": None, "post_tokens": None,
281
+ "duration_ms": None, "message": None,
282
+ "evidence_uuid": None, "reason": accepted.get("reason"),
283
+ }
284
+
285
+ def admission_released(row: dict) -> bool:
286
+ # A native compact boundary can precede the host's IDLE transition (or
287
+ # the Stop for a native turn queued behind /compact). The synchronous
288
+ # operation must include that lifecycle settlement, not just the journal.
289
+ if row.get("state") not in {
290
+ "idle", "idle-killed", "crashed", "dead", "budget-stopped",
291
+ }:
292
+ return False
293
+ current = store.record(handle)
294
+ lc = current.get("lifecycle") or {}
295
+ return (
296
+ lc.get("state") in {
297
+ "idle", "idle-killed", "crashed", "dead", "budget-stopped",
298
+ }
299
+ and not lc.get("activeTurnId")
300
+ and not current.get("activeCompact")
301
+ and not current.get("pendingSend")
302
+ and not current.get("waitingFor")
303
+ and not current.get("pendingApprovals")
304
+ )
305
+
306
+ while True:
307
+ row = compact_status(handle, operation_id=operation_id)
308
+ status = row.get("operation_status")
309
+ if status in ("succeeded", "noop") and admission_released(row):
310
+ return row
311
+ if status == "failed":
312
+ raise _compact_failed(
313
+ get_operation(handle, operation_id),
314
+ short=owner.short, state=row.get("state"),
315
+ operation_id=operation_id,
316
+ )
317
+ if status == "unknown":
318
+ raise indeterminate(
319
+ row.get("reason") or "native compact outcome indeterminate",
320
+ status="unknown",
321
+ )
322
+ if deadline is not None and time.monotonic() >= deadline:
323
+ # Final evidence at the deadline wins over a simultaneous timeout.
324
+ last = compact_status(handle, operation_id=operation_id)
325
+ if (last["operation_status"] in ("succeeded", "noop")
326
+ and admission_released(last)):
327
+ return last
328
+ if last["operation_status"] == "failed":
329
+ raise _compact_failed(
330
+ get_operation(handle, operation_id), short=owner.short,
331
+ state=last.get("state"), operation_id=operation_id,
332
+ )
333
+ raise indeterminate(
334
+ "compact outcome or worker input settlement remains pending at observation timeout",
335
+ status=last.get("operation_status") or "unknown",
336
+ )
337
+ time.sleep(COMPACT_POLL_S)