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,158 @@
1
+ """Reconstruct a native thread from source-attested paged Codex history."""
2
+ from __future__ import annotations
3
+
4
+ import time
5
+ from typing import Any, Callable
6
+
7
+ _PAGE_SIZE = 100
8
+ _MAX_PAGES = 10_000
9
+
10
+
11
+ def _remaining(deadline: float) -> float:
12
+ remaining = deadline - time.monotonic()
13
+ if remaining <= 0:
14
+ raise TimeoutError("Codex history paging timed out")
15
+ return remaining
16
+
17
+
18
+ def _full_turn_items(request: Callable[..., dict], thread_id: str,
19
+ turn_id: str, deadline: float) -> list[dict]:
20
+ items: list[dict] = []
21
+ cursor: str | None = None
22
+ seen: set[str] = set()
23
+ for _ in range(_MAX_PAGES):
24
+ params: dict[str, Any] = {
25
+ "threadId": thread_id, "turnId": turn_id,
26
+ "limit": _PAGE_SIZE, "sortDirection": "asc",
27
+ }
28
+ if cursor is not None:
29
+ params["cursor"] = cursor
30
+ page = request("thread/items/list", params, timeout=_remaining(deadline))
31
+ entries = page.get("data") if isinstance(page, dict) else None
32
+ if not isinstance(entries, list):
33
+ raise ValueError("Codex item-history page is malformed")
34
+ for wrapper in entries:
35
+ if (not isinstance(wrapper, dict) or wrapper.get("turnId") != turn_id
36
+ or not isinstance(wrapper.get("item"), dict)):
37
+ raise ValueError("Codex item-history page belongs to another turn")
38
+ items.append(wrapper["item"])
39
+ next_cursor = page.get("nextCursor")
40
+ if next_cursor is None:
41
+ return items
42
+ if (not isinstance(next_cursor, str) or not next_cursor
43
+ or next_cursor in seen or not entries):
44
+ raise ValueError("Codex item-history cursor did not advance")
45
+ seen.add(next_cursor)
46
+ cursor = next_cursor
47
+ raise ValueError("Codex item history exceeded page budget")
48
+
49
+
50
+ def fill_thread(
51
+ thread: dict, request: Callable[..., dict], *, deadline: float,
52
+ limit: int | None = None, newest_first: bool = False,
53
+ exact_turn_id: str | None = None,
54
+ ) -> dict:
55
+ """Return the thread/read shape, with actual turn statuses and full items."""
56
+ thread_id = thread.get("id") if isinstance(thread, dict) else None
57
+ if not isinstance(thread_id, str) or not thread_id:
58
+ raise ValueError("Codex source metadata has no original thread id")
59
+ if limit is not None and (type(limit) is not int or limit < 1):
60
+ raise ValueError("Codex history turn limit must be positive")
61
+ if exact_turn_id is not None and (not isinstance(exact_turn_id, str)
62
+ or not exact_turn_id):
63
+ raise ValueError("Codex exact turn id is invalid")
64
+ descending = newest_first or exact_turn_id is not None
65
+ turns: list[dict] = []
66
+ seen: set[str] = set()
67
+ seen_turns: set[str] = set()
68
+ cursor: str | None = None
69
+ for _ in range(_MAX_PAGES):
70
+ params: dict[str, Any] = {
71
+ "threadId": thread_id,
72
+ "limit": min(_PAGE_SIZE, limit - len(turns)) if limit else _PAGE_SIZE,
73
+ "sortDirection": "desc" if descending else "asc",
74
+ "itemsView": "notLoaded" if exact_turn_id else "full",
75
+ }
76
+ if cursor is not None:
77
+ params["cursor"] = cursor
78
+ page = request("thread/turns/list", params, timeout=_remaining(deadline))
79
+ data = page.get("data") if isinstance(page, dict) else None
80
+ if not isinstance(data, list):
81
+ raise ValueError("Codex turn-history page is malformed")
82
+ for turn in data:
83
+ if (not isinstance(turn, dict) or not isinstance(turn.get("id"), str)
84
+ or not turn["id"] or not isinstance(turn.get("status"), str)
85
+ or not isinstance(turn.get("items"), list)
86
+ or turn["id"] in seen_turns):
87
+ raise ValueError("Codex native turn-history entry is malformed")
88
+ seen_turns.add(turn["id"])
89
+ if exact_turn_id is not None and turn["id"] != exact_turn_id:
90
+ continue
91
+ result = dict(turn)
92
+ if turn.get("itemsView", "full") != "full":
93
+ result["items"] = _full_turn_items(request, thread_id, turn["id"], deadline)
94
+ result["itemsView"] = "full"
95
+ turns.append(result)
96
+ if exact_turn_id is not None or (limit is not None and len(turns) >= limit):
97
+ return {**thread, "turns": list(reversed(turns)) if descending else turns,
98
+ "historySource": "thread/turns/list"}
99
+ next_cursor = page.get("nextCursor")
100
+ if next_cursor is None:
101
+ return {**thread, "turns": list(reversed(turns)) if descending else turns,
102
+ "historySource": "thread/turns/list"}
103
+ if (not isinstance(next_cursor, str) or not next_cursor
104
+ or next_cursor in seen or not data):
105
+ raise ValueError("Codex turn-history cursor did not advance")
106
+ seen.add(next_cursor)
107
+ cursor = next_cursor
108
+ raise ValueError("Codex turn history exceeded page budget")
109
+
110
+
111
+ def read_original_thread(request: Callable[..., dict], thread_id: str, *,
112
+ deadline: float, limit: int | None = None,
113
+ newest_first: bool = False,
114
+ exact_turn_id: str | None = None,
115
+ expected_source: str | None = None) -> dict:
116
+ """One provider connection: metadata proof followed by bounded page reads."""
117
+ reply = request("thread/read", {"threadId": thread_id, "includeTurns": False},
118
+ timeout=_remaining(deadline))
119
+ thread = reply.get("thread") if isinstance(reply, dict) else None
120
+ if not isinstance(thread, dict) or thread.get("id") != thread_id:
121
+ raise ValueError("Codex source metadata did not return the expected thread")
122
+ if expected_source is not None and thread.get("threadSource") != expected_source:
123
+ raise ValueError("Codex original thread source changed before history paging")
124
+ return fill_thread(thread, request, deadline=deadline, limit=limit,
125
+ newest_first=newest_first, exact_turn_id=exact_turn_id)
126
+
127
+
128
+ def read_fork_source_point(request: Callable[..., dict], thread_id: str, *,
129
+ deadline: float) -> dict:
130
+ """Read only an unmanaged native source's metadata and newest turn.
131
+
132
+ This is NOT worker history authorization. It never starts, resumes,
133
+ unarchives or mutates the native source. Admission is a separate child
134
+ thread/fork with its own independently verified source tag.
135
+ """
136
+ from uuid import UUID
137
+ if (not isinstance(thread_id, str)
138
+ or str(UUID(thread_id)) != thread_id):
139
+ raise ValueError("import needs one exact native Codex thread UUID")
140
+ reply = request(
141
+ "thread/read", {"threadId": thread_id, "includeTurns": False},
142
+ timeout=_remaining(deadline),
143
+ )
144
+ thread = reply.get("thread") if isinstance(reply, dict) else None
145
+ if not isinstance(thread, dict) or thread.get("id") != thread_id:
146
+ raise ValueError("Codex native import source ID is not proven")
147
+ page = request("thread/turns/list", {
148
+ "threadId": thread_id, "limit": 1,
149
+ "sortDirection": "desc", "itemsView": "notLoaded",
150
+ }, timeout=_remaining(deadline))
151
+ items = page.get("data") if isinstance(page, dict) else None
152
+ if not isinstance(items, list) or len(items) > 1:
153
+ raise ValueError("Codex native import source turn page malformed")
154
+ last = items[0] if items else None
155
+ if last is not None and (not isinstance(last, dict)
156
+ or not isinstance(last.get("id"), str)):
157
+ raise ValueError("Codex native import source turn ID unavailable")
158
+ return {"thread": thread, "lastTurn": last}
@@ -0,0 +1,140 @@
1
+ """Targeted Codex parent-notification delivery reconciliation.
2
+
3
+ Notification delivery only needs to answer one question: did a native
4
+ ``worker_notification`` tool output with this delivery id already land?
5
+ Hydrating an entire thread with ``thread/read(includeTurns=true)`` is both
6
+ unnecessary and harmful for long-lived paginated Codex threads. Modern Codex
7
+ exposes ``thread/items/list`` for incremental history access, so CCX pages
8
+ that newest-first and never hydrates the full history for reconciliation.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from typing import Any, Callable
15
+
16
+ ITEM_PAGE_SIZE = 100
17
+ MAX_ITEM_PAGES = 1000
18
+ ITEM_PAGE_TIMEOUT_S = 10.0
19
+
20
+
21
+ def _notification_payload(item: Any, *, name: str = "worker_notification") -> dict[str, Any] | None:
22
+ if not isinstance(item, dict):
23
+ return None
24
+ # ``thread/items/list`` wraps each native item with its containing turn id.
25
+ candidate = item.get("item") if isinstance(item.get("item"), dict) else item
26
+ if not isinstance(candidate, dict):
27
+ return None
28
+ if (
29
+ candidate.get("type") != "functionCallOutput"
30
+ or candidate.get("name") != name
31
+ ):
32
+ return None
33
+ raw = candidate.get("output")
34
+ if isinstance(raw, dict):
35
+ return raw
36
+ if not isinstance(raw, str):
37
+ return None
38
+ try:
39
+ value = json.loads(raw)
40
+ except json.JSONDecodeError:
41
+ return None
42
+ return value if isinstance(value, dict) else None
43
+
44
+
45
+ def _item_delivery_match(
46
+ item: Any,
47
+ delivery_id: str,
48
+ *,
49
+ legacy_event_id: str | None = None,
50
+ name: str = "worker_notification",
51
+ ) -> dict[str, Any] | None:
52
+ value = _notification_payload(item, name=name)
53
+ if value is None:
54
+ return None
55
+ matches = (
56
+ value.get("deliveryId") == delivery_id
57
+ or bool(
58
+ legacy_event_id
59
+ and value.get("deliveryId") is None
60
+ and value.get("eventId") == legacy_event_id
61
+ )
62
+ )
63
+ if not matches:
64
+ return None
65
+ turn_id = item.get("turnId") if isinstance(item, dict) else None
66
+ return {
67
+ "deliveryId": delivery_id,
68
+ "eventId": value.get("eventId"),
69
+ "turnId": turn_id if isinstance(turn_id, str) and turn_id else None,
70
+ }
71
+
72
+
73
+ def _paged_delivery_match(
74
+ request: Callable[..., dict[str, Any]],
75
+ thread_id: str,
76
+ delivery_id: str,
77
+ *,
78
+ legacy_event_id: str | None = None,
79
+ stop_at_turn_id: str | None = None,
80
+ name: str = "worker_notification",
81
+ ) -> dict[str, Any] | None:
82
+ cursor: str | None = None
83
+ seen_cursors: set[str] = set()
84
+ for _ in range(MAX_ITEM_PAGES):
85
+ params: dict[str, Any] = {
86
+ "threadId": thread_id,
87
+ "limit": ITEM_PAGE_SIZE,
88
+ "sortDirection": "desc",
89
+ }
90
+ if cursor is not None:
91
+ params["cursor"] = cursor
92
+ result = request("thread/items/list", params, timeout=ITEM_PAGE_TIMEOUT_S)
93
+ data = result.get("data") if isinstance(result, dict) else None
94
+ if not isinstance(data, list):
95
+ raise RuntimeError("thread/items/list returned malformed data")
96
+ for entry in data:
97
+ match = _item_delivery_match(
98
+ entry, delivery_id, legacy_event_id=legacy_event_id, name=name
99
+ )
100
+ if match is not None:
101
+ return match
102
+ if (
103
+ stop_at_turn_id
104
+ and isinstance(entry, dict)
105
+ and entry.get("turnId") == stop_at_turn_id
106
+ ):
107
+ # Newest-first reached a turn that had already completed
108
+ # before the first attempt; nothing older can hold it.
109
+ return None
110
+ next_cursor = result.get("nextCursor")
111
+ if not isinstance(next_cursor, str) or not next_cursor:
112
+ return None
113
+ if next_cursor in seen_cursors:
114
+ raise RuntimeError("thread/items/list repeated a pagination cursor")
115
+ seen_cursors.add(next_cursor)
116
+ cursor = next_cursor
117
+ raise RuntimeError(
118
+ f"thread/items/list exceeded {MAX_ITEM_PAGES} pages while reconciling notification"
119
+ )
120
+
121
+
122
+ def notification_delivery_match(
123
+ request: Callable[..., dict[str, Any]],
124
+ thread_id: str,
125
+ delivery_id: str,
126
+ *,
127
+ legacy_event_id: str | None = None,
128
+ stop_at_turn_id: str | None = None,
129
+ name: str = "worker_notification",
130
+ ) -> dict[str, Any] | None:
131
+ """Find a landed notification, newest first.
132
+
133
+ ``stop_at_turn_id`` names a turn that had completed before the delivery's
134
+ first attempt: the paged walk ends there instead of at the thread's start.
135
+ """
136
+ return _paged_delivery_match(
137
+ request, thread_id, delivery_id,
138
+ legacy_event_id=legacy_event_id, stop_at_turn_id=stop_at_turn_id,
139
+ name=name,
140
+ )