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.
- package/LICENSE +21 -0
- package/README.md +241 -2
- package/bin/ccx +56 -0
- package/ccx/__init__.py +163 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +762 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +250 -0
- package/ccx/claude/host/session.py +5184 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2281 -0
- package/ccx/claude/usage.py +837 -0
- package/ccx/claude/usage_control.py +152 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/read.py +1020 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +301 -0
- package/ccx/cli/parser.py +1324 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1900 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +732 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1556 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2396 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1412 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +289 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1540 -0
- package/ccx/ops/conversation_paging.py +1338 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +579 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +770 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +306 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +636 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +254 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +804 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +705 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2081 -0
- package/ccx.py +9 -0
- package/package.json +40 -4
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +155 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- 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"]
|