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,306 @@
1
+ """Exact managed-turn identity and report reads shared by public adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ccx.codex import records as codex_records
8
+ from ccx.codex.reports import native_timestamp_ms, report_from_turn
9
+ from ccx.errors import InvalidRequest, OperationConflict, SessionNotFound, SessionUnavailable
10
+ from ccx.claude.managed_turns import report_window_for_turn_ref, settlement_for_turn_ref, turn_settlements
11
+ from ccx.store.records import read_record as read_claude_record
12
+ from ccx.claude.transcript import exact_assistant_text
13
+
14
+ _CLAUDE_PREFIX = "ccx-turn:claude:v1:"
15
+ _CODEX_PREFIX = "ccx-turn:codex:v1:"
16
+
17
+
18
+ def _ref_id(turn_ref: str, *, engine: str) -> str:
19
+ prefix = _CLAUDE_PREFIX if engine == "claude" else _CODEX_PREFIX
20
+ if not isinstance(turn_ref, str) or not turn_ref.startswith(prefix):
21
+ raise InvalidRequest(
22
+ f"{engine.capitalize()} turn_ref must use {prefix}<native-turn-id>"
23
+ if engine == "codex"
24
+ else f"Claude turn_ref must use {prefix}<start-id>"
25
+ )
26
+ value = turn_ref[len(prefix):]
27
+ if not value:
28
+ raise InvalidRequest(f"turn_ref must contain an id after {prefix}")
29
+ return value
30
+
31
+
32
+ def codex_settled_turn(
33
+ value: dict[str, Any] | None,
34
+ *,
35
+ fallback_completed_at: int | None = None,
36
+ ) -> dict[str, Any] | None:
37
+ """Normalize one exact native Codex terminal projection."""
38
+ if not isinstance(value, dict):
39
+ return None
40
+ turn_id = value.get("turnId")
41
+ if not isinstance(turn_id, str) or not turn_id:
42
+ return None
43
+ completed_at = value.get("completedAtMs")
44
+ if not isinstance(completed_at, int) or isinstance(completed_at, bool):
45
+ completed_at = fallback_completed_at
46
+ return {
47
+ "turn_ref": f"{_CODEX_PREFIX}{turn_id}",
48
+ "turn_id": turn_id,
49
+ "outcome": str(value.get("status") or "unknown"),
50
+ "completed_at": completed_at,
51
+ "context": ({**value["context"], "as_of_turn_ref": f"{_CODEX_PREFIX}{turn_id}"}
52
+ if isinstance(value.get("context"), dict) else None),
53
+ }
54
+
55
+
56
+ def _claude_settlement(value: Any) -> dict[str, Any] | None:
57
+ """One usable ``{turnRef, outcome, settledAtMs}`` settlement, else None."""
58
+ if not isinstance(value, dict):
59
+ return None
60
+ turn_ref = value.get("turnRef")
61
+ settled_at = value.get("settledAtMs")
62
+ if not isinstance(turn_ref, str) or not turn_ref.startswith(_CLAUDE_PREFIX):
63
+ return None
64
+ if not isinstance(settled_at, int) or isinstance(settled_at, bool):
65
+ return None
66
+ return value
67
+
68
+
69
+ def _latest_claude_settlement(record: dict[str, Any], short: str) -> dict[str, Any] | None:
70
+ """Exact latest managed settlement, or None when it is unknown.
71
+
72
+ The host stores the latest summary in the record (``lastSettledTurn``)
73
+ together with ``lastSettledAt``. Records written before that field fall
74
+ back to the latest retained sidecar terminal. A retained terminal older
75
+ than the stored summary never replaces it, and one older than the record's
76
+ ``lastSettledAt`` is known stale (rotated out, or not yet broadcast), so
77
+ the latest settlement is unknown rather than that older ref.
78
+ """
79
+ stored = _claude_settlement(record.get("lastSettledTurn"))
80
+ values = turn_settlements(short) or []
81
+ retained = _claude_settlement(values[-1]) if values else None
82
+ latest = stored
83
+ if retained is not None and (
84
+ stored is None
85
+ or (
86
+ retained["turnRef"] != stored["turnRef"]
87
+ and retained["settledAtMs"] > stored["settledAtMs"]
88
+ )
89
+ ):
90
+ # Only a strictly newer, different terminal (e.g. broadcast by a host
91
+ # that predates the stored summary) supersedes the record.
92
+ latest = retained
93
+ if latest is None:
94
+ return None
95
+ last_settled_at = record.get("lastSettledAt")
96
+ if (
97
+ isinstance(last_settled_at, int)
98
+ and not isinstance(last_settled_at, bool)
99
+ and latest["settledAtMs"] < last_settled_at
100
+ ):
101
+ return None
102
+ return latest
103
+
104
+
105
+ def last_settled_turn(engine: str, short: str) -> dict[str, Any] | None:
106
+ """Normalized exact latest settled-turn identity for one owned worker.
107
+
108
+ ``None`` means unknown, never "no newer turn": a caller comparing
109
+ checkpoints must treat it as unavailable evidence.
110
+ """
111
+ if engine == "claude":
112
+ record = read_claude_record(short)
113
+ if record is None:
114
+ return None
115
+ value = _latest_claude_settlement(record, short)
116
+ if value is None:
117
+ return None
118
+ return {
119
+ "turn_ref": value["turnRef"],
120
+ "turn_id": None,
121
+ "outcome": str(value.get("outcome") or "unknown"),
122
+ "completed_at": value["settledAtMs"],
123
+ "context": ({**value["context"], "as_of_turn_ref": value["turnRef"]}
124
+ if isinstance(value.get("context"), dict) else None),
125
+ }
126
+
127
+ if engine == "codex":
128
+ record = codex_records.read_record(short)
129
+ if record is None:
130
+ return None
131
+ return codex_settled_turn(
132
+ record.get("lastTurn"),
133
+ fallback_completed_at=record.get("lastTurnAt"),
134
+ )
135
+
136
+ raise InvalidRequest(f"unknown engine: {engine!r}")
137
+
138
+
139
+ def codex_native_turn_evidence(
140
+ child: str, record: dict, thread_id: str, turn_id: str,
141
+ ) -> dict[str, Any] | None:
142
+ """Existing original-owned Codex journal terminal proof; no native read."""
143
+ from ccx.codex.evidence import build_index, read_journal
144
+
145
+ if (not turn_id or (record.get("activeCompact") or {}).get("turnId") == turn_id
146
+ or (record.get("lastCompact") or {}).get("turnId") == turn_id):
147
+ return None
148
+ journal = read_journal(child)
149
+ index = build_index(journal, health=record.get("evidence"))
150
+ turn = index["turns"].get(turn_id)
151
+ if (not index["complete"] or not turn or not turn["sawTurnStarted"]
152
+ or any(item.get("itemType") == "contextCompaction"
153
+ for item in index["items"].get(turn_id, {}).values())):
154
+ return None
155
+ events = [event for event in journal.events if event.get("turnId") == turn_id]
156
+ if not events or any(event.get("threadId") != thread_id for event in events):
157
+ return None
158
+ terminals = [(event.get("params") or {}).get("turn") for event in events
159
+ if event.get("method") == "turn/completed"]
160
+ if not terminals:
161
+ return {"status": None, "error": None}
162
+ if (any(not isinstance(turn, dict) or turn.get("id") != turn_id
163
+ or turn.get("status") not in {"completed", "failed", "interrupted", "cancelled"}
164
+ for turn in terminals)
165
+ or any(turn != terminals[0] for turn in terminals[1:])):
166
+ return None
167
+ return {"status": terminals[0]["status"], "error": terminals[0].get("error")}
168
+
169
+
170
+ def codex_source_report(
171
+ thread: dict, worker_id: str, thread_id: str, turn_id: str,
172
+ *, expected_source: str,
173
+ ) -> dict[str, Any]:
174
+ """Select only the completed original native R from caller-fetched history."""
175
+ if (not isinstance(thread, dict) or thread.get("id") != thread_id
176
+ or thread.get("threadSource") != expected_source):
177
+ raise SessionUnavailable("Codex conversation belongs to another worker; check the worker")
178
+ turns = [turn for turn in (thread.get("turns") or [])
179
+ if isinstance(turn, dict) and turn.get("id") == turn_id]
180
+ if len(turns) != 1 or turns[0].get("status") != "completed":
181
+ raise SessionUnavailable("Completed Codex turn is unavailable; inspect worker status")
182
+ selected = report_from_turn(turns[0])
183
+ return {
184
+ "turn_ref": f"{_CODEX_PREFIX}{turn_id}", "turn_id": turn_id,
185
+ "turn": None, "item_id": selected.get("itemId") if selected else None,
186
+ "phase": selected.get("phase") if selected else None,
187
+ "text": str(selected["text"]) if selected else "",
188
+ "has_final_message": selected is not None,
189
+ "completed_at": (selected.get("completedAt") if selected
190
+ else native_timestamp_ms(turns[0].get("completedAt"))),
191
+ }
192
+
193
+
194
+ def claude_source_report(
195
+ store, child: str, worker_id: str, ref: str,
196
+ window: dict[str, Any],
197
+ ) -> dict[str, Any]:
198
+ """Pinned original-source Claude R window, including accepted EOF recheck.
199
+
200
+ Unlike notification presentation, a verified empty final is "" rather than
201
+ unavailable. All source/window failures are typed instead of coerced to None.
202
+ """
203
+ from ccx.claude.transcript import open_original_claude_source
204
+ from ccx.store.root import StorageIntegrityError
205
+
206
+ owner = store.resolve(child)
207
+ if owner.engine != "claude" or owner.worker_id != worker_id:
208
+ raise SessionUnavailable("Claude exact report owner/source mismatch")
209
+ record = store.record(child)
210
+ start, stop = window.get("startOffset"), window.get("endOffset")
211
+ if (window.get("turnRef") != ref or not window.get("endOffsetKnown")
212
+ or type(start) is not int or start < 0
213
+ or (stop is not None and (type(stop) is not int or stop < start))):
214
+ raise SessionUnavailable("Claude turn history is incomplete; inspect the worker")
215
+ try:
216
+ with open_original_claude_source(store, owner, record) as source:
217
+ if source is None:
218
+ raise SessionUnavailable("Claude history file is missing; check worker status")
219
+ pinned, _stat, eof, _original_path, _directory_fd = source
220
+ if start > eof or (stop is not None and stop > eof):
221
+ raise SessionUnavailable("Claude turn history changed during reading; retry")
222
+ text = exact_assistant_text(
223
+ pinned, after_offset=start,
224
+ before_offset=stop if stop is not None else eof,
225
+ )
226
+ if stop is None:
227
+ # Lazy to avoid turn_reports -> protocol_ops -> turn_reports cycle.
228
+ from ccx.ops.wait import wait_worker
229
+ later, available = report_window_for_turn_ref(worker_id, ref)
230
+ snapshot = wait_worker(
231
+ child, timeout=.3, expected_generation=store.generation,
232
+ )
233
+ if (not available or later != window
234
+ or (snapshot.get("last_settled_turn") or {}).get("turn_ref") != ref
235
+ or snapshot.get("active_turn_ref") not in {None, ref}):
236
+ raise SessionUnavailable(
237
+ "Claude original report EOF could include a successor turn"
238
+ )
239
+ store.revalidate()
240
+ except (StorageIntegrityError, OSError, ValueError) as exc:
241
+ raise SessionUnavailable("Claude conversation is unavailable; inspect worker status") from exc
242
+ return {
243
+ "turn_ref": ref, "turn_id": None, "turn": None,
244
+ "item_id": None, "phase": None, "text": text,
245
+ "has_final_message": bool(text), "completed_at": None,
246
+ }
247
+
248
+
249
+ def exact_report(
250
+ store, owner, turn_ref: str, *, thread: dict | None = None,
251
+ ) -> dict[str, Any]:
252
+ """Exact CCX-owned native R; caller owns Codex history read transport."""
253
+ turn_id = _ref_id(turn_ref, engine=owner.engine)
254
+ child = owner.worker
255
+ record = store.record(child)
256
+ native = record.get("native") or {}
257
+ activation = store.native_activation(child)
258
+ original = native.get("sessionId" if owner.engine == "claude" else "threadId")
259
+ source = (f"claude-session:{original}" if owner.engine == "claude"
260
+ else store.codex_source(owner.worker))
261
+ if (not isinstance(original, str) or not original
262
+ or activation.get("state") != "confirmed"
263
+ or activation.get("source") != source
264
+ or (activation.get("nativeBinding") or {}).get(
265
+ "sessionId" if owner.engine == "claude" else "threadId"
266
+ ) != original
267
+ or (owner.engine == "codex" and native.get("threadSource") != source)):
268
+ raise SessionUnavailable("Conversation source could not be verified; inspect the worker")
269
+ if owner.engine == "claude":
270
+ terminal, available = settlement_for_turn_ref(owner.worker_id, turn_ref)
271
+ window, window_available = report_window_for_turn_ref(owner.worker_id, turn_ref)
272
+ if not available or not window_available:
273
+ raise SessionUnavailable("Claude turn history is unavailable; inspect the worker")
274
+ if terminal is None:
275
+ if window is not None:
276
+ raise OperationConflict("Claude turn is still running; wait for it to finish")
277
+ raise SessionNotFound("Claude turn is no longer saved; inspect recent history")
278
+ if (terminal.get("eventType") != "turn_complete"
279
+ or terminal.get("outcome") != "completed"):
280
+ raise SessionUnavailable("Claude turn is not complete; check worker status")
281
+ if window is None or window.get("turnRef") != turn_ref:
282
+ raise SessionUnavailable("Claude turn history is unavailable; retry later")
283
+ result = claude_source_report(
284
+ store, child, owner.worker_id, turn_ref, window,
285
+ )
286
+ result["turn"] = terminal.get("turn")
287
+ result["completed_at"] = terminal.get("settledAtMs")
288
+ elif owner.engine == "codex":
289
+ proof = codex_native_turn_evidence(child, record, original, turn_id)
290
+ if proof is None:
291
+ raise SessionUnavailable("Codex completion record is missing; check status")
292
+ if proof["status"] is None:
293
+ raise OperationConflict("Codex turn is not complete; wait before reporting")
294
+ if proof["status"] != "completed":
295
+ raise SessionUnavailable("Codex turn ended without completion; inspect worker status")
296
+ if thread is None:
297
+ raise SessionUnavailable("Codex history is unavailable; retry this report")
298
+ result = codex_source_report(
299
+ thread, owner.worker_id, original, turn_id, expected_source=source,
300
+ )
301
+ else:
302
+ raise InvalidRequest(f"unsupported engine {owner.engine!r}")
303
+ store.revalidate()
304
+ return result
305
+
306
+ __all__ = ["codex_settled_turn", "exact_report", "last_settled_turn"]