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,304 @@
1
+ from __future__ import annotations
2
+ import json
3
+ from typing import Any
4
+ from ccx.errors import SessionUnavailable
5
+ from ccx.notify.registry import _current_worker_name, _delivery_id, iter_registrations, read_registration
6
+ from ccx.notify.select import _original_notification_registration
7
+ from ccx.notify.settle import _truncate_report
8
+
9
+
10
+ class DeliveryDeferred(RuntimeError):
11
+ """The wake is still valid but cannot be admitted right now."""
12
+
13
+
14
+ def _same_parent(a: dict[str, Any], b: dict[str, Any]) -> bool:
15
+ return (a.get("kind") == b.get("kind") and a.get("threadId") == b.get("threadId")
16
+ and (a.get("kind") != "ccx-codex" or
17
+ (a.get("workerId") == b.get("workerId") and
18
+ a.get("generation") == b.get("generation"))))
19
+
20
+
21
+ def outstanding_for_parent(reg: dict[str, Any], *, limit: int = 20) -> list[dict[str, Any]]:
22
+ parent = reg.get("parent") or {}
23
+ current = reg.get("eventId")
24
+ out: list[dict[str, Any]] = []
25
+ for other in iter_registrations():
26
+ if other.get("eventId") == current or not _same_parent(parent, other.get("parent") or {}):
27
+ continue
28
+ worker = other.get("worker") or {}
29
+ out.append({
30
+ "eventId": other.get("eventId"),
31
+ "worker": f"{worker.get('engine')}:{worker.get('workerId')}",
32
+ "workerName": _current_worker_name(worker),
33
+ "state": other.get("state"),
34
+ })
35
+ if len(out) >= limit:
36
+ break
37
+ return out
38
+
39
+
40
+ def payload_for_registration(
41
+ reg: dict[str, Any], settlement: dict[str, Any]
42
+ ) -> dict[str, Any]:
43
+ _store, parent, child = _original_notification_registration(reg)
44
+ controller_attempt = reg["worker"]["engine"] == "controller"
45
+ report = settlement.get("report")
46
+ report_truncated = bool(settlement.get("reportTruncated"))
47
+ if reg["source"] == "notify":
48
+ # The receipt-backed standalone path must obey the same bound as
49
+ # receiptless standalone notifications.
50
+ report, clipped = _truncate_report(report)
51
+ report_truncated = report_truncated or clipped
52
+ payload = {
53
+ "kind": "ccx-worker-notification", "phase": "settled",
54
+ "eventId": reg["eventId"],
55
+ "deliveryId": _delivery_id(reg["eventId"]),
56
+ "worker": child, "parent": parent,
57
+ "source": reg["source"], "status": settlement["status"],
58
+ **({"selectionMode": reg["operation"]["selectionMode"],
59
+ "selectedTurnRef": reg["operation"].get("selectedTurnRef"),
60
+ "selectedOriginalDeliveryId": reg["operation"].get("deliveryId")}
61
+ if reg["source"] in {"notify", "new"} else {}),
62
+ **({"selectedOriginalDeliveryId": reg["operation"]["deliveryId"]}
63
+ if reg["source"] == "send" else {}),
64
+ **({"selectedTurnRef": reg["operation"]["selectedTurnRef"]}
65
+ if reg["source"] == "input" else {}),
66
+ "workerState": settlement.get("workerState"),
67
+ "report": report,
68
+ "reportTruncated": report_truncated,
69
+ "turnId": settlement.get("turnId"),
70
+ **({"turnRef": settlement.get("turnRef")}
71
+ if reg["worker"]["engine"] == "claude" else {}),
72
+ "turnStatus": settlement.get("turnStatus"),
73
+ "waitingFor": settlement.get("waitingFor"),
74
+ "error": settlement.get("error"),
75
+ **({"settle": True, "settlementVerdict": settlement.get("settlementVerdict"),
76
+ "settlement": settlement.get("settlement")}
77
+ if reg.get("settlementMode") == "orchestration" else {}),
78
+ "activity": {"workerState": settlement.get("workerState")},
79
+ "callerMetadata": reg["operation"].get("callerMetadata"),
80
+ }
81
+ if reg["operation"].get("kind") in {"claude-compact", "codex-compact"}:
82
+ payload.update(operationType="compact",
83
+ operationId=reg["operation"]["operationId"],
84
+ operationStatus=settlement.get("operationStatus"),
85
+ compacted=settlement.get("compacted"),
86
+ evidenceId=settlement.get("evidenceId"),
87
+ compactTurnId=settlement.get("compactTurnId"),
88
+ contextItemId=settlement.get("contextItemId"),
89
+ nativeTurnStatus=settlement.get("nativeTurnStatus"),
90
+ preTokens=settlement.get("preTokens"),
91
+ postTokens=settlement.get("postTokens"),
92
+ contextAfter=settlement.get("contextAfter"))
93
+ if controller_attempt:
94
+ payload.update(
95
+ workerName=settlement.get("workerName"),
96
+ operationType="controller-attempt",
97
+ operationId=settlement.get("operationId"),
98
+ runId=settlement.get("runId"), attempt=settlement.get("attempt"),
99
+ controller=settlement.get("controller"),
100
+ controllerStatus=settlement.get("controllerStatus"),
101
+ checkpoint=settlement.get("checkpoint"),
102
+ summary=settlement.get("summary"),
103
+ reportPath=settlement.get("reportPath"),
104
+ activity=settlement["activity"],
105
+ outstanding=settlement["outstanding"],
106
+ )
107
+ if payload["reportTruncated"] and isinstance(settlement.get("reportPath"), str):
108
+ import shlex
109
+ payload["reportCommand"] = f"cat {shlex.quote(settlement['reportPath'])}"
110
+ return payload
111
+
112
+
113
+ def _external_codex_deliver(
114
+ reg: dict[str, Any], payload: dict[str, Any], *, reconcile: bool,
115
+ ) -> dict[str, Any]:
116
+ """Wake the caller-captured Codex thread; uncertain retries only observe.
117
+
118
+ The thread belongs to the user's own Codex UI, so it is only ever woken
119
+ while that UI has it loaded. It is never resumed here: a resumed thread
120
+ would run the notification turn headless, where nobody sees it.
121
+ """
122
+ from ccx.codex.control import CodexControlClient, CodexControlError
123
+ from ccx.codex.notify_history import notification_delivery_match
124
+
125
+ store, parent, _child = _original_notification_registration(reg)
126
+ if not isinstance(parent, dict) or parent.get("kind") != "codex-native-thread":
127
+ raise DeliveryDeferred("interactive Codex return address was not captured")
128
+ thread_id = parent["threadId"]
129
+ delivery_id = payload["deliveryId"]
130
+
131
+ def defer(reason: object, *, no_effect: bool = False) -> None:
132
+ error = DeliveryDeferred(str(reason))
133
+ # On an earlier possible machine write, a missing/failed history walk
134
+ # never authorizes the notifier to clear its durable attempt marker.
135
+ error.no_effect = no_effect and not reconcile
136
+ raise error
137
+
138
+ try:
139
+ client = CodexControlClient() # existing primary CODEX_HOME socket
140
+ except FileNotFoundError as error:
141
+ missing = DeliveryDeferred(f"external Codex parent control socket absent: {error}")
142
+ missing.no_effect = not reconcile
143
+ raise missing from error
144
+ except (OSError, ConnectionError, TimeoutError) as error:
145
+ defer(error, no_effect=True)
146
+ try:
147
+ try:
148
+ client.initialize()
149
+ except Exception as error:
150
+ defer(error, no_effect=True) # no native machine request made
151
+
152
+ if reconcile:
153
+ try:
154
+ match = notification_delivery_match(client.request, thread_id, delivery_id)
155
+ except Exception as error:
156
+ defer(error) # e.g. its UI closed; never resume to look
157
+ if (not isinstance(match, dict)
158
+ or match.get("deliveryId") != delivery_id
159
+ or match.get("eventId") != reg["eventId"]
160
+ or not isinstance(match.get("turnId"), str)
161
+ or not match["turnId"]):
162
+ defer("prior native notification placement remains unproven")
163
+ store.revalidate()
164
+ return {"ok": True, "reconciled": True,
165
+ "eventId": reg["eventId"], "deliveryId": delivery_id,
166
+ "threadId": thread_id, "turnId": match["turnId"]}
167
+
168
+ params = {
169
+ "threadId": thread_id, "input": [],
170
+ "turnTrigger": "ccx-worker-notification",
171
+ "toolOutput": {
172
+ "namespace": "ccx", "name": "worker_notification",
173
+ "output": json.dumps(payload, separators=(",", ":")),
174
+ },
175
+ }
176
+ try:
177
+ store.revalidate()
178
+ answer = client.request("turn/start", params, timeout=60)
179
+ except CodexControlError as error:
180
+ # Positive refusal, no write; "thread not found" means its UI is
181
+ # closed, so wait for it to reopen (bounded by notifyd's expiry).
182
+ defer(error, no_effect=True)
183
+ except Exception as error:
184
+ defer(error) # possible native placement, history-only next time
185
+ native_turn = answer.get("turn") if isinstance(answer, dict) else None
186
+ turn_id = (answer.get("turnId") if isinstance(answer, dict) else None)
187
+ if not isinstance(turn_id, str) or not turn_id:
188
+ turn_id = native_turn.get("id") if isinstance(native_turn, dict) else None
189
+ if not isinstance(turn_id, str) or not turn_id:
190
+ defer("native machine wake returned no positive original turn ID")
191
+ store.revalidate()
192
+ # A native ACK proves machine tool output placement, not model completion.
193
+ return {"ok": True, "reconciled": False,
194
+ "eventId": reg["eventId"], "deliveryId": delivery_id,
195
+ "threadId": thread_id, "turnId": turn_id}
196
+ finally:
197
+ client.close()
198
+
199
+
200
+ def _managed_codex_deliver(
201
+ reg: dict[str, Any], payload: dict[str, Any], *, reconcile: bool,
202
+ ) -> dict[str, Any]:
203
+ """Deliver/reconcile once; reload only an idle-unloaded original parent."""
204
+ from ccx.codex import runtime as runtime
205
+ from ccx.constants import CODEXD_RPC_GENERATION
206
+
207
+ store, parent, _child = _original_notification_registration(reg)
208
+ if not (reg.get("state") == "ready" and reg.get("payload") == payload
209
+ and isinstance(reg.get("settlement"), dict)
210
+ and payload == payload_for_registration(reg, reg["settlement"])
211
+ and payload.get("parent") == parent
212
+ and payload.get("deliveryId") == _delivery_id(reg["eventId"])):
213
+ raise DeliveryDeferred("original parent wake registration/payload not ready")
214
+ parent_id = reg["parent"]["workerId"]
215
+ lifecycle = (store.record(parent).get("lifecycle") or {})
216
+ state = lifecycle.get("state")
217
+ if state in {"dead", "budget-stopped"}:
218
+ # An explicitly killed parent cannot be reactivated by a child wake.
219
+ # The ready event and any earlier native attempt remain intact until
220
+ # an independent user action makes that same parent live again.
221
+ error = DeliveryDeferred(f"original parent {state}; machine wake still pending")
222
+ error.no_effect = not reconcile
223
+ raise error
224
+ if state in {"idle", "idle-killed", "crashed"}:
225
+ # Idle-killed means the janitor (or a daemon exit) unloaded the
226
+ # original thread; crashed is a runtime loss, not an explicit stop.
227
+ # A recorded idle parent also needs reload if the singleton has gone;
228
+ # do not force an idle-only resume preflight on an ACTIVE busy parent.
229
+ reload_needed = state in {"idle-killed", "crashed"}
230
+ if not reload_needed:
231
+ from ccx.store.worker_state import RUNTIME_ABSENT, RUNTIME_INCONCLUSIVE
232
+ observed = runtime.observe_worker(parent_id, timeout=.5)
233
+ if observed["presence"] == RUNTIME_INCONCLUSIVE:
234
+ error = DeliveryDeferred("original parent runtime presence inconclusive")
235
+ error.no_effect = not reconcile
236
+ raise error
237
+ reload_needed = observed["presence"] == RUNTIME_ABSENT
238
+ if reload_needed:
239
+ from ccx.ops.send import ensure_ready
240
+ try:
241
+ ready = ensure_ready(parent, timeout=60,
242
+ expected_generation=store.generation)
243
+ except Exception as exc:
244
+ error = DeliveryDeferred(f"original idle parent reload unavailable: {exc}")
245
+ # A failed reload is no evidence about a previous possible
246
+ # machine wake: W1 must retain its attempted marker.
247
+ error.no_effect = not reconcile
248
+ raise error from exc
249
+ if (ready.get("worker") != parent or ready.get("state") != "idle"
250
+ or ready.get("thread_id") != reg["parent"]["threadId"]):
251
+ error = DeliveryDeferred("original parent reload has no exact idle/thread proof")
252
+ error.no_effect = not reconcile
253
+ raise error
254
+ store.revalidate()
255
+ reply = runtime.daemon_rpc({
256
+ "op": "parent-notify", "short": parent_id,
257
+ "rpcGeneration": CODEXD_RPC_GENERATION,
258
+ "eventId": reg["eventId"], "deliveryId": payload["deliveryId"],
259
+ "payload": payload, "reconcile": reconcile,
260
+ }, timeout=65)
261
+ if reply.get("transport") != "served":
262
+ error = DeliveryDeferred("original parent runtime unavailable or inconclusive")
263
+ # A positively absent daemon received no native request. A lost
264
+ # response from a served daemon is never proof of native non-delivery.
265
+ # A reconcile-only retry says nothing about the earlier attempt's
266
+ # possible wake, so it must never clear that attempted marker.
267
+ error.no_effect = not reconcile and reply.get("transport") == "absent"
268
+ raise error
269
+ try:
270
+ runtime.require_served_generation(reply, short=parent_id)
271
+ except SessionUnavailable as error:
272
+ raise DeliveryDeferred(str(error)) from error
273
+ if reply.get("ok") is not True:
274
+ error = DeliveryDeferred(str(reply.get("error") or "original parent notification deferred"))
275
+ error.no_effect = (not reconcile and reply.get("definitive") is True
276
+ and reply.get("pending") is not True)
277
+ raise error
278
+ if (reply.get("workerId") != parent_id
279
+ or reply.get("threadId") != reg["parent"]["threadId"]
280
+ or reply.get("eventId") != reg["eventId"]
281
+ or reply.get("deliveryId") != payload["deliveryId"]
282
+ or not isinstance(reply.get("turnId"), str)
283
+ or not reply["turnId"]):
284
+ raise DeliveryDeferred("positive parent wake lacks original UUID/thread/turn proof")
285
+ store.revalidate()
286
+ # Native ACK means machine output was placed, not parent model completed.
287
+ return reply
288
+
289
+
290
+ def deliver_to_parent(reg: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]:
291
+ _original_notification_registration(reg)
292
+ fresh = read_registration(reg["eventId"])
293
+ if (fresh is None or not (
294
+ fresh.get("state") == "ready" and fresh.get("payload") == payload
295
+ and isinstance(fresh.get("settlement"), dict)
296
+ and payload == payload_for_registration(fresh, fresh["settlement"])
297
+ )):
298
+ raise DeliveryDeferred("original notification cancelled or changed before parent delivery")
299
+ # The supplied reg is the pre-attempt snapshot. First placement stays
300
+ # fresh; ambiguous retries reconcile the same final delivery ID only.
301
+ reconcile = reg.get("deliveryAttemptedAtMs") is not None
302
+ if fresh["parent"]["kind"] == "codex-native-thread":
303
+ return _external_codex_deliver(fresh, payload, reconcile=reconcile)
304
+ return _managed_codex_deliver(fresh, payload, reconcile=reconcile)
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def root() -> Path:
7
+ from ccx import constants
8
+ return constants.CCX_ROOT / "notify"
9
+
10
+
11
+ def ensure_dirs() -> None:
12
+ from ccx.store.paths import private_mkdir
13
+
14
+ base = root()
15
+ private_mkdir(base.parent)
16
+ try:
17
+ private_mkdir(base)
18
+ except OSError:
19
+ pass
20
+ registrations_dir().mkdir(exist_ok=True)
21
+ try:
22
+ registrations_dir().chmod(0o700)
23
+ except OSError:
24
+ pass
25
+
26
+
27
+ def registrations_dir() -> Path:
28
+ return root() / "registrations"
29
+
30
+
31
+ def registration_path(event_id: str) -> Path:
32
+ return registrations_dir() / f"{event_id}.json"
33
+
34
+
35
+ def daemon_generation_path() -> Path:
36
+ # One published role stamp: PID, protocol, ACTIVE generation, original
37
+ # kernel start token. A bare PID or obsolete two-field stamp never proves
38
+ # authority to signal the supervisor.
39
+ return root() / "notifyd.generation"
40
+
41
+
42
+ def daemon_lock_path() -> Path:
43
+ return root() / "notifyd.lock"
44
+
45
+
46
+ def daemon_spawn_lock_path() -> Path:
47
+ return root() / "notifyd.spawn.lock"
48
+
49
+
50
+ def daemon_log_path() -> Path:
51
+ return root() / "notifyd.log"