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.
- package/LICENSE +21 -0
- package/README.md +241 -2
- package/bin/ccx +56 -0
- package/ccx/__init__.py +163 -0
- package/ccx/__main__.py +28 -0
- package/ccx/claude/__init__.py +0 -0
- package/ccx/claude/analytics.py +466 -0
- package/ccx/claude/attach.py +762 -0
- package/ccx/claude/context.py +465 -0
- package/ccx/claude/delivery.py +806 -0
- package/ccx/claude/external.py +253 -0
- package/ccx/claude/host/__init__.py +0 -0
- package/ccx/claude/host/events.py +297 -0
- package/ccx/claude/host/hooks.py +438 -0
- package/ccx/claude/host/pty.py +250 -0
- package/ccx/claude/host/session.py +5184 -0
- package/ccx/claude/host/subagents.py +169 -0
- package/ccx/claude/host/successor.py +267 -0
- package/ccx/claude/managed_turns.py +240 -0
- package/ccx/claude/oauth.py +176 -0
- package/ccx/claude/settings.py +339 -0
- package/ccx/claude/spawn.py +557 -0
- package/ccx/claude/tasks.py +72 -0
- package/ccx/claude/telemetry.py +282 -0
- package/ccx/claude/transcript.py +2281 -0
- package/ccx/claude/usage.py +837 -0
- package/ccx/claude/usage_control.py +152 -0
- package/ccx/cli/__init__.py +0 -0
- package/ccx/cli/codex.py +1009 -0
- package/ccx/cli/commands/__init__.py +0 -0
- package/ccx/cli/commands/_common.py +407 -0
- package/ccx/cli/commands/control.py +161 -0
- package/ccx/cli/commands/create.py +669 -0
- package/ccx/cli/commands/read.py +1020 -0
- package/ccx/cli/commands/turns.py +1081 -0
- package/ccx/cli/controller.py +757 -0
- package/ccx/cli/main.py +301 -0
- package/ccx/cli/parser.py +1324 -0
- package/ccx/cli/skills.py +215 -0
- package/ccx/cli/statusline.py +160 -0
- package/ccx/client.py +1900 -0
- package/ccx/codex/__init__.py +0 -0
- package/ccx/codex/attach_tty.py +267 -0
- package/ccx/codex/client.py +732 -0
- package/ccx/codex/completion.py +121 -0
- package/ccx/codex/control.py +288 -0
- package/ccx/codex/daemon/__init__.py +0 -0
- package/ccx/codex/daemon/__main__.py +4 -0
- package/ccx/codex/daemon/daemon.py +1556 -0
- package/ccx/codex/daemon/models.py +140 -0
- package/ccx/codex/daemon/native.py +1590 -0
- package/ccx/codex/daemon/release.py +367 -0
- package/ccx/codex/daemon/worker.py +2396 -0
- package/ccx/codex/delivery.py +758 -0
- package/ccx/codex/delivery_recovery.py +232 -0
- package/ccx/codex/evidence.py +827 -0
- package/ccx/codex/history.py +158 -0
- package/ccx/codex/notify_history.py +140 -0
- package/ccx/codex/ops.py +666 -0
- package/ccx/codex/paths.py +70 -0
- package/ccx/codex/protocol.py +805 -0
- package/ccx/codex/provenance.py +105 -0
- package/ccx/codex/read.py +29 -0
- package/ccx/codex/records.py +51 -0
- package/ccx/codex/reports.py +177 -0
- package/ccx/codex/runtime.py +1412 -0
- package/ccx/codex/settle.py +363 -0
- package/ccx/codex/telemetry.py +792 -0
- package/ccx/codex/types.py +659 -0
- package/ccx/codex/worker_environment.py +25 -0
- package/ccx/constants.py +288 -0
- package/ccx/controllers/__init__.py +0 -0
- package/ccx/controllers/controller.py +1449 -0
- package/ccx/controllers/live_vars.py +1706 -0
- package/ccx/controllers/live_vars_runner.py +216 -0
- package/ccx/controllers/notify.py +213 -0
- package/ccx/controllers/orchestrator.py +193 -0
- package/ccx/controllers/policy.py +57 -0
- package/ccx/controllers/store.py +681 -0
- package/ccx/errors.py +634 -0
- package/ccx/notify/__init__.py +0 -0
- package/ccx/notify/__main__.py +5 -0
- package/ccx/notify/client.py +116 -0
- package/ccx/notify/daemon.py +480 -0
- package/ccx/notify/deliver.py +304 -0
- package/ccx/notify/paths.py +51 -0
- package/ccx/notify/registry.py +649 -0
- package/ccx/notify/select.py +750 -0
- package/ccx/notify/settle.py +684 -0
- package/ccx/ops/__init__.py +0 -0
- package/ccx/ops/background.py +115 -0
- package/ccx/ops/cancel.py +289 -0
- package/ccx/ops/codex_import.py +96 -0
- package/ccx/ops/common.py +397 -0
- package/ccx/ops/compact.py +337 -0
- package/ccx/ops/compact_ops.py +550 -0
- package/ccx/ops/context_events.py +48 -0
- package/ccx/ops/conversation.py +1540 -0
- package/ccx/ops/conversation_paging.py +1338 -0
- package/ccx/ops/create.py +56 -0
- package/ccx/ops/creation.py +952 -0
- package/ccx/ops/creation_steps.py +579 -0
- package/ccx/ops/event_feed.py +710 -0
- package/ccx/ops/forking.py +711 -0
- package/ccx/ops/input.py +556 -0
- package/ccx/ops/maintenance.py +295 -0
- package/ccx/ops/report.py +770 -0
- package/ccx/ops/semantics.py +490 -0
- package/ccx/ops/send.py +961 -0
- package/ccx/ops/steer_ops.py +867 -0
- package/ccx/ops/terminate.py +383 -0
- package/ccx/ops/turn_reports.py +306 -0
- package/ccx/ops/wait.py +762 -0
- package/ccx/ops/worker_events.py +636 -0
- package/ccx/rpc/__init__.py +0 -0
- package/ccx/rpc/local_rpc.py +1602 -0
- package/ccx/rpc/rpc.py +559 -0
- package/ccx/rpc/serve.py +374 -0
- package/ccx/rpc/transport.py +409 -0
- package/ccx/rpc/view.py +125 -0
- package/ccx/store/__init__.py +0 -0
- package/ccx/store/discovery.py +170 -0
- package/ccx/store/identity.py +105 -0
- package/ccx/store/journal_io.py +79 -0
- package/ccx/store/liveness.py +194 -0
- package/ccx/store/paths.py +254 -0
- package/ccx/store/record_cache.py +40 -0
- package/ccx/store/records.py +309 -0
- package/ccx/store/root.py +804 -0
- package/ccx/store/runtime_presence.py +137 -0
- package/ccx/store/store.py +2332 -0
- package/ccx/store/worker_state.py +86 -0
- package/ccx/system/__init__.py +0 -0
- package/ccx/system/bounded.py +296 -0
- package/ccx/system/cwd_paths.py +25 -0
- package/ccx/system/engine_binary.py +59 -0
- package/ccx/system/executable.py +50 -0
- package/ccx/system/oslayer.py +667 -0
- package/ccx/system/pipes.py +46 -0
- package/ccx/system/sanitize.py +43 -0
- package/ccx/types.py +705 -0
- package/ccx/version.py +29 -0
- package/ccx/workers.py +2081 -0
- package/ccx.py +9 -0
- package/package.json +40 -4
- package/pyproject.toml +87 -0
- package/skills/ccx/SKILL.md +155 -0
- package/skills/ccx/references/controllers.md +244 -0
- package/skills/ccx/references/troubleshooting.md +94 -0
- 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"
|