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
File without changes
@@ -0,0 +1,115 @@
1
+ """A small engine-neutral recorded snapshot of background work.
2
+
3
+ Producers own these facts; consumers only read the saved summary/event.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ import time
9
+
10
+ KINDS = ("command", "monitor", "subagent", "notification")
11
+ OPEN = {"running", "pending"}
12
+ LABEL_LIMIT = 120
13
+ EVENT_LIMIT = 1000
14
+ ITEM_LIMIT = 100
15
+
16
+
17
+ def label(value: object, fallback: str) -> str:
18
+ text = re.sub(r"\s+", " ", str(value)) if isinstance(value, str) else ""
19
+ text = "".join(ch for ch in text if ch.isprintable()).strip()
20
+ return text[:LABEL_LIMIT] or fallback
21
+
22
+
23
+ def bounded_event(value: object) -> str | None:
24
+ """Bound one sanitized Monitor notification; labels remain much shorter."""
25
+ if not isinstance(value, str):
26
+ return None
27
+ return re.sub(r"\s+", " ", value).strip()[:EVENT_LIMIT]
28
+
29
+
30
+
31
+ def summary(items: list[dict], *, as_of_ms: int | None = None,
32
+ available: bool = True) -> dict:
33
+ if as_of_ms is None:
34
+ as_of_ms = int(time.time() * 1000)
35
+ if not available:
36
+ return {
37
+ "running_count": None, "pending_count": None, "by_kind": None,
38
+ "items": [], "items_truncated": 0,
39
+ "as_of_ms": as_of_ms, "available": False,
40
+ }
41
+ active = [dict(item) for item in items
42
+ if item.get("status") in OPEN and item.get("kind") in KINDS]
43
+ by_kind = {kind: sum(item["kind"] == kind for item in active) for kind in KINDS}
44
+ # Sort once, before applying the display bound. Unknown start times are
45
+ # after known starts, then every item is ordered by stable ID.
46
+ active.sort(key=lambda item: (
47
+ item.get("started_at_ms") if type(item.get("started_at_ms")) is int
48
+ else float("inf"), item["id"]))
49
+ # Keep snapshots small and deterministic; avoid leaking native content.
50
+ preview = [{key: (bounded_event(item[key]) if key == "event" else item[key])
51
+ for key in ("id", "kind", "status", "label", "event", "event_count",
52
+ "expires_at_ms", "notification_id", "delivery_id")
53
+ if key in item} for item in active[:ITEM_LIMIT]]
54
+ return {
55
+ "running_count": sum(by_kind[kind] for kind in KINDS if kind != "notification"),
56
+ "pending_count": by_kind["notification"],
57
+ "by_kind": by_kind, "items": preview,
58
+ "items_truncated": max(0, len(active) - len(preview)),
59
+ "as_of_ms": as_of_ms, "available": True,
60
+ }
61
+
62
+
63
+ def materially_changed(previous: dict | None, current: dict) -> bool:
64
+ if previous is None:
65
+ return True
66
+ return {key: value for key, value in previous.items() if key != "as_of_ms"} != {
67
+ key: value for key, value in current.items() if key != "as_of_ms"}
68
+
69
+
70
+ def claude_task_item(task: dict) -> dict | None:
71
+ task_id = task.get("task_id")
72
+ if not isinstance(task_id, str) or not task_id:
73
+ return None # launching tool-result has not yet proved native task identity
74
+ native_kind = task.get("kind")
75
+ kind = "command" if native_kind == "bash" else native_kind
76
+ if kind not in ("command", "monitor", "subagent"):
77
+ return None
78
+ item = {
79
+ "id": f"{kind}:{task_id}", "kind": kind,
80
+ "status": task.get("status") or "unknown",
81
+ "label": label(task.get("description"), f"background {kind}"),
82
+ "tool_use_id": task.get("tool_use_id"),
83
+ "started_at_ms": task.get("launched_at"),
84
+ }
85
+ if kind == "monitor":
86
+ launched, timeout = task.get("launched_at"), task.get("timeout_ms")
87
+ item["expires_at_ms"] = (launched + timeout if type(launched) is int
88
+ and type(timeout) is int and timeout > 0 else None)
89
+ item["event_count"] = task.get("event_count") or 0
90
+ if task.get("last_event"):
91
+ item["event"] = bounded_event(task["last_event"])
92
+ if task.get("exit_code") is not None:
93
+ item["exit_code"] = task["exit_code"]
94
+ return item
95
+
96
+
97
+ def diff_items(before: list[dict], after: list[dict]) -> list[tuple[str, dict]]:
98
+ """One source-row transition, never a second invented native observation."""
99
+ old = {item["id"]: item for item in before}
100
+ out = []
101
+ for item in after:
102
+ prior = old.get(item["id"])
103
+ if item == prior:
104
+ continue
105
+ status = item.get("status")
106
+ if prior is None or (prior.get("status") not in OPEN and status in OPEN):
107
+ transition = "reopened" if prior is not None else "started"
108
+ elif status not in OPEN and (prior is None or prior.get("status") in OPEN):
109
+ transition = {"completed": "finished", "failed": "failed",
110
+ "stopped": "stopped"}.get(status, "event")
111
+ else:
112
+ transition = "event"
113
+ out.append((transition, item))
114
+ return out
115
+
@@ -0,0 +1,289 @@
1
+ from __future__ import annotations
2
+ import time
3
+ from typing import Any
4
+ from ccx.errors import (
5
+ DeliveryRejected,
6
+ InvalidRequest,
7
+ OperationConflict,
8
+ SessionUnavailable,
9
+ )
10
+ from ccx.ops.common import split_target
11
+ from ccx.ops.wait import reconcile_turn
12
+
13
+
14
+ def _cancel_live_turn(handle: str, engine: str, worker_id: str,
15
+ *, timeout: float = 0.5) -> tuple[bool, str | None]:
16
+ """Authenticated exact active turn, or UNKNOWN (never inferred from absence)."""
17
+ if engine == "claude":
18
+ from ccx.rpc.rpc import _rpc
19
+ reply = _rpc(handle, {"op": "status"}, timeout=timeout)
20
+ if reply.get("ok") is True and "activeTurnRef" in reply:
21
+ return True, reply["activeTurnRef"]
22
+ return False, None
23
+ from ccx.codex.runtime import daemon_rpc
24
+ from ccx.constants import CODEXD_RPC_GENERATION
25
+ reply = daemon_rpc({"op": "status", "short": worker_id,
26
+ "rpcGeneration": CODEXD_RPC_GENERATION}, timeout=timeout)
27
+ if (reply.get("ok") is True and reply.get("loaded") is True
28
+ and "activeTurnId" in reply):
29
+ return True, ("ccx-turn:codex:v1:" + reply["activeTurnId"]
30
+ if isinstance(reply["activeTurnId"], str) else None)
31
+ return False, None
32
+
33
+
34
+ def cancel_turn(
35
+ target: str, *, cancellation_id: str, turn_ref: str,
36
+ timeout: float | None = 90,
37
+ expected_generation: str | None = None,
38
+ ) -> dict[str, Any]:
39
+ """Request one exact full-UUID turn interrupt, never replay its native write.
40
+
41
+ protocol cancellation_id is caller-owned. The existing GenerationStore 'cancel'
42
+ intent is the only ledger, independent of legacy short-keyed turn_cancel.
43
+ A native ACK means requested, not that model activity stopped.
44
+ """
45
+ from math import isfinite
46
+ from ccx import constants
47
+ from ccx.store.root import NativeEffectIndeterminate, require_ccx_active
48
+ from ccx.store.store import _digest
49
+ from ccx.store.identity import canonical_worker_id
50
+ from ccx.codex.delivery import receipt_for_turn as codex_receipt_for_turn
51
+
52
+ if (not isinstance(cancellation_id, str) or not cancellation_id
53
+ or len(cancellation_id) > 128 or "\x00" in cancellation_id):
54
+ raise InvalidRequest("cancellation_id must be nonempty and <=128 characters")
55
+ if (timeout is not None and (type(timeout) not in (int, float)
56
+ or not isfinite(timeout) or timeout <= 0)):
57
+ raise InvalidRequest("cancel timeout must be positive finite seconds")
58
+ handle, engine, worker_id = split_target(target)
59
+ canonical_worker_id(worker_id)
60
+ prefix = f"ccx-turn:{engine}:v1:"
61
+ if not isinstance(turn_ref, str) or not turn_ref.startswith(prefix) or not turn_ref[len(prefix):]:
62
+ raise InvalidRequest("turn_ref must name an exact managed turn for the selected engine")
63
+ native_turn_id = turn_ref[len(prefix):] if engine == "codex" else None
64
+ store = require_ccx_active(constants.CCX_ROOT,
65
+ expected_generation=expected_generation)
66
+ rec = store.record(handle)
67
+ source = rec.get("native") or {}
68
+ if (engine == "claude" and not isinstance(source.get("sessionId"), str)) or (
69
+ engine == "codex" and (not isinstance(source.get("threadId"), str)
70
+ or source.get("threadSource") != store.codex_source(handle))
71
+ ):
72
+ raise SessionUnavailable("original native worker binding is not verified")
73
+ identity = _digest({"workerId": worker_id, "engine": engine,
74
+ "turnRef": turn_ref, "operation": "cancel"})
75
+ prior = store.read_receipt(handle, "cancel", cancellation_id)
76
+ if prior is not None and prior.get("intentSha256") != identity:
77
+ raise OperationConflict("cancellation_id already names a different turn",
78
+ short=worker_id[:8], worker_id=worker_id)
79
+ # In particular, an invented native id is never a cancel capability.
80
+ if engine == "codex":
81
+ opening = codex_receipt_for_turn(handle, native_turn_id)
82
+ else:
83
+ from ccx.claude.delivery import iter_receipts
84
+ opening = next((row for row in iter_receipts(handle)
85
+ if row.get("turnRef") == turn_ref
86
+ and row.get("route") == "new_turn"), None)
87
+ if prior is None and (not isinstance(opening, dict)
88
+ or opening.get("state") != "accepted"):
89
+ # Human input on the shared managed Claude PTY starts a genuine parent
90
+ # turn without a CCX delivery receipt. Do not fabricate one: the live
91
+ # authenticated host's exact activeTurnRef is sufficient to request a
92
+ # one-shot native Escape, under the same durable cancel intent. This
93
+ # exception never applies to Codex or an absent/invented/stale turn.
94
+ if engine != "claude":
95
+ raise DeliveryRejected("exact active turn has no original accepted delivery receipt",
96
+ short=worker_id[:8])
97
+ known, active = _cancel_live_turn(handle, engine, worker_id)
98
+ store.revalidate()
99
+ if not known or active != turn_ref:
100
+ raise DeliveryRejected("exact active turn has no original accepted delivery receipt",
101
+ short=worker_id[:8])
102
+ deadline = None if timeout is None else time.monotonic() + timeout
103
+
104
+ def public(state: str, *, outcome=None, reason=None, owned=False,
105
+ receipt=None) -> dict[str, Any]:
106
+ return {
107
+ "worker": {"worker": handle, "worker_id": worker_id,
108
+ "engine": engine, "short": worker_id[:8]},
109
+ "cancellation_id": cancellation_id, "turn_ref": turn_ref,
110
+ "status": state, "confirmed": state == "confirmed",
111
+ "owned": owned, "accepted": bool(receipt and receipt.get("state") == "accepted"),
112
+ "recommended_action": ("retry_same_id" if state == "prepared"
113
+ else "wait" if state == "submitted" else "none"),
114
+ "outcome": outcome, "reason": reason,
115
+ "receipt": receipt,
116
+ }
117
+
118
+ def observe(receipt: dict) -> dict | None:
119
+ # Original semantic receipt is the only proof of completed assistant
120
+ # text. A changed live ref proves INACTIVITY, not why the turn stopped.
121
+ def exact_claude_terminal() -> dict | None:
122
+ last = store.record(handle).get("lastSettledTurn") or {}
123
+ if (last.get("turnRef") == turn_ref
124
+ and last.get("outcome") in {"interrupted", "completed", "failed"}):
125
+ return public("confirmed", outcome=last["outcome"],
126
+ owned=True, receipt=receipt)
127
+ return None
128
+
129
+ if engine == "codex":
130
+ seen = reconcile_turn(handle, turn_id=native_turn_id,
131
+ expected_generation=store.generation)
132
+ if seen.get("status") == "settled":
133
+ return public("confirmed", outcome="completed", owned=True,
134
+ receipt=receipt)
135
+ if seen.get("native_status") in ("interrupted", "cancelled"):
136
+ return public("confirmed", outcome="interrupted", owned=True,
137
+ receipt=receipt)
138
+ else:
139
+ from ccx.claude.delivery import read_receipt
140
+ latest = read_receipt(handle, opening["deliveryId"]) if opening else None
141
+ if (latest and latest.get("turnRef") == turn_ref
142
+ and isinstance(latest.get("settlement"), dict)):
143
+ return public("confirmed", outcome="completed", owned=True,
144
+ receipt=receipt)
145
+ # Positive completed-answer receipt wins above. Otherwise the
146
+ # exact original host finalizer proves this turn's outcome for
147
+ # both human and receipt-backed cancellations; it does not invent
148
+ # an opening delivery receipt or completed assistant text.
149
+ terminal = exact_claude_terminal()
150
+ if terminal is not None:
151
+ return terminal
152
+ known, active = _cancel_live_turn(handle, engine, worker_id)
153
+ store.revalidate()
154
+ if known and active != turn_ref:
155
+ if engine == "claude":
156
+ # The terminal CAS can land between our first record read and
157
+ # the host status RPC. Reread the same exact source before
158
+ # falling back to unknown; never report an invented outcome.
159
+ terminal = exact_claude_terminal()
160
+ if terminal is not None:
161
+ return terminal
162
+ return public("confirmed", outcome="unknown", owned=True,
163
+ receipt=receipt)
164
+ return None
165
+
166
+ # No preflight can replace the provider's expected-turn check, but an
167
+ # observed IDLE/other turn can reject without any native effect at all.
168
+ if prior is None or prior.get("state") == "prepared":
169
+ store.authorize_native_action(handle, "cancel")
170
+ known, active = _cancel_live_turn(handle, engine, worker_id)
171
+ if not known:
172
+ return public("prepared", reason="live active turn not proven")
173
+ if active != turn_ref:
174
+ return public("rejected", reason="active turn changed or no active turn")
175
+ attempt = store.begin_native_intent(
176
+ handle, family="cancel", action="cancel", operation_id=cancellation_id,
177
+ intent_sha256=identity,
178
+ )
179
+ if attempt.replayed:
180
+ prior = store.read_receipt(handle, "cancel", cancellation_id)
181
+ else:
182
+ from ccx.constants import CODEXD_RPC_GENERATION
183
+ from ccx.codex.runtime import daemon_rpc
184
+ from ccx.rpc.rpc import _rpc
185
+ budget = min(2., max(.01, deadline - time.monotonic())) if deadline else 2.
186
+ def submit(seconds):
187
+ if engine == "claude":
188
+ return _rpc(handle, {"op": "interrupt",
189
+ "expected_turn_ref": turn_ref}, timeout=seconds)
190
+ return daemon_rpc({"op": "interrupt", "short": worker_id,
191
+ "expectedTurnId": native_turn_id,
192
+ "rpcGeneration": CODEXD_RPC_GENERATION}, timeout=seconds)
193
+ try:
194
+ reply = store.dispatch_native_intent_once(
195
+ attempt, bounded_submit=submit, dispatch_budget_s=budget)
196
+ except NativeEffectIndeterminate:
197
+ # Durable submitted-unknown; a same-ID caller only observes.
198
+ prior = store.read_receipt(handle, "cancel", cancellation_id)
199
+ return public("submitted", reason="original interrupt reply unknown",
200
+ owned=True, receipt=prior)
201
+ if (reply.get("ok") is True and
202
+ (engine == "codex" and reply.get("turnId") == native_turn_id
203
+ or engine == "claude" and reply.get("turnRef") == turn_ref
204
+ and (reply.get("sent") is True or
205
+ (reply.get("sent") is False and
206
+ reply.get("already_observing") is True)))):
207
+ # A prior same-turn Escape is accepted, never proof of completion.
208
+ state = "accepted"
209
+ elif reply.get("ok") is False and reply.get("definitive") is True:
210
+ state = "rejected"
211
+ else:
212
+ prior = store.read_receipt(handle, "cancel", cancellation_id)
213
+ return public("submitted", reason="native interrupt not positively acknowledged",
214
+ owned=True, receipt=prior)
215
+ prior = store.settle_native_intent(
216
+ attempt, state=state, native_evidence_sha256=_digest({
217
+ "workerId": worker_id, "turnRef": turn_ref, "reply": reply,
218
+ }),
219
+ )
220
+ if state == "rejected":
221
+ return public("rejected", reason=str(reply.get("error")), receipt=prior)
222
+ if prior is None:
223
+ prior = store.read_receipt(handle, "cancel", cancellation_id)
224
+ if prior.get("state") == "rejected":
225
+ return public("rejected", reason="original native interrupt refused", receipt=prior)
226
+ # Acceptance and caller timeout do not mutate the durable native effect:
227
+ # a later same-ID call may still observe terminality, but cannot re-dispatch.
228
+ while True:
229
+ store.revalidate()
230
+ try:
231
+ verified = observe(prior)
232
+ except (SessionUnavailable, OSError):
233
+ store.revalidate() # changed ACTIVE is not a temporary daemon loss
234
+ verified = None # temporary runtime loss is NOT changed-turn proof
235
+ if verified is not None:
236
+ return verified
237
+ if deadline is not None and time.monotonic() >= deadline:
238
+ return public("unknown", reason="exact target terminality not proven",
239
+ owned=True, receipt=prior)
240
+ time.sleep(.2 if deadline is None else
241
+ min(.2, max(0., deadline-time.monotonic())))
242
+
243
+
244
+ def stop_worker(
245
+ target: str, *, timeout: float | None = 1.0,
246
+ expected_turn_ref: str | None = None,
247
+ expected_generation: str | None = None,
248
+ ) -> dict[str, Any]:
249
+ """One explicit stop request, fresh ID, exact ref; acceptance is not completion."""
250
+ from uuid import uuid4
251
+ from ccx import constants
252
+ from ccx.errors import TurnIncomplete
253
+ from ccx.store.root import require_ccx_active
254
+
255
+ handle, engine, worker_id = split_target(target)
256
+ store = require_ccx_active(constants.CCX_ROOT,
257
+ expected_generation=expected_generation)
258
+ if expected_turn_ref is None:
259
+ known, active = _cancel_live_turn(handle, engine, worker_id)
260
+ store.revalidate()
261
+ if not known:
262
+ raise SessionUnavailable("stop: active turn could not be observed",
263
+ short=worker_id[:8])
264
+ if not active:
265
+ raise DeliveryRejected("stop: worker has no active turn",
266
+ short=worker_id[:8])
267
+ else:
268
+ active = expected_turn_ref # cancel_turn validates the exact ref.
269
+ result = cancel_turn(handle, cancellation_id=str(uuid4()),
270
+ turn_ref=active, timeout=timeout,
271
+ expected_generation=store.generation)
272
+ if result["status"] == "rejected":
273
+ raise DeliveryRejected(result.get("reason") or "active turn changed",
274
+ short=worker_id[:8],
275
+ operation_id=result["cancellation_id"])
276
+ if result["status"] == "prepared":
277
+ raise SessionUnavailable("stop: original interrupt not submitted",
278
+ short=worker_id[:8])
279
+ if not result.get("accepted"):
280
+ error = TurnIncomplete(
281
+ "stop: native interrupt acceptance unconfirmed; "
282
+ "reconcile the original cancellation ID, do not repeat it",
283
+ short=worker_id[:8], turn_status=result["status"],
284
+ )
285
+ error.cancellation_id = result["cancellation_id"]
286
+ error.turn_ref = result["turn_ref"]
287
+ error.worker_id = worker_id
288
+ raise error
289
+ return result
@@ -0,0 +1,96 @@
1
+ """Fork a user-owned native Codex thread into a separate ordinary CCX worker."""
2
+ from __future__ import annotations
3
+
4
+ import time
5
+ from uuid import UUID
6
+
7
+ from ccx.errors import InvalidRequest, SessionUnavailable
8
+ from ccx.ops.creation_steps import validated_create_spec
9
+ from ccx.ops.creation import create
10
+ from ccx.store.identity import mint_worker_id
11
+ from ccx.constants import DEFAULT_IDLE_TIMEOUT_S
12
+
13
+
14
+ def import_thread(
15
+ thread_id: str, *, source: dict | None = None,
16
+ model: str | None = None, effort: str | None = None,
17
+ ) -> dict:
18
+ """Only the child is native-mutated; the original may remain open in Codex.
19
+
20
+ Importing a second time intentionally forks a second independent child.
21
+ """
22
+ if not isinstance(thread_id, str):
23
+ raise InvalidRequest("Codex import needs an exact native thread UUID")
24
+ try:
25
+ if str(UUID(thread_id)) != thread_id:
26
+ raise ValueError("noncanonical")
27
+ except (ValueError, TypeError, AttributeError) as exc:
28
+ raise InvalidRequest("Codex import needs an exact native thread UUID") from exc
29
+ if any(value is not None and (not isinstance(value, str) or not value.strip())
30
+ for value in (model, effort)):
31
+ raise InvalidRequest("import model/effort overrides must be nonempty strings")
32
+ if source is None:
33
+ from ccx.codex.runtime import read_import_source
34
+ source = read_import_source(thread_id)
35
+ thread = source.get("thread") if isinstance(source, dict) else None
36
+ last = source.get("lastTurn") if isinstance(source, dict) else None
37
+ if not isinstance(thread, dict) or thread.get("id") != thread_id:
38
+ raise SessionUnavailable("Codex import native thread ID was not confirmed")
39
+ status = thread.get("status")
40
+ status = status.get("type") if isinstance(status, dict) else status
41
+ if status not in (None, "idle", "notLoaded"):
42
+ raise SessionUnavailable("Codex source has an active turn; wait for it to finish")
43
+ if (not isinstance(last, dict) or not isinstance(last.get("id"), str)
44
+ or not last["id"] or last.get("status") != "completed"):
45
+ raise SessionUnavailable(
46
+ "Codex import needs a latest completed native turn; no worker created"
47
+ )
48
+ cwd = thread.get("cwd")
49
+ if not isinstance(cwd, str) or not cwd:
50
+ raise SessionUnavailable("Codex source has no recorded workspace")
51
+ # Native metadata may omit the latest model context. Never infer it from
52
+ # prose/rollout files; leave omitted settings at the normal CCX defaults.
53
+ native_model = last.get("model") or thread.get("model")
54
+ native_effort = (last.get("reasoningEffort") or last.get("effort")
55
+ or thread.get("reasoningEffort"))
56
+ chosen_model = model if model is not None else (
57
+ native_model if isinstance(native_model, str) and native_model.strip() else None
58
+ )
59
+ chosen_effort = effort if effort is not None else (
60
+ native_effort if isinstance(native_effort, str) and native_effort.strip() else None
61
+ )
62
+ directory, args = validated_create_spec(
63
+ engine="codex", cwd=cwd, model=chosen_model, effort=chosen_effort,
64
+ idle_timeout=DEFAULT_IDLE_TIMEOUT_S, permissions="full",
65
+ sandbox=None, approval_policy=None, agent=None,
66
+ append_system_prompt=None, allowed_tools=None, disallowed_tools=None,
67
+ add_dir=None, permission_mode=None,
68
+ )
69
+ from ccx.codex.runtime import validate_model
70
+ if model is not None:
71
+ validate_model(model, cwd=directory)
72
+ name = thread.get("name") or thread.get("title")
73
+ if not isinstance(name, str) or not name.strip():
74
+ name = None
75
+ completed_at = last.get("completedAt")
76
+ last_at = (int(completed_at * 1000)
77
+ if type(completed_at) in (float, int) and completed_at > 0 else None)
78
+ worker_id = mint_worker_id()
79
+ anchor = {
80
+ "engine": "codex", "sourceKind": "codex-thread",
81
+ "sourceWorkerId": None, "sourceNativeThreadId": thread_id,
82
+ "lastTurnId": last["id"], "cwd": directory,
83
+ "createArgs": args,
84
+ # The newest page cannot attest the historical total; zero here
85
+ # counts managed submissions, NOT all source turns.
86
+ "seed": {"stats": {"turns": 0}, "lastTurnAt": last_at,
87
+ "lastTurn": {"turnId": last["id"], "status": "completed"}},
88
+ "overrides": {"model": model, "effort": effort},
89
+ "forkedAtMs": int(time.time() * 1000),
90
+ }
91
+ return create(
92
+ engine="codex", cwd=directory, worker_id=worker_id, name=name,
93
+ model=chosen_model, effort=chosen_effort,
94
+ permissions="full", idle_timeout=DEFAULT_IDLE_TIMEOUT_S,
95
+ created_via="import", _fork_anchor=anchor, start_timeout=90,
96
+ )