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,232 @@
1
+ """Recover a Codex delivery left ``submitted`` from exact native evidence.
2
+
3
+ Only the V2 caller writes ``accepted``, after codexd's answer. When that
4
+ answer is lost (caller budget expired, native request timed out, caller
5
+ killed), the receipt would stay ``submitted`` although the input landed. The
6
+ native ``userMessage`` item carries the ``clientId`` CCX minted for exactly
7
+ this delivery, and codexd journals it before reducing it. That item, and
8
+ nothing else, is positive evidence of placement.
9
+
10
+ One resolver, two callers: codexd, when it journals a matching user message
11
+ (off its app-server reader thread), and ``reconcile_turn``, for recovery after
12
+ a restart. Acceptance goes through the receipt's own ``mark_accepted`` and the
13
+ global native intent's ``settle_native_intent`` with the very evidence digest
14
+ codexd's acknowledgment carries, so a late acknowledgment and this resolver
15
+ converge on one immutable answer in either order.
16
+
17
+ Missing, ambiguous, foreign or degraded evidence changes nothing: the receipt
18
+ stays ``submitted`` (unknown). Absence never proves rejection, and nothing
19
+ here ever sends anything native.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import contextlib
24
+ from typing import Any
25
+
26
+ from ccx.codex import delivery as codex_delivery
27
+ from ccx.codex import evidence as codex_evidence
28
+ from ccx.codex.provenance import parse_client_message_id
29
+
30
+ USER_MESSAGE = "userMessage"
31
+ _ITEM_METHODS = frozenset({"item/started", "item/completed"})
32
+
33
+
34
+ def ccx_client_id(item: Any, worker_id: str) -> str | None:
35
+ """The CCX-minted v2 client id of this worker's native user message, else None."""
36
+ if not isinstance(item, dict) or item.get("type") != USER_MESSAGE:
37
+ return None
38
+ client_id = item.get("clientId")
39
+ parsed = parse_client_message_id(client_id)
40
+ if not parsed or parsed.get("workerId") != worker_id:
41
+ return None
42
+ return client_id
43
+
44
+
45
+ def _placement(read: codex_evidence.JournalRead, client_id: str,
46
+ thread_id: str) -> str | None:
47
+ """The one native turn the exact client id was placed in, or None.
48
+
49
+ Every journaled item carrying the id must be the same user-message item
50
+ of the same turn on the bound thread; anything else is ambiguous.
51
+ """
52
+ seen: set[tuple[Any, Any]] = set()
53
+ for event in read.events:
54
+ if event.get("method") not in _ITEM_METHODS:
55
+ continue
56
+ params = event.get("params")
57
+ item = params.get("item") if isinstance(params, dict) else None
58
+ if not isinstance(item, dict) or item.get("clientId") != client_id:
59
+ continue
60
+ if (item.get("type") != USER_MESSAGE
61
+ or event.get("threadId") != thread_id
62
+ or not isinstance(event.get("turnId"), str)
63
+ or not event["turnId"]):
64
+ return None
65
+ seen.add((event["turnId"], event.get("itemId")))
66
+ if len(seen) != 1:
67
+ return None
68
+ (turn_id, item_id), = seen
69
+ return turn_id if isinstance(item_id, str) and item_id else None
70
+
71
+
72
+ def recover(store, handle: str, delivery_id: str, *,
73
+ journal: codex_evidence.JournalRead | None = None) -> dict | None:
74
+ """Accept a ``submitted`` receipt only from exact journaled placement.
75
+
76
+ Also completes an ``accepted`` receipt whose global native intent was left
77
+ ``submitted-unknown`` (a caller that died between the two writes), from
78
+ the same evidence. Returns the current receipt (None if there is none).
79
+ Receipt conflicts raise; a busy native-intent lock leaves the intent for
80
+ a later call and still returns the accepted receipt.
81
+ """
82
+ owner = store.resolve(handle)
83
+ if owner.engine != "codex":
84
+ return None
85
+ # Pin the original generation across every read and bounded local write:
86
+ # a cooperating cutover needs LOCK_EX on the activation lock this shared
87
+ # lease holds, and the receipt helpers resolve the CURRENT pointer, so it
88
+ # must be this store's. No native call happens under it.
89
+ with _pinned(store):
90
+ return _recover_pinned(store, owner, delivery_id, journal)
91
+
92
+
93
+ @contextlib.contextmanager
94
+ def _pinned(store):
95
+ from ccx import constants
96
+ from ccx.store.root import StaleGeneration, require_ccx_active
97
+ with store.lease():
98
+ current = require_ccx_active(constants.CCX_ROOT,
99
+ expected_generation=store.generation)
100
+ if current.root != store.root:
101
+ raise StaleGeneration("recovery store is not the active root")
102
+ yield
103
+
104
+
105
+ def _recover_pinned(store, owner, delivery_id: str,
106
+ journal: codex_evidence.JournalRead | None) -> dict | None:
107
+ from ccx.store.root import GenerationBusy
108
+ from ccx.store.store import OperationIntent, _digest
109
+ receipt = codex_delivery.read_receipt(owner.worker, delivery_id)
110
+ if not isinstance(receipt, dict) or receipt.get("state") not in ("submitted", "accepted"):
111
+ return receipt
112
+ effect = store.read_receipt(owner.worker, "codex-send", delivery_id)
113
+ if (not isinstance(effect, dict)
114
+ or effect.get("action") != "send"
115
+ or effect.get("workerId") != owner.worker_id
116
+ or effect.get("engine") != "codex"
117
+ or effect.get("generation") != store.generation
118
+ or effect.get("intentSha256") != _digest(codex_delivery.intent_spec(receipt))
119
+ or effect.get("state") not in ("submitted-unknown", "accepted")):
120
+ return receipt
121
+ if receipt["state"] == "accepted" and effect["state"] == "accepted":
122
+ return receipt # Both terminal: nothing to write, nothing asserted.
123
+
124
+ route = codex_delivery.route(receipt)
125
+ client_id = receipt.get("clientUserMessageId")
126
+ target = receipt.get("targetTurnId")
127
+ parsed = parse_client_message_id(client_id)
128
+ if (route not in codex_delivery.ROUTES
129
+ or not parsed or parsed.get("workerId") != owner.worker_id
130
+ or parsed.get("kind") != ("steer" if route == "steer" else "start")
131
+ or (route == "steer" and (not isinstance(target, str) or not target))
132
+ or (route == "new_turn" and target is not None)):
133
+ return receipt
134
+
135
+ record = store.record(owner.worker)
136
+ native = record.get("native") or {}
137
+ thread_id = native.get("threadId")
138
+ activation = store.native_activation(owner.worker)
139
+ # The confirmed activation's source, not codex_source(): that one also
140
+ # demands send capability, and recovery is a read of past evidence that
141
+ # must work for a worker stopped since.
142
+ source = activation.get("source")
143
+ binding = activation.get("nativeBinding") or {}
144
+ if (not isinstance(thread_id, str) or not thread_id
145
+ or not isinstance(source, str) or not source
146
+ or native.get("threadSource") != source
147
+ or activation.get("state") != "confirmed"
148
+ or not isinstance(binding, dict)
149
+ or binding.get("threadId") != thread_id):
150
+ return receipt
151
+
152
+ read = journal if journal is not None else codex_evidence.read_journal(owner.worker_id)
153
+ health = record.get("evidence")
154
+ status, _warnings = codex_evidence._merged_health(
155
+ read, health if isinstance(health, dict) else None)
156
+ if status != "complete":
157
+ return receipt # Degraded evidence never decides.
158
+ turn_id = _placement(read, client_id, thread_id)
159
+ if turn_id is None:
160
+ return receipt
161
+ if route == "steer" and turn_id != target:
162
+ return receipt
163
+ if receipt["state"] == "accepted" and receipt.get("turnId") != turn_id:
164
+ return receipt
165
+ if route == "new_turn":
166
+ try:
167
+ opener = codex_delivery.receipt_for_turn(owner.worker, turn_id)
168
+ except codex_delivery.DeliveryReceiptError:
169
+ return receipt # Damaged receipts never let evidence pick a side.
170
+ if (isinstance(opener, dict) and opener.get("deliveryId") != delivery_id
171
+ and codex_delivery.route(opener) != "steer"):
172
+ return receipt
173
+
174
+ # Exactly the digest codexd's native-send acknowledgment carries.
175
+ evidence = _digest({
176
+ "source": source, "threadId": thread_id, "turnId": turn_id,
177
+ "deliveryId": delivery_id, "route": route, "targetTurnId": target,
178
+ "clientUserMessageId": client_id,
179
+ })
180
+ if (effect["state"] == "accepted"
181
+ and effect.get("nativeEvidenceSha256") != evidence):
182
+ # An immutable terminal global proof of something else (or of
183
+ # nothing valid): never fabricate a consistent answer over it.
184
+ return receipt
185
+ store.revalidate()
186
+ accepted = codex_delivery.mark_accepted(owner.worker, delivery_id, turn_id=turn_id)
187
+ if effect["state"] == "submitted-unknown":
188
+ try:
189
+ store.settle_native_intent(OperationIntent(
190
+ owner.worker_id, "codex", "codex-send", delivery_id,
191
+ effect["intentSha256"], "submitted-unknown", store.generation,
192
+ ), state="accepted", native_evidence_sha256=evidence)
193
+ except GenerationBusy:
194
+ # The dispatching caller still holds this intent; it settles it
195
+ # with the same digest, or a later call does.
196
+ return accepted
197
+ if route == "new_turn" and accepted.get("settlement") is None:
198
+ from ccx.codex.completion import settle_from_journal
199
+ try:
200
+ settled = settle_from_journal(store, owner.worker, turn_id)
201
+ if settled is not None:
202
+ accepted = settled
203
+ except Exception:
204
+ pass # Accepted placement stands; settlement has its own recovery.
205
+ return codex_delivery.read_receipt(owner.worker, delivery_id)
206
+
207
+
208
+ def recover_for_turn(store, handle: str, turn_id: str) -> None:
209
+ """Before a turn is called foreign: recover a submitted delivery placed in it."""
210
+ owner = store.resolve(handle)
211
+ if owner.engine != "codex":
212
+ return
213
+ with _pinned(store):
214
+ pending = [value for value in codex_delivery.iter_receipts(owner.worker)
215
+ if value.get("state") == "submitted"
216
+ and codex_delivery.route(value) == "new_turn"]
217
+ if not pending:
218
+ return
219
+ read = codex_evidence.read_journal(owner.worker_id)
220
+ for value in pending:
221
+ _recover_pinned(store, owner, value["deliveryId"], read)
222
+
223
+
224
+ def delivery_for_client(handle: str, client_id: str) -> str | None:
225
+ """The delivery that minted this exact client id (submitted or accepted)."""
226
+ matches = [value.get("deliveryId") for value in codex_delivery.iter_receipts(handle)
227
+ if value.get("clientUserMessageId") == client_id
228
+ and value.get("state") in ("submitted", "accepted")]
229
+ return matches[0] if len(matches) == 1 else None
230
+
231
+
232
+ __all__ = ["ccx_client_id", "delivery_for_client", "recover", "recover_for_turn"]