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,121 @@
1
+ """Exact, proof-backed settlement of Codex's original accepted turn receipts.
2
+
3
+ The live notification supplies the completed turn without a history read. A
4
+ fast completion can precede the caller's accepted-receipt write; its durable
5
+ evidence journal is then the same notification source for a later catch-up.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from ccx.codex import delivery as codex_delivery
13
+ from ccx.codex import evidence as codex_evidence
14
+ from ccx.codex.reports import report_from_turn, select_report_for_turn
15
+
16
+
17
+ def settle_completed(
18
+ store: Any, handle: str, *, thread_id: str, turn: dict[str, Any],
19
+ live_report: dict[str, Any] | None = None,
20
+ ) -> dict[str, Any] | None:
21
+ """Settle only the exact admitted new-turn delivery and original source."""
22
+ from ccx.store.store import _digest
23
+
24
+ turn_id = turn.get("id") if isinstance(turn, dict) else None
25
+ if (not isinstance(turn_id, str) or not turn_id
26
+ or turn.get("status") != "completed"):
27
+ return None
28
+ owner = store.resolve(handle)
29
+ if owner.engine != "codex":
30
+ return None
31
+ receipt = codex_delivery.receipt_for_turn(owner.worker, turn_id)
32
+ if (not isinstance(receipt, dict)
33
+ or receipt.get("state") != "accepted"
34
+ or codex_delivery.route(receipt) != "new_turn"):
35
+ return None
36
+ if isinstance(receipt.get("settlement"), dict):
37
+ return receipt
38
+ source = store.codex_source(owner.worker)
39
+ native = (store.record(owner.worker).get("native") or {})
40
+ activation = store.native_activation(owner.worker)
41
+ binding = activation.get("nativeBinding") or {}
42
+ effect = store.read_receipt(owner.worker, "codex-send", receipt["deliveryId"])
43
+ if (not isinstance(native, dict)
44
+ or native.get("threadId") != thread_id
45
+ or native.get("threadSource") != source
46
+ or activation.get("state") != "confirmed"
47
+ or activation.get("source") != source
48
+ or not isinstance(binding, dict)
49
+ or binding.get("threadId") != thread_id
50
+ or not isinstance(effect, dict)
51
+ or effect.get("state") != "accepted"
52
+ or effect.get("workerId") != owner.worker_id
53
+ or effect.get("engine") != "codex"
54
+ or effect.get("generation") != store.generation
55
+ or effect.get("action") != "send"
56
+ or effect.get("intentSha256") != _digest(
57
+ codex_delivery.intent_spec(receipt))):
58
+ return None
59
+
60
+ selected = select_report_for_turn(turn, live_report=live_report)
61
+ text = selected["text"] if selected is not None else ""
62
+ proof = {
63
+ "ok": True, "status": "completed", "workerId": owner.worker_id,
64
+ "engine": "codex", "nativeSource": source, "threadId": thread_id,
65
+ "turnId": turn_id, "deliveryId": receipt["deliveryId"],
66
+ "text": text, "nativeEvidenceSha256": _digest({
67
+ "source": source, "threadId": thread_id, "turnId": turn_id,
68
+ "status": "completed", "text": text,
69
+ }),
70
+ }
71
+ store.revalidate()
72
+ return codex_delivery.mark_settled(
73
+ owner.worker, receipt["deliveryId"], turn_id=turn_id,
74
+ original_positive_completion=proof,
75
+ )
76
+
77
+
78
+ def settle_from_journal(store: Any, handle: str, turn_id: str) -> dict[str, Any] | None:
79
+ """Catch a pre-acceptance completion from original sparse native events.
80
+
81
+ This is not a substitute for native observation when events were missed.
82
+ In that case reconciliation falls through to a source-fenced thread read.
83
+ """
84
+ owner = store.resolve(handle)
85
+ native = store.record(owner.worker).get("native") or {}
86
+ thread_id = native.get("threadId")
87
+ if not isinstance(thread_id, str) or not thread_id:
88
+ return None
89
+ journal = codex_evidence.read_journal(owner.worker_id)
90
+ if not journal.complete:
91
+ return None
92
+ items: list[dict[str, Any]] = []
93
+ started = False
94
+ for event in journal.events:
95
+ if event.get("turnId") != turn_id:
96
+ continue
97
+ if event.get("threadId") != thread_id:
98
+ return None
99
+ params = event.get("params") or {}
100
+ if event.get("method") == "turn/started":
101
+ started_turn = params.get("turn")
102
+ started = (isinstance(started_turn, dict)
103
+ and started_turn.get("id") == turn_id)
104
+ elif event.get("method") == "item/completed":
105
+ item = params.get("item")
106
+ if isinstance(item, dict):
107
+ items.append(item)
108
+ elif event.get("method") == "turn/completed":
109
+ turn = params.get("turn")
110
+ if (not started or not isinstance(turn, dict)
111
+ or turn.get("id") != turn_id):
112
+ return None
113
+ # Keep native turn.items intact, but also honor a stronger
114
+ # final_answer published in the live item/completed stream. The
115
+ # same report_from_turn rule applies to both observation surfaces.
116
+ stream_report = report_from_turn({"id": turn_id, "items": items})
117
+ return settle_completed(
118
+ store, owner.worker, thread_id=thread_id, turn=turn,
119
+ live_report=stream_report,
120
+ )
121
+ return None
@@ -0,0 +1,288 @@
1
+ "Small dependency-free client for Codex's local app-server control socket.\n\nNormal CCX-owned Codex workers use :mod:`ccx.codex.daemon`; this transport is only for\nwaking a *parent* Codex thread owned by the user's primary Codex runtime/UI.\n"
2
+
3
+ from __future__ import annotations
4
+
5
+ from ccx.version import ccx_version
6
+
7
+ import base64
8
+ import hashlib
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+ import queue
13
+ import select
14
+ import socket
15
+ import struct
16
+ import threading
17
+ from typing import Any, Callable
18
+
19
+ _GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
20
+
21
+
22
+ class CodexControlError(RuntimeError):
23
+ def __init__(self, method: str, error: Any):
24
+ self.method = method
25
+ self.error = error
26
+ self.code = error.get("code") if isinstance(error, dict) else None
27
+ self.data = error.get("data") if isinstance(error, dict) else None
28
+ message = error.get("message") if isinstance(error, dict) else str(error)
29
+ self.message = str(message or error)
30
+ super().__init__(f"{method} failed: {self.message}")
31
+
32
+
33
+ def _masked_frame(opcode: int, payload: bytes = b"") -> bytes:
34
+ """Wire bytes for a client-to-server WebSocket frame (always masked)."""
35
+ mask = os.urandom(4)
36
+ size = len(payload)
37
+ if size < 126:
38
+ header = bytes([0x80 | opcode, 0x80 | size])
39
+ elif size < 65536:
40
+ header = bytes([0x80 | opcode, 0x80 | 126]) + struct.pack("!H", size)
41
+ else:
42
+ header = bytes([0x80 | opcode, 0x80 | 127]) + struct.pack("!Q", size)
43
+ return header + mask + bytes(value ^ mask[i % 4] for i, value in enumerate(payload))
44
+
45
+
46
+ class _UnixWebSocket:
47
+ def __init__(self, path: str, *, handshake_timeout: float | None = None):
48
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
49
+ if handshake_timeout is not None:
50
+ self.sock.settimeout(handshake_timeout)
51
+ self.sock.connect(path)
52
+ self._write_lock = threading.Lock()
53
+ key = base64.b64encode(os.urandom(16)).decode("ascii")
54
+ request = (
55
+ "GET /rpc HTTP/1.1\r\n"
56
+ "Host: localhost\r\n"
57
+ "Upgrade: websocket\r\n"
58
+ "Connection: Upgrade\r\n"
59
+ f"Sec-WebSocket-Key: {key}\r\n"
60
+ "Sec-WebSocket-Version: 13\r\n\r\n"
61
+ ).encode("ascii")
62
+ self.sock.sendall(request)
63
+ head = b""
64
+ while b"\r\n\r\n" not in head:
65
+ chunk = self.sock.recv(4096)
66
+ if not chunk:
67
+ raise ConnectionError("Codex control socket closed during WebSocket handshake")
68
+ head += chunk
69
+ if len(head) > 65536:
70
+ raise ConnectionError("oversized Codex control WebSocket handshake")
71
+ header, self._buffer = head.split(b"\r\n\r\n", 1)
72
+ lines = header.decode("latin1").split("\r\n")
73
+ if not lines or " 101 " not in (" " + lines[0] + " "):
74
+ raise ConnectionError(
75
+ f"Codex control WebSocket upgrade failed: {lines[0] if lines else header!r}"
76
+ )
77
+ headers: dict[str, str] = {}
78
+ for line in lines[1:]:
79
+ if ":" in line:
80
+ k, v = line.split(":", 1)
81
+ headers[k.strip().lower()] = v.strip()
82
+ expected = base64.b64encode(
83
+ hashlib.sha1((key + _GUID).encode("ascii")).digest()
84
+ ).decode("ascii")
85
+ if headers.get("sec-websocket-accept") != expected:
86
+ raise ConnectionError("Codex control WebSocket accept key mismatch")
87
+ if handshake_timeout is not None:
88
+ self.sock.settimeout(None)
89
+
90
+ def _read_exact(self, size: int) -> bytes:
91
+ out = b""
92
+ if self._buffer:
93
+ take = min(size, len(self._buffer))
94
+ out, self._buffer = self._buffer[:take], self._buffer[take:]
95
+ while len(out) < size:
96
+ try:
97
+ chunk = self.sock.recv(size - len(out))
98
+ except BlockingIOError:
99
+ # Socket-mode codexd writes are bounded/nonblocking; the
100
+ # single WS reader tolerates that flag without busy-spinning.
101
+ select.select([self.sock], [], [], 0.1)
102
+ continue
103
+ if not chunk:
104
+ raise EOFError("Codex control WebSocket closed")
105
+ out += chunk
106
+ return out
107
+
108
+ def _send_frame(self, opcode: int, payload: bytes = b"") -> None:
109
+ with self._write_lock:
110
+ self.sock.sendall(_masked_frame(opcode, payload))
111
+
112
+ def send_text(self, text: str) -> None:
113
+ self._send_frame(0x1, text.encode("utf-8"))
114
+
115
+ def recv_text(self, *, on_ping: Callable[[bytes], None] | None = None) -> str:
116
+ pieces: list[bytes] = []
117
+ started = False
118
+ while True:
119
+ b1, b2 = self._read_exact(2)
120
+ fin = bool(b1 & 0x80)
121
+ opcode = b1 & 0x0F
122
+ masked = bool(b2 & 0x80)
123
+ size = b2 & 0x7F
124
+ if size == 126:
125
+ size = struct.unpack("!H", self._read_exact(2))[0]
126
+ elif size == 127:
127
+ size = struct.unpack("!Q", self._read_exact(8))[0]
128
+ mask = self._read_exact(4) if masked else None
129
+ payload = self._read_exact(size)
130
+ if mask:
131
+ payload = bytes(value ^ mask[i % 4] for i, value in enumerate(payload))
132
+ if opcode == 0x8:
133
+ raise EOFError("Codex control WebSocket close frame")
134
+ if opcode == 0x9:
135
+ if on_ping is not None:
136
+ on_ping(payload)
137
+ else:
138
+ self._send_frame(0xA, payload)
139
+ continue
140
+ if opcode == 0xA:
141
+ continue
142
+ if opcode == 0x1:
143
+ pieces = [payload]
144
+ started = True
145
+ elif opcode == 0x0 and started:
146
+ pieces.append(payload)
147
+ else:
148
+ continue
149
+ if fin:
150
+ return b"".join(pieces).decode("utf-8")
151
+
152
+ def close(self) -> None:
153
+ try:
154
+ self._send_frame(0x8)
155
+ except Exception:
156
+ pass
157
+ try:
158
+ self.sock.shutdown(socket.SHUT_RDWR)
159
+ except Exception:
160
+ pass
161
+ try:
162
+ self.sock.close()
163
+ except Exception:
164
+ pass
165
+
166
+
167
+ def control_socket_path() -> Path:
168
+ # Like Codex itself, an empty CODEX_HOME means unset (not the current directory).
169
+ codex_home = Path(os.environ.get("CODEX_HOME") or "~/.codex").expanduser()
170
+ return codex_home / "app-server-control" / "app-server-control.sock"
171
+
172
+
173
+ class CodexControlClient:
174
+ def __init__(
175
+ self,
176
+ *,
177
+ on_notification: Callable[[str, dict[str, Any]], None] | None = None,
178
+ socket_path: str | None = None,
179
+ handshake_timeout: float | None = None,
180
+ ):
181
+ self.ws = _UnixWebSocket(socket_path or str(control_socket_path()),
182
+ handshake_timeout=handshake_timeout)
183
+ self.on_notification = on_notification or (lambda method, params: None)
184
+ self._next_id = 1
185
+ self._pending: dict[int, queue.Queue] = {}
186
+ self._lock = threading.Lock()
187
+ self._closed = threading.Event()
188
+ self._failure: BaseException | None = None
189
+ self._reader = threading.Thread(
190
+ target=self._read_loop, name="ccx-codex-parent-control", daemon=True
191
+ )
192
+ self._reader.start()
193
+
194
+ def _send(self, value: dict[str, Any]) -> None:
195
+ if self._failure:
196
+ raise RuntimeError(f"Codex control connection failed: {self._failure!r}")
197
+ self.ws.send_text(json.dumps(value, separators=(",", ":")))
198
+
199
+ def request(
200
+ self, method: str, params: dict[str, Any] | None = None, *, timeout: float = 30
201
+ ) -> dict[str, Any]:
202
+ with self._lock:
203
+ if self._failure:
204
+ raise RuntimeError(f"Codex control connection failed: {self._failure!r}")
205
+ rid = self._next_id
206
+ self._next_id += 1
207
+ result_queue: queue.Queue = queue.Queue(maxsize=1)
208
+ self._pending[rid] = result_queue
209
+ message: dict[str, Any] = {"id": rid, "method": method}
210
+ if params is not None:
211
+ message["params"] = params
212
+ self._send(message)
213
+ try:
214
+ response = result_queue.get(timeout=timeout)
215
+ except queue.Empty as error:
216
+ with self._lock:
217
+ self._pending.pop(rid, None)
218
+ raise TimeoutError(f"{method} timed out") from error
219
+ if isinstance(response, BaseException):
220
+ raise response
221
+ if "error" in response:
222
+ raise CodexControlError(method, response["error"])
223
+ result = response.get("result")
224
+ return result if isinstance(result, dict) else {"value": result}
225
+
226
+ def notify(self, method: str, params: dict[str, Any] | None = None) -> None:
227
+ message: dict[str, Any] = {"method": method}
228
+ if params is not None:
229
+ message["params"] = params
230
+ self._send(message)
231
+
232
+ def initialize(self, *, timeout: float = 20) -> None:
233
+ self.request(
234
+ "initialize",
235
+ {
236
+ "clientInfo": {
237
+ "name": "ccx-parent-notify",
238
+ "title": "CCX parent notification",
239
+ "version": ccx_version(),
240
+ },
241
+ "capabilities": {"experimentalApi": False},
242
+ },
243
+ timeout=timeout,
244
+ )
245
+ self.notify("initialized")
246
+
247
+ def _read_loop(self) -> None:
248
+ try:
249
+ while not self._closed.is_set():
250
+ message = json.loads(self.ws.recv_text())
251
+ rid = message.get("id")
252
+ if rid is not None and ("result" in message or "error" in message):
253
+ with self._lock:
254
+ result_queue = self._pending.pop(rid, None)
255
+ if result_queue:
256
+ result_queue.put(message)
257
+ continue
258
+ method = message.get("method")
259
+ params = message.get("params")
260
+ if not isinstance(params, dict):
261
+ params = {}
262
+ if rid is not None:
263
+ self._send(
264
+ {
265
+ "id": rid,
266
+ "error": {
267
+ "code": -32601,
268
+ "message": f"CCX parent notification does not handle {method}",
269
+ },
270
+ }
271
+ )
272
+ elif method:
273
+ self.on_notification(method, params)
274
+ except BaseException as error:
275
+ if not self._closed.is_set():
276
+ self._failure = error
277
+ with self._lock:
278
+ pending = list(self._pending.values())
279
+ self._pending.clear()
280
+ for result_queue in pending:
281
+ try:
282
+ result_queue.put_nowait(error)
283
+ except queue.Full:
284
+ pass
285
+
286
+ def close(self) -> None:
287
+ self._closed.set()
288
+ self.ws.close()
File without changes
@@ -0,0 +1,4 @@
1
+ from ccx.codex.daemon.daemon import main
2
+
3
+ if __name__ == "__main__":
4
+ main()