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,649 @@
1
+ from __future__ import annotations
2
+ import fcntl
3
+ import hashlib
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import time
8
+ import uuid
9
+ from typing import Any, Callable
10
+ from ccx.errors import InvalidRequest, SessionUnavailable
11
+ from ccx.notify.paths import (
12
+ ensure_dirs,
13
+ registration_path,
14
+ registrations_dir,
15
+ )
16
+
17
+
18
+ NOTIFY_REPORT_MAX_CHARS = int(os.environ.get("CCX_NOTIFY_REPORT_MAX_CHARS", "12000"))
19
+
20
+
21
+ TERMINAL_REGISTRATION_STATES = {"delivered", "cancelled", "undeliverable"}
22
+
23
+
24
+ def now_ms() -> int:
25
+ return int(time.time() * 1000)
26
+
27
+
28
+ def _delivery_id(event_id: str) -> str:
29
+ return f"{event_id}:settled"
30
+
31
+
32
+ def _current_worker_name(worker: dict[str, Any]) -> str | None:
33
+ """Prefer current durable display metadata over registration-time snapshots."""
34
+ engine = worker.get("engine")
35
+ short = worker.get("workerId") or worker.get("short")
36
+ if not isinstance(short, str) or not short:
37
+ return worker.get("name")
38
+ try:
39
+ if engine == "codex":
40
+ from ccx.codex.records import read_record as read_codex_record
41
+ record = read_codex_record(short)
42
+ elif engine == "claude":
43
+ from ccx.store.records import read_record as read_claude_record
44
+ record = read_claude_record(short)
45
+ else:
46
+ record = None
47
+ except Exception:
48
+ record = None
49
+ current = record.get("name") if isinstance(record, dict) else None
50
+ return current if isinstance(current, str) and current else worker.get("name")
51
+
52
+
53
+ def _atomic_write(path: Path, value: dict[str, Any]) -> None:
54
+ path.parent.mkdir(parents=True, exist_ok=True)
55
+ tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
56
+ with open(tmp, "w", encoding="utf-8") as handle:
57
+ json.dump(value, handle, indent=2)
58
+ handle.write("\n")
59
+ handle.flush()
60
+ os.fsync(handle.fileno())
61
+ os.replace(tmp, path)
62
+ # The event ID is not durably armed until the rename's directory entry
63
+ # also survives a crash. This is the existing registry, not another log.
64
+ directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
65
+ try:
66
+ os.fsync(directory)
67
+ finally:
68
+ os.close(directory)
69
+
70
+
71
+ def _lock_path(event_id: str) -> Path:
72
+ return registration_path(event_id).with_suffix(".lock")
73
+
74
+
75
+ def read_registration(event_id: str) -> dict[str, Any] | None:
76
+ try:
77
+ value = json.loads(registration_path(event_id).read_text(encoding="utf-8"))
78
+ except (OSError, ValueError):
79
+ return None
80
+ return value if isinstance(value, dict) else None
81
+
82
+
83
+ def _hint_managed_parent_background(reg: dict[str, Any]) -> None:
84
+ """Best-effort projection hint to an already-running managed codexd.
85
+
86
+ The registry write precedes this hint. No native wake, daemon startup,
87
+ journal append from notifyd, or retry ledger is involved.
88
+ """
89
+ parent = reg.get("parent") or {}
90
+ if parent.get("kind") != "ccx-codex":
91
+ return
92
+ worker_id = parent.get("workerId")
93
+ if not isinstance(worker_id, str) or not worker_id:
94
+ return
95
+ try:
96
+ from ccx.codex.runtime import daemon_rpc
97
+ from ccx.constants import CODEXD_RPC_GENERATION
98
+ daemon_rpc({
99
+ "op": "background-refresh",
100
+ "short": worker_id,
101
+ "rpcGeneration": CODEXD_RPC_GENERATION,
102
+ }, timeout=0.25)
103
+ except Exception:
104
+ pass # Notifications do not depend on observer availability.
105
+
106
+
107
+ def mutate_registration(
108
+ event_id: str, mutate: Callable[[dict[str, Any]], None]
109
+ ) -> dict[str, Any] | None:
110
+ from ccx.notify.select import _original_notification_registration
111
+ ensure_dirs()
112
+ with open(_lock_path(event_id), "a+") as lock:
113
+ fcntl.flock(lock, fcntl.LOCK_EX)
114
+ value = read_registration(event_id)
115
+ if value is None:
116
+ return None
117
+ _original_notification_registration(value)
118
+ previous = value.get("state")
119
+ mutate(value)
120
+ value["updatedAtMs"] = now_ms()
121
+ _atomic_write(registration_path(event_id), value)
122
+ # Never call codexd while the registration file is locked.
123
+ if value.get("state") != previous and value.get("state") in {
124
+ "cancelled", "undeliverable", "delivered",
125
+ }:
126
+ _hint_managed_parent_background(value)
127
+ return value
128
+
129
+
130
+ def _settlement_child_thread(reg: dict[str, Any], store) -> str | None:
131
+ worker = reg.get("worker") or {}
132
+ if worker.get("engine") != "codex" or worker.get("generation") != store.generation:
133
+ return None
134
+ owner = store.resolve(f"codex:{worker['workerId']}")
135
+ native = (store.record(owner.worker).get("native") or {})
136
+ thread = native.get("threadId")
137
+ return (thread if isinstance(thread, str) and thread
138
+ and native.get("threadSource") == store.codex_source(owner.worker)
139
+ else None)
140
+
141
+
142
+ def _assert_no_settlement_cycle(parent: dict[str, Any], child_id: str, store) -> None:
143
+ """One original managed parent→child graph under its publication lock."""
144
+ child = _settlement_child_thread(
145
+ {"worker": {"engine": "codex", "workerId": child_id,
146
+ "generation": store.generation}}, store)
147
+ if child is None:
148
+ raise InvalidRequest("Child Codex worker cannot be verified; inspect its status")
149
+ if parent["kind"] == "codex-native-thread":
150
+ return # A captured interactive return address is no managed graph node.
151
+ parent_thread = parent["threadId"]
152
+ if child == parent_thread:
153
+ raise InvalidRequest("recursive settlement cannot wait on the parent thread itself")
154
+ edges: dict[str, set[str]] = {}
155
+ for reg in iter_registrations(strict=True):
156
+ if reg.get("settlementMode") != "orchestration":
157
+ continue
158
+ edge_parent = reg.get("parent") or {}
159
+ if (edge_parent.get("kind") != "ccx-codex"
160
+ or edge_parent.get("generation") != store.generation):
161
+ continue
162
+ edge_child = _settlement_child_thread(reg, store)
163
+ if edge_child:
164
+ edges.setdefault(edge_parent["threadId"], set()).add(edge_child)
165
+ stack = [child]
166
+ visited = set()
167
+ while stack:
168
+ thread = stack.pop()
169
+ if thread == parent_thread:
170
+ raise InvalidRequest("recursive settlement would create a notification cycle")
171
+ if thread not in visited:
172
+ visited.add(thread)
173
+ stack.extend(edges.get(thread, ()))
174
+
175
+
176
+ def create_registration(
177
+ *, parent: dict[str, Any], worker_engine: str, worker_id: str,
178
+ worker_name: str | None, operation: dict[str, Any],
179
+ source: str, settle: bool = False,
180
+ registration_key: str | None = None,
181
+ ) -> dict[str, Any]:
182
+ """Stage one strict v3 send receipt or independent read-only notify arm."""
183
+ from ccx.notify.select import _active_compact_selection, _compact_effect_receipt, _controller_attempt_at_arm, _original_notification_registration
184
+ from ccx.notify.daemon import _retire_incompatible_notifyd, ensure_notify_daemon
185
+ from ccx import constants
186
+ from ccx.store.root import require_ccx_active
187
+ from ccx.claude.delivery import valid_delivery_id
188
+
189
+ hashed_input = (source == "input" and worker_engine == "claude"
190
+ and operation.get("kind") in {"claude-input-answer", "claude-input-respond"}
191
+ if isinstance(operation, dict) else False)
192
+ if (settle and worker_engine != "codex"
193
+ or worker_engine not in {"codex", "claude", "controller"}
194
+ or not isinstance(operation, dict)
195
+ or source not in ({"controller.run", "controller.resume", "controller.notify"}
196
+ if worker_engine == "controller" else {"send", "notify", "new", "input", "compact"})
197
+ or (source == "input" and (
198
+ set(operation) != ({"kind", "gateId", "responseId",
199
+ "nativeSourceId", "intentSha256", "selectedTurnRef"}
200
+ | ({"payloadSha256"} if hashed_input
201
+ else ({"decision", "reason"} if worker_engine == "claude"
202
+ else {"decision", "turnId"})))
203
+ or (not hashed_input and (
204
+ operation.get("kind") != f"{worker_engine}-input-decision"
205
+ or operation.get("decision") not in {"approve", "reject"}))))
206
+ or (source == "send" and operation.get("kind") != f"{worker_engine}-turn")
207
+ or (source == "compact" and operation.get("kind") != f"{worker_engine}-compact")
208
+ or (source == "new" and (worker_engine not in {"claude", "codex"}
209
+ or type(operation.get("expectTurn")) is not bool
210
+ or operation != {"kind": f"{worker_engine}-stable", "expectTurn": False,
211
+ "selectionMode": "status-only", "stableState": "idle"}))
212
+ or (source == "notify" and operation.get("kind") not in {
213
+ f"{worker_engine}-observed-turn", f"{worker_engine}-stable",
214
+ f"{worker_engine}-compact",
215
+ *({"claude-next-turn"} if worker_engine == "claude" else set()),
216
+ })):
217
+ raise InvalidRequest("This notification type is unsupported; choose a valid source")
218
+ if (source == "notify" and operation.get("kind") == f"{worker_engine}-compact"):
219
+ if (set(operation) != {"kind", "selectionMode", "operationId", "nativeSourceId"}
220
+ or operation.get("selectionMode") != "active-compact"):
221
+ raise InvalidRequest("standalone compact needs one original active operation")
222
+ if source == "compact":
223
+ token = operation.get("operationId")
224
+ native_id = operation.get("nativeSourceId")
225
+ if (set(operation) != {"kind", "operationId", "nativeSourceId"}
226
+ or not isinstance(token, str) or not 0 < len(token) <= 128
227
+ or "\x00" in token or not isinstance(native_id, str) or not native_id):
228
+ raise InvalidRequest("compact notification needs an exact native operation and source")
229
+ delivery_id = operation.get("deliveryId")
230
+ if (source == "notify" and operation.get("kind") == "claude-next-turn"
231
+ and operation.get("selectedTurnRef") is not None):
232
+ raise InvalidRequest("future turn must arm before its first R is selected")
233
+ if source == "send" and not valid_delivery_id(delivery_id):
234
+ raise InvalidRequest("notify_parent requires a valid delivery_id")
235
+ store = require_ccx_active(constants.CCX_ROOT)
236
+ # No caller-provided registration key can create an alternate event for the
237
+ # same semantic operation. It also excludes parent intentionally: a foreign
238
+ # parent retry must hit the same event and fail its identity comparison.
239
+ from ccx.store.identity import canonical_worker_id
240
+ wid = canonical_worker_id(worker_id)
241
+ key = (f"full-uuid-{worker_engine}-send:{store.generation}:{wid}:{delivery_id}"
242
+ if source == "send" else
243
+ f"full-uuid-{worker_engine}-new:{store.generation}:{wid}:promptless"
244
+ if source == "new" else
245
+ f"full-uuid-{worker_engine}-input:{store.generation}:{wid}:{operation['gateId']}"
246
+ if source == "input" else
247
+ f"full-uuid-{worker_engine}-compact:{store.generation}:{wid}:{operation['operationId']}"
248
+ if source == "compact" else None)
249
+ if registration_key is not None and registration_key != key:
250
+ raise InvalidRequest("Notification ID does not match its delivery; inspect the request")
251
+ if not isinstance(parent, dict):
252
+ raise InvalidRequest("notification needs a captured parent return address")
253
+ if parent.get("kind") == "ccx-codex":
254
+ parent_id = canonical_worker_id(parent.get("workerId"))
255
+ if parent.get("generation") != store.generation:
256
+ raise InvalidRequest("notification parent original generation changed")
257
+ parent_value = {
258
+ "kind": "ccx-codex", "workerId": parent_id,
259
+ "threadId": parent.get("threadId"), "generation": store.generation,
260
+ }
261
+ elif parent.get("kind") == "codex-native-thread":
262
+ parent_value = dict(parent) # captured once, no env reread on retry
263
+ else:
264
+ raise InvalidRequest("unsupported notification parent return-address kind")
265
+ if worker_engine == "controller" or source in {"notify", "new", "input", "compact"}:
266
+ # Input reuses the original global answer intent (not a new receipt).
267
+ # Notify has independent UUIDv4; new/input have keyed UUIDv5.
268
+ receipt = None
269
+ receipt_fields = dict(operation)
270
+ immutable = tuple(receipt_fields)
271
+ elif worker_engine == "codex":
272
+ from ccx.codex.delivery import read_receipt
273
+ receipt = read_receipt(f"codex:{wid}", delivery_id)
274
+ receipt_fields = {
275
+ "kind": "codex-turn", "deliveryId": delivery_id,
276
+ "requestedMode": (receipt or {}).get("requestedMode"),
277
+ "promptSha256": (receipt or {}).get("promptSha256"),
278
+ "callerMetadata": (receipt or {}).get("callerMetadata"),
279
+ "clientUserMessageId": (receipt or {}).get("clientUserMessageId"),
280
+ "turnId": None,
281
+ }
282
+ immutable = ("kind", "deliveryId", "requestedMode", "promptSha256",
283
+ "callerMetadata", "clientUserMessageId")
284
+ else:
285
+ from ccx.claude.delivery import read_receipt
286
+ receipt = read_receipt(f"claude:{wid}", delivery_id)
287
+ receipt_fields = {
288
+ "kind": "claude-turn", "deliveryId": delivery_id,
289
+ "requestedMode": (receipt or {}).get("requestedMode"),
290
+ "promptSha256": (receipt or {}).get("promptSha256"),
291
+ "promptByteCount": (receipt or {}).get("promptByteCount"),
292
+ "callerMetadata": (receipt or {}).get("callerMetadata"),
293
+ "cap": (receipt or {}).get("cap"),
294
+ }
295
+ immutable = tuple(receipt_fields)
296
+ if source == "send":
297
+ if receipt is None:
298
+ raise InvalidRequest("prepare original child receipt before notification registration")
299
+ if (receipt_fields["requestedMode"] != "new_turn_only"
300
+ or any(k in operation and operation[k] != receipt_fields[k]
301
+ for k in immutable)):
302
+ raise InvalidRequest("notification operation conflicts with original child receipt")
303
+ event_id = (str(uuid.uuid5(uuid.NAMESPACE_URL, "ccx-notify:" + key))
304
+ if key is not None else str(uuid.uuid4()))
305
+ timestamp = now_ms()
306
+
307
+ value = {
308
+ "version": 3, "generation": store.generation,
309
+ "eventId": event_id, "state": "armed", "source": source,
310
+ "parent": parent_value,
311
+ "worker": {"engine": worker_engine, "workerId": wid,
312
+ "generation": store.generation, "name": worker_name},
313
+ "operation": receipt_fields,
314
+ "settlementMode": "orchestration" if settle else None,
315
+ "registrationKeySha256": (hashlib.sha256(key.encode("utf-8")).hexdigest()
316
+ if key is not None else None),
317
+ "createdAtMs": timestamp, "updatedAtMs": timestamp,
318
+ "attempts": 0, "nextAttemptAtMs": 0,
319
+ }
320
+ _original_notification_registration(value) # no file or daemon effect on refusal
321
+ if worker_engine == "controller":
322
+ _controller_attempt_at_arm(wid, receipt_fields, source)
323
+ ensure_dirs()
324
+ # An incompatible supervisor cannot safely interpret this registration.
325
+ # Retire it before publishing; arming does not start a replacement.
326
+ _retire_incompatible_notifyd(timeout=5.0)
327
+ def publish() -> dict[str, Any]:
328
+ with open(_lock_path(event_id), "a+") as lock:
329
+ fcntl.flock(lock, fcntl.LOCK_EX)
330
+ prior = read_registration(event_id)
331
+ if prior is not None:
332
+ if source == "notify":
333
+ raise SessionUnavailable("independent notification event UUID collision")
334
+ # Do not reset progressed/ready/cancelled/delivered registration.
335
+ _original_notification_registration(prior)
336
+ if (prior.get("parent") != value["parent"]
337
+ or any((prior.get("worker") or {}).get(field) != value["worker"][field]
338
+ for field in ("engine", "workerId", "generation"))
339
+ or prior.get("registrationKeySha256") != value["registrationKeySha256"]
340
+ or prior.get("settlementMode") != value["settlementMode"]
341
+ or any((prior.get("operation") or {}).get(k) != receipt_fields[k]
342
+ for k in immutable)):
343
+ raise InvalidRequest("notification operation is owned by a different parent or input")
344
+ return prior
345
+ if source == "notify" and operation.get("kind") == f"{worker_engine}-compact":
346
+ owner = store.resolve(f"{worker_engine}:{wid}")
347
+ if _active_compact_selection(store, owner, store.record(owner.worker)) != operation:
348
+ raise SessionUnavailable("original active compact changed before publication")
349
+ if source == "compact":
350
+ # An ordinary compact may already own this operation ID. It
351
+ # cannot acquire a new "pre-effect" parent event retroactively.
352
+ effect = _compact_effect_receipt(
353
+ store, f"{worker_engine}:{wid}", worker_engine, wid, operation)
354
+ if effect is not None and effect.get("state") != "prepared":
355
+ raise InvalidRequest("cannot retroactively arm an existing native compact")
356
+ if worker_engine == "claude":
357
+ from ccx.ops.compact_ops import get_operation
358
+ marker = get_operation(wid, operation["operationId"])
359
+ else:
360
+ record = store.record(f"codex:{wid}")
361
+ marker = next((
362
+ row for field in ("activeCompact", "lastCompact")
363
+ if isinstance(row := record.get(field), dict)
364
+ and row.get("operationId") == operation["operationId"]
365
+ ), None)
366
+ if marker is not None:
367
+ raise InvalidRequest("cannot retroactively arm a recorded native compact")
368
+ if source == "send" and receipt.get("state") != "prepared":
369
+ raise InvalidRequest("cannot retroactively arm an accepted original Codex delivery")
370
+ if source == "input":
371
+ input_intent = store.read_receipt(f"{worker_engine}:{wid}", "answer", operation["responseId"])
372
+ if input_intent is None or input_intent.get("state") != "prepared":
373
+ raise InvalidRequest("cannot retroactively arm an already submitted original input")
374
+ store.revalidate()
375
+ _atomic_write(registration_path(event_id), value) # fsync before return
376
+ return value
377
+ if settle:
378
+ graph_lock = registrations_dir() / ".settlement-graph.lock"
379
+ with graph_lock.open("a+") as lock:
380
+ fcntl.flock(lock, fcntl.LOCK_EX)
381
+ if read_registration(event_id) is None:
382
+ _assert_no_settlement_cycle(parent_value, wid, store)
383
+ published = publish()
384
+ _hint_managed_parent_background(published)
385
+ return published
386
+ published = publish()
387
+ _hint_managed_parent_background(published)
388
+ if worker_engine == "controller":
389
+ # A preflighted, empty supervisor may have retired before this arm.
390
+ # The disk registration is authoritative even if restart is unavailable.
391
+ try:
392
+ ensure_notify_daemon()
393
+ except (SessionUnavailable, OSError):
394
+ pass
395
+ return published
396
+
397
+
398
+ def compact_notification_failure(reg: dict[str, Any], reason: str) -> tuple[bool, str]:
399
+ """On core failure, cancel only when its original receipt proves no submission."""
400
+ from ccx import constants
401
+ from ccx.store.root import require_ccx_active
402
+
403
+ try:
404
+ store = require_ccx_active(constants.CCX_ROOT)
405
+ worker = reg["worker"]
406
+ family = "compact" if worker["engine"] == "claude" else "codex-compact"
407
+ receipt = store.read_receipt(
408
+ f"{worker['engine']}:{worker['workerId']}", family,
409
+ reg["operation"]["operationId"])
410
+ except (ValueError, OSError):
411
+ receipt = {"state": "unreadable"} # Never cancel an ambiguous effect.
412
+ if receipt is None or receipt.get("state") in {"prepared", "rejected"}:
413
+ cancel_registration(reg["eventId"], reason)
414
+ uncertain = False
415
+ else:
416
+ uncertain = True
417
+ current = read_registration(reg["eventId"])
418
+ return uncertain, (current or reg)["state"]
419
+
420
+
421
+ def cancel_registration(event_id: str, reason: str) -> None:
422
+ def mutate(value: dict[str, Any]) -> None:
423
+ if value.get("state") in TERMINAL_REGISTRATION_STATES:
424
+ return
425
+ value["state"] = "cancelled"
426
+ value["cancelReason"] = reason
427
+ mutate_registration(event_id, mutate)
428
+
429
+
430
+ def set_codex_turn_id(event_id: str, turn_id: str | None) -> None:
431
+ """Bind only this registration's original accepted semantic turn."""
432
+ if not isinstance(turn_id, str) or not turn_id:
433
+ return
434
+ def mutate(value: dict[str, Any]) -> None:
435
+ from ccx.codex.delivery import read_receipt
436
+ worker = value["worker"]["workerId"]
437
+ delivery_id = value["operation"]["deliveryId"]
438
+ receipt = read_receipt(f"codex:{worker}", delivery_id)
439
+ if (receipt is None or receipt.get("state") != "accepted"
440
+ or receipt.get("turnId") != turn_id):
441
+ raise InvalidRequest("native turn differs from original Codex delivery receipt")
442
+ operation = value["operation"]
443
+ if operation.get("turnId") not in (None, turn_id):
444
+ raise InvalidRequest("original notification turn identity already bound")
445
+ operation["turnId"] = turn_id
446
+ mutate_registration(event_id, mutate)
447
+
448
+
449
+ def public_registration(value: dict[str, Any]) -> dict[str, Any]:
450
+ """Project the original event and native wake status, not parent completion."""
451
+ from ccx.notify.select import _original_notification_registration
452
+ from ccx.notify.daemon import _notifyd_alive
453
+ _store, parent, child = _original_notification_registration(value)
454
+ operation = value["operation"]
455
+ state = value["state"]
456
+ instruction = (
457
+ "Parent machine notification was placed; parent model completion is unproven."
458
+ if state == "delivered" else
459
+ ("Notification cancelled; the controller run is unaffected."
460
+ if value["worker"]["engine"] == "controller" else
461
+ "Notification cancelled; the child delivery is unaffected.")
462
+ if state == "cancelled" else
463
+ ("Parent notification undeliverable; the controller attempt remains recorded."
464
+ if value["worker"]["engine"] == "controller" else
465
+ "Parent notification undeliverable; the child receipt remains recorded.")
466
+ if state == "undeliverable" else
467
+ ("Controller parent notification is durable; a machine wake can follow "
468
+ "this attempt's outcome."
469
+ if value["worker"]["engine"] == "controller" else
470
+ "Notification saved; CCX will try to notify the calling Codex worker "
471
+ "once this worker's turn outcome is confirmed.")
472
+ )
473
+ turn_ref = operation.get("selectedTurnRef")
474
+ if (value["worker"]["engine"] == "claude" and turn_ref is None
475
+ and operation.get("deliveryId")):
476
+ from ccx.claude.delivery import read_receipt
477
+ original = read_receipt(child, operation["deliveryId"])
478
+ turn_ref = (original.get("turnRef") if isinstance(original, dict)
479
+ and original.get("deliveryId") == operation["deliveryId"] else None)
480
+ turn_ref = turn_ref or (value.get("settlement") or {}).get("turnRef")
481
+ result = {
482
+ "event_id": value["eventId"], "state": value["state"],
483
+ "parent": parent, "parent_kind": value["parent"]["kind"],
484
+ "worker": child, "worker_id": value["worker"]["workerId"],
485
+ "worker_name": value["worker"].get("name"),
486
+ "short": value["worker"]["workerId"][:8],
487
+ "delivery_id": operation.get("deliveryId"),
488
+ "turn_id": operation.get("turnId") or (value.get("settlement") or {}).get("turnId"),
489
+ **({"selection_mode": operation["selectionMode"],
490
+ "selected_turn_ref": operation.get("selectedTurnRef"),
491
+ "selected_original_delivery_id": operation.get("deliveryId")}
492
+ if value["source"] in {"notify", "new"} else {}),
493
+ **({"selected_turn_ref": operation["selectedTurnRef"]}
494
+ if value["source"] == "input" else {}),
495
+ **({"turn_ref": turn_ref} if value["worker"]["engine"] == "claude" else {}),
496
+ "source": value["source"],
497
+ "settle": value.get("settlementMode") == "orchestration",
498
+ "resume_automatically": False, "polling_required": False,
499
+ "delivery_available": state in {"armed", "ready", "delivered"},
500
+ "instruction": instruction,
501
+ "caller_metadata": operation.get("callerMetadata"),
502
+ "created_at_ms": value.get("createdAtMs"),
503
+ "updated_at_ms": value.get("updatedAtMs"),
504
+ "ready_at_ms": value.get("readyAtMs"),
505
+ "delivered_at_ms": value.get("deliveredAtMs"),
506
+ "attempts": int(value.get("attempts") or 0),
507
+ "last_attempt_at_ms": value.get("lastAttemptAtMs"),
508
+ "next_attempt_at_ms": value.get("nextAttemptAtMs"),
509
+ "last_error": value.get("lastError"),
510
+ "operation_kind": operation["kind"],
511
+ "settlement": value.get("settlement"),
512
+ }
513
+ if operation.get("kind") in {"claude-compact", "codex-compact"}:
514
+ result.update(operation_type="compact",
515
+ operation_id=operation["operationId"],
516
+ operation_status=(value.get("operationSettlement") or
517
+ value.get("settlement") or {}).get("operationStatus"))
518
+ if value["worker"]["engine"] == "controller":
519
+ result.update(operation_type="controller-attempt",
520
+ run_id=value["worker"]["workerId"], attempt=operation["attempt"],
521
+ notifier_available=_notifyd_alive())
522
+ return result
523
+
524
+
525
+ _VALID_TERMINAL_ROWS: set[tuple[str, int, int, int]] = set()
526
+
527
+
528
+ def iter_registrations(*, include_terminal: bool = False,
529
+ strict: bool = False) -> list[dict[str, Any]]:
530
+ """Read current notification rows without hiding damaged inventory."""
531
+ from ccx.notify.select import _original_notification_registration
532
+ ensure_dirs()
533
+ values: list[dict[str, Any]] = []
534
+ if strict:
535
+ from ccx import constants
536
+ from ccx.store.root import require_ccx_active
537
+ generation = require_ccx_active(constants.CCX_ROOT).generation
538
+
539
+ directory = registrations_dir().absolute() # entry paths, never symlink targets
540
+ invalid: list[str] = []
541
+ for path in sorted(directory.glob("*.json")):
542
+ try:
543
+ st = path.stat()
544
+ value = json.loads(path.read_text(encoding="utf-8"))
545
+ except (OSError, ValueError):
546
+ invalid.append(f"{path} (unreadable JSON)")
547
+ continue
548
+ version_key = (str(path), st.st_ino, st.st_mtime_ns, st.st_size)
549
+ if not isinstance(value, dict):
550
+ invalid.append(f"{path} (invalid row)")
551
+ continue
552
+ version, recorded = value.get("version"), value.get("generation")
553
+ if strict and ((type(version) is int and version in (1, 2)) or
554
+ (version == 3 and isinstance(recorded, str) and
555
+ recorded != generation)):
556
+ continue # Recognizable pre-migration or foreign generation: inert.
557
+ terminal = value.get("state") in TERMINAL_REGISTRATION_STATES
558
+ if terminal and not include_terminal and not strict:
559
+ continue # strict callers must still see damaged settled rows
560
+ # A settled row never changes again, so one successful validation per
561
+ # file version is enough; notifyd re-reads every row each 0.35 s tick.
562
+ if not (terminal and version_key in _VALID_TERMINAL_ROWS):
563
+ try:
564
+ _original_notification_registration(value)
565
+ except Exception:
566
+ # Normal notify supervision remains permissive to legacy rows.
567
+ invalid.append(f"{path} (invalid active row)")
568
+ continue
569
+ if terminal:
570
+ if len(_VALID_TERMINAL_ROWS) > 4096:
571
+ _VALID_TERMINAL_ROWS.clear()
572
+ _VALID_TERMINAL_ROWS.add(version_key)
573
+ if terminal and not include_terminal:
574
+ continue
575
+ values.append(value)
576
+ if strict and invalid:
577
+ raise SessionUnavailable(
578
+ "notification inventory incomplete: " + "; ".join(invalid)
579
+ + f". Move these files out of {directory} to unblock settle.")
580
+ return values
581
+
582
+
583
+ def has_pending_registrations() -> bool:
584
+ return bool(iter_registrations())
585
+
586
+
587
+ def terminal_retention_s() -> float:
588
+ """How long completed notification receipts remain on disk.
589
+
590
+ The receipts are useful for debugging and ambiguous-delivery reconciliation,
591
+ but they are one file per awaited operation and must not accumulate forever.
592
+ Invalid configuration falls back to seven days; zero prunes terminal receipts
593
+ on the next notifyd start.
594
+ """
595
+ raw = os.environ.get("CCX_NOTIFY_TERMINAL_RETENTION_S", str(7 * 24 * 60 * 60))
596
+ try:
597
+ value = float(raw)
598
+ except (TypeError, ValueError):
599
+ value = float(7 * 24 * 60 * 60)
600
+ return max(0.0, value)
601
+
602
+
603
+ def prune_terminal_registrations(*, now_ms_value: int | None = None) -> int:
604
+ """Remove terminal receipts older than the configured retention window.
605
+
606
+ Each candidate is re-read while holding its per-event flock so a concurrent
607
+ mutation cannot be pruned from a stale snapshot. Lock sidecars are removed
608
+ with the terminal JSON while the old inode remains locked by this process.
609
+ """
610
+ ensure_dirs()
611
+ current = now_ms() if now_ms_value is None else int(now_ms_value)
612
+ cutoff = current - int(terminal_retention_s() * 1000)
613
+ removed = 0
614
+ for path in sorted(registrations_dir().glob("*.json")):
615
+ event_id = path.stem
616
+ lock_path = _lock_path(event_id)
617
+ try:
618
+ lock = open(lock_path, "a+")
619
+ except OSError:
620
+ continue
621
+ try:
622
+ try:
623
+ fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
624
+ except BlockingIOError:
625
+ continue
626
+ value = read_registration(event_id)
627
+ if not isinstance(value, dict):
628
+ continue
629
+ if value.get("state") not in TERMINAL_REGISTRATION_STATES:
630
+ continue
631
+ terminal_at = (
632
+ value.get("deliveredAtMs")
633
+ or value.get("updatedAtMs")
634
+ or value.get("createdAtMs")
635
+ )
636
+ if not isinstance(terminal_at, (int, float)) or terminal_at > cutoff:
637
+ continue
638
+ try:
639
+ registration_path(event_id).unlink()
640
+ except FileNotFoundError:
641
+ pass
642
+ try:
643
+ lock_path.unlink()
644
+ except FileNotFoundError:
645
+ pass
646
+ removed += 1
647
+ finally:
648
+ lock.close()
649
+ return removed