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,750 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import hashlib
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any
|
|
6
|
+
from ccx.errors import InvalidRequest, SessionUnavailable
|
|
7
|
+
from ccx.ops.turn_reports import codex_native_turn_evidence as _codex_native_turn_evidence
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _controller_notification_shape(
|
|
11
|
+
value: dict[str, Any], worker: dict[str, Any], operation: dict[str, Any],
|
|
12
|
+
) -> None:
|
|
13
|
+
"""Static v3 controller subject, independent of any later run state."""
|
|
14
|
+
from ccx.store.identity import IdentityError, canonical_worker_id
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
canonical_worker_id(worker.get("workerId"))
|
|
18
|
+
canonical_worker_id(operation.get("attemptToken"))
|
|
19
|
+
event = uuid.UUID(value.get("eventId"))
|
|
20
|
+
except (IdentityError, TypeError, ValueError, AttributeError):
|
|
21
|
+
raise SessionUnavailable("controller notification identity invalid") from None
|
|
22
|
+
if (set(worker) != {"engine", "workerId", "generation", "name"}
|
|
23
|
+
or set(operation) != {"kind", "attempt", "attemptToken", "startupDeadlineMs"}
|
|
24
|
+
or operation.get("kind") != "controller-attempt"
|
|
25
|
+
or type(operation.get("attempt")) is not int or operation["attempt"] < 1
|
|
26
|
+
or type(operation.get("startupDeadlineMs")) is not int
|
|
27
|
+
or operation["startupDeadlineMs"] <= 0
|
|
28
|
+
or str(event) != value.get("eventId") or event.version != 4
|
|
29
|
+
or value.get("registrationKeySha256") is not None
|
|
30
|
+
or value.get("settlementMode") is not None):
|
|
31
|
+
raise SessionUnavailable("controller notification identity invalid")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _controller_attempt_at_arm(run_id: str, operation: dict[str, Any], source: str) -> None:
|
|
35
|
+
"""Check controller run/layout/slot once, before publishing an event."""
|
|
36
|
+
from ccx.controllers import store as controller_store
|
|
37
|
+
from ccx.errors import ControllerError
|
|
38
|
+
|
|
39
|
+
bootstrap = controller_store.bootstrap_root() / run_id
|
|
40
|
+
run_dir = (bootstrap if bootstrap.is_dir() else next(
|
|
41
|
+
(path for path in controller_store._all_run_dirs() if path.name == run_id), None
|
|
42
|
+
))
|
|
43
|
+
if run_dir is None:
|
|
44
|
+
raise SessionUnavailable("controller run not found for notification")
|
|
45
|
+
try:
|
|
46
|
+
record = controller_store.read_json(controller_store.run_record_path(run_dir))
|
|
47
|
+
controller_store.require_operation_journal_layout(run_dir, record)
|
|
48
|
+
except (ControllerError, OSError) as exc:
|
|
49
|
+
raise SessionUnavailable(f"controller notification run unavailable: {exc}") from exc
|
|
50
|
+
if record.get("runId") != run_id:
|
|
51
|
+
raise SessionUnavailable("controller notification run identity changed")
|
|
52
|
+
number, token = operation["attempt"], operation["attemptToken"]
|
|
53
|
+
attempts = record.get("attempts") or []
|
|
54
|
+
selected = next((item for item in attempts
|
|
55
|
+
if isinstance(item, dict) and item.get("number") == number), None)
|
|
56
|
+
if selected is not None and selected.get("attemptToken") == token:
|
|
57
|
+
if source != "controller.run" or number == 1:
|
|
58
|
+
return
|
|
59
|
+
elif (selected is None and source == "controller.resume" and number == len(attempts) + 1
|
|
60
|
+
and attempts and isinstance(attempts[-1], dict)
|
|
61
|
+
and attempts[-1].get("number") == len(attempts)
|
|
62
|
+
and (record.get("lifecycle") or {}).get("state")
|
|
63
|
+
in controller_store.TERMINAL_LIFECYCLES):
|
|
64
|
+
# The caller holds resume.lock and publishes this exact token next.
|
|
65
|
+
return
|
|
66
|
+
raise InvalidRequest("controller notification attempt or token is not the selected start")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _original_notification_parent(store, parent: dict[str, Any], worker_id: str | None):
|
|
70
|
+
"""The existing exact Codex return address; never infer one from a child."""
|
|
71
|
+
if parent["kind"] == "codex-native-thread":
|
|
72
|
+
native_thread = parent.get("threadId")
|
|
73
|
+
if (set(parent) != {"kind", "threadId"}
|
|
74
|
+
or not isinstance(native_thread, str) or not native_thread
|
|
75
|
+
or native_thread != native_thread.strip() or "\x00" in native_thread
|
|
76
|
+
or len(native_thread.encode("utf-8")) > 1024):
|
|
77
|
+
raise SessionUnavailable("interactive parent return address malformed")
|
|
78
|
+
# Managed-native inventory exclusion is proven when the caller arms.
|
|
79
|
+
return dict(parent)
|
|
80
|
+
parent_id = parent.get("workerId")
|
|
81
|
+
parent_owner = store.resolve(f"codex:{parent_id}")
|
|
82
|
+
if (parent_owner.worker_id != parent_id or parent_id == worker_id
|
|
83
|
+
or parent.get("generation") != store.generation):
|
|
84
|
+
raise SessionUnavailable("Notification recipient is invalid; verify the parent worker")
|
|
85
|
+
store.authorize_native_action(parent_owner.worker, "send")
|
|
86
|
+
parent_native = (store.record(parent_owner.worker).get("native") or {})
|
|
87
|
+
if (parent.get("threadId") != parent_native.get("threadId")
|
|
88
|
+
or parent_native.get("threadSource") != store.codex_source(parent_owner.worker)):
|
|
89
|
+
raise SessionUnavailable("Notification recipient changed; create a new notification")
|
|
90
|
+
return parent_owner.worker
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _compact_effect_receipt(store, child: str, engine: str, worker_id: str,
|
|
94
|
+
operation: dict[str, Any]) -> dict[str, Any] | None:
|
|
95
|
+
"""Bind a compact event to its original full-UUID native source and intent."""
|
|
96
|
+
from ccx.ops.compact_ops import compact_intent_digest as claude_digest
|
|
97
|
+
from ccx.codex.ops import compact_intent_digest as codex_digest
|
|
98
|
+
|
|
99
|
+
original = operation["nativeSourceId"]
|
|
100
|
+
operation_id = operation["operationId"]
|
|
101
|
+
record = store.record(child)
|
|
102
|
+
native = record.get("native") or {}
|
|
103
|
+
activation = store.native_activation(child)
|
|
104
|
+
name = "sessionId" if engine == "claude" else "threadId"
|
|
105
|
+
source = (f"claude-session:{original}" if engine == "claude"
|
|
106
|
+
else store.codex_source(child))
|
|
107
|
+
if (native.get(name) != original
|
|
108
|
+
or activation.get("state") != "confirmed"
|
|
109
|
+
or activation.get("source") != source
|
|
110
|
+
or (activation.get("nativeBinding") or {}).get(name) != original
|
|
111
|
+
or (engine == "codex" and native.get("threadSource") != source)):
|
|
112
|
+
raise SessionUnavailable("Compaction worker changed; inspect its operation")
|
|
113
|
+
family = "compact" if engine == "claude" else "codex-compact"
|
|
114
|
+
digest = (claude_digest(worker_id, generation=store.generation,
|
|
115
|
+
native_session_id=original, operation_id=operation_id)
|
|
116
|
+
if engine == "claude" else codex_digest(
|
|
117
|
+
worker_id, store.generation, original, source, operation_id))
|
|
118
|
+
receipt = store.read_receipt(child, family, operation_id)
|
|
119
|
+
if receipt is not None and (
|
|
120
|
+
receipt.get("workerId") != worker_id
|
|
121
|
+
or receipt.get("engine") != engine
|
|
122
|
+
or receipt.get("generation") != store.generation
|
|
123
|
+
or receipt.get("operationId") != operation_id
|
|
124
|
+
or receipt.get("family") != family
|
|
125
|
+
or receipt.get("action") != "compact"
|
|
126
|
+
or receipt.get("intentSha256") != digest):
|
|
127
|
+
raise SessionUnavailable("Compaction delivery does not match the worker; inspect the operation")
|
|
128
|
+
return receipt
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _original_notification_registration(value: dict[str, Any]) -> tuple[Any, str | dict[str, Any], str]:
|
|
132
|
+
"""Verify the typed notification subject and original Codex parent.
|
|
133
|
+
|
|
134
|
+
The old short-keyed registration layout is not a routing fallback; an
|
|
135
|
+
interactive native return address is never a managed worker target.
|
|
136
|
+
"""
|
|
137
|
+
from ccx import constants
|
|
138
|
+
from ccx.store.root import require_ccx_active
|
|
139
|
+
|
|
140
|
+
store = require_ccx_active(constants.CCX_ROOT)
|
|
141
|
+
parent = value.get("parent") if isinstance(value.get("parent"), dict) else {}
|
|
142
|
+
worker = value.get("worker") if isinstance(value.get("worker"), dict) else {}
|
|
143
|
+
operation = value.get("operation") if isinstance(value.get("operation"), dict) else {}
|
|
144
|
+
worker_id = worker.get("workerId")
|
|
145
|
+
engine = worker.get("engine")
|
|
146
|
+
source = value.get("source")
|
|
147
|
+
if (value.get("version") != 3 or value.get("generation") != store.generation
|
|
148
|
+
or parent.get("kind") not in {"ccx-codex", "codex-native-thread"}
|
|
149
|
+
or engine not in {"codex", "claude", "controller"}
|
|
150
|
+
or worker.get("generation") != store.generation
|
|
151
|
+
or source not in ({"controller.run", "controller.resume", "controller.notify"}
|
|
152
|
+
if engine == "controller" else {"send", "notify", "new", "input", "compact"})
|
|
153
|
+
or value.get("settlementMode") not in {None, "orchestration"}
|
|
154
|
+
or (value.get("settlementMode") == "orchestration" and engine != "codex")):
|
|
155
|
+
raise SessionUnavailable("Notification record is incompatible; create a new notification")
|
|
156
|
+
if engine == "controller":
|
|
157
|
+
_controller_notification_shape(value, worker, operation)
|
|
158
|
+
# The independently minted controller run UUID is not a worker owner.
|
|
159
|
+
return store, _original_notification_parent(store, parent, None), f"controller:{worker_id}"
|
|
160
|
+
if source == "send":
|
|
161
|
+
from ccx.claude.delivery import valid_delivery_id
|
|
162
|
+
if (operation.get("kind") != f"{engine}-turn"
|
|
163
|
+
or not valid_delivery_id(operation.get("deliveryId"))):
|
|
164
|
+
raise SessionUnavailable("original send notification has no owned delivery ID")
|
|
165
|
+
key = f"full-uuid-{engine}-send:{store.generation}:{worker_id}:{operation['deliveryId']}"
|
|
166
|
+
if (value.get("eventId") != str(uuid.uuid5(uuid.NAMESPACE_URL, "ccx-notify:" + key))
|
|
167
|
+
or value.get("registrationKeySha256") != hashlib.sha256(key.encode()).hexdigest()):
|
|
168
|
+
raise SessionUnavailable("notification event ID differs from original semantic key")
|
|
169
|
+
elif source == "compact":
|
|
170
|
+
operation_id = operation.get("operationId")
|
|
171
|
+
original = operation.get("nativeSourceId")
|
|
172
|
+
if (set(operation) != {"kind", "operationId", "nativeSourceId"}
|
|
173
|
+
or operation.get("kind") != f"{engine}-compact"
|
|
174
|
+
or not isinstance(operation_id, str) or not 0 < len(operation_id) <= 128
|
|
175
|
+
or "\x00" in operation_id
|
|
176
|
+
or not isinstance(original, str) or not original):
|
|
177
|
+
raise SessionUnavailable("original compact notification operation malformed")
|
|
178
|
+
key = f"full-uuid-{engine}-compact:{store.generation}:{worker_id}:{operation_id}"
|
|
179
|
+
if (value.get("eventId") != str(uuid.uuid5(
|
|
180
|
+
uuid.NAMESPACE_URL, "ccx-notify:" + key))
|
|
181
|
+
or value.get("registrationKeySha256") != hashlib.sha256(
|
|
182
|
+
key.encode()).hexdigest()):
|
|
183
|
+
raise SessionUnavailable("original compact notification event differs from operation")
|
|
184
|
+
elif source == "input":
|
|
185
|
+
from ccx.store.root import _HASH
|
|
186
|
+
key = f"full-uuid-{engine}-input:{store.generation}:{worker_id}:{operation.get('gateId')}"
|
|
187
|
+
hashed_input = (engine == "claude"
|
|
188
|
+
and operation.get("kind") in {"claude-input-answer", "claude-input-respond"})
|
|
189
|
+
fields = {"kind", "gateId", "responseId", "nativeSourceId",
|
|
190
|
+
"intentSha256", "selectedTurnRef"}
|
|
191
|
+
fields |= ({"payloadSha256"} if hashed_input
|
|
192
|
+
else {"decision", "reason" if engine == "claude" else "turnId"})
|
|
193
|
+
reason = operation.get("reason")
|
|
194
|
+
ref = operation.get("selectedTurnRef")
|
|
195
|
+
if (set(operation) != fields
|
|
196
|
+
or (not hashed_input and (
|
|
197
|
+
operation.get("kind") != f"{engine}-input-decision"
|
|
198
|
+
or operation.get("decision") not in {"approve", "reject"}))
|
|
199
|
+
or (hashed_input and (
|
|
200
|
+
not isinstance(operation.get("payloadSha256"), str)
|
|
201
|
+
or not _HASH.fullmatch(operation["payloadSha256"])))
|
|
202
|
+
or any(not isinstance(operation.get(field), str) or not operation[field]
|
|
203
|
+
for field in ("gateId", "responseId", "nativeSourceId",
|
|
204
|
+
"intentSha256"))
|
|
205
|
+
or (engine == "claude"
|
|
206
|
+
and (reason is not None and (not isinstance(reason, str) or not reason)
|
|
207
|
+
or not isinstance(ref, str)
|
|
208
|
+
or not ref.startswith("ccx-turn:claude:v1:")
|
|
209
|
+
or len(ref) <= len("ccx-turn:claude:v1:")))
|
|
210
|
+
or (engine == "codex"
|
|
211
|
+
and (not isinstance(operation.get("turnId"), str)
|
|
212
|
+
or not operation["turnId"]
|
|
213
|
+
or ref != f"ccx-turn:codex:v1:{operation['turnId']}"))
|
|
214
|
+
or value.get("eventId") != str(uuid.uuid5(
|
|
215
|
+
uuid.NAMESPACE_URL, "ccx-notify:" + key))
|
|
216
|
+
or value.get("registrationKeySha256") != hashlib.sha256(key.encode()).hexdigest()):
|
|
217
|
+
raise SessionUnavailable("original input notification identity invalid")
|
|
218
|
+
elif source == "new":
|
|
219
|
+
# One original positive native creation, no model send or latest-turn
|
|
220
|
+
# selection. The event is keyed to the child, not the parent/mode.
|
|
221
|
+
key = f"full-uuid-{engine}-new:{store.generation}:{worker_id}:promptless"
|
|
222
|
+
if (engine not in {"claude", "codex"} or type(operation.get("expectTurn")) is not bool
|
|
223
|
+
or operation != {"kind": f"{engine}-stable", "expectTurn": False,
|
|
224
|
+
"selectionMode": "status-only", "stableState": "idle"}
|
|
225
|
+
or value.get("eventId") != str(uuid.uuid5(
|
|
226
|
+
uuid.NAMESPACE_URL, "ccx-notify:" + key))
|
|
227
|
+
or value.get("registrationKeySha256") != hashlib.sha256(key.encode()).hexdigest()):
|
|
228
|
+
raise SessionUnavailable("original promptless creation identity invalid")
|
|
229
|
+
else:
|
|
230
|
+
# Independent observation arms use random event identity. They are not
|
|
231
|
+
# fresh send attempts and cannot rekey an earlier delivery's event.
|
|
232
|
+
try:
|
|
233
|
+
event = uuid.UUID(value.get("eventId"))
|
|
234
|
+
except (ValueError, TypeError, AttributeError):
|
|
235
|
+
raise SessionUnavailable("standalone notification event UUID malformed") from None
|
|
236
|
+
if (str(event) != value.get("eventId") or event.version != 4
|
|
237
|
+
or value.get("registrationKeySha256") is not None):
|
|
238
|
+
raise SessionUnavailable("standalone observation needs its own UUIDv4 event")
|
|
239
|
+
kind = operation.get("kind")
|
|
240
|
+
selection = operation.get("selectionMode")
|
|
241
|
+
if kind == f"{engine}-compact" and selection == "active-compact":
|
|
242
|
+
token = operation.get("operationId")
|
|
243
|
+
original = operation.get("nativeSourceId")
|
|
244
|
+
if (set(operation) != {"kind", "selectionMode", "operationId",
|
|
245
|
+
"nativeSourceId"}
|
|
246
|
+
or not isinstance(token, str) or not 0 < len(token) <= 128
|
|
247
|
+
or "\x00" in token or not isinstance(original, str) or not original):
|
|
248
|
+
raise SessionUnavailable("standalone compact observation identity invalid")
|
|
249
|
+
elif kind == f"{engine}-stable":
|
|
250
|
+
if (selection != "status-only"
|
|
251
|
+
or operation.get("stableState") not in {
|
|
252
|
+
"idle", "idle-killed", "dead", "crashed", "budget-stopped"
|
|
253
|
+
} or operation.get("selectedTurnRef") is not None
|
|
254
|
+
or operation.get("deliveryId") is not None):
|
|
255
|
+
raise SessionUnavailable("standalone stable observation identity invalid")
|
|
256
|
+
elif (kind == "claude-next-turn" and engine == "claude"
|
|
257
|
+
and selection == "future"):
|
|
258
|
+
from ccx.ops.event_feed import decode_cursor
|
|
259
|
+
cursor = operation.get("armCursor")
|
|
260
|
+
ref = operation.get("selectedTurnRef")
|
|
261
|
+
if (set(operation) != {"kind", "selectionMode", "nativeSourceId",
|
|
262
|
+
"armCursor", "selectedTurnRef"}
|
|
263
|
+
or not isinstance(cursor, str)
|
|
264
|
+
or (ref is not None and (not isinstance(ref, str)
|
|
265
|
+
or not ref.startswith("ccx-turn:claude:v1:")
|
|
266
|
+
or len(ref) <= len("ccx-turn:claude:v1:")))):
|
|
267
|
+
raise SessionUnavailable("future Claude notification identity invalid")
|
|
268
|
+
decode_cursor(cursor, engine="claude", short=worker_id)
|
|
269
|
+
elif (kind == f"{engine}-observed-turn"
|
|
270
|
+
and selection in {"active", "last-settled"}):
|
|
271
|
+
from ccx.claude.delivery import valid_delivery_id
|
|
272
|
+
ref = operation.get("selectedTurnRef")
|
|
273
|
+
if (not isinstance(ref, str)
|
|
274
|
+
or not ref.startswith(f"ccx-turn:{engine}:v1:")
|
|
275
|
+
or len(ref) <= len(f"ccx-turn:{engine}:v1:")):
|
|
276
|
+
raise SessionUnavailable("standalone selected original turn identity invalid")
|
|
277
|
+
if operation.get("nativeProof") == "original-native-turn":
|
|
278
|
+
fields = {"kind", "selectionMode", "selectedTurnRef",
|
|
279
|
+
"nativeProof", "nativeSourceId"}
|
|
280
|
+
if engine == "codex":
|
|
281
|
+
fields.add("turnId")
|
|
282
|
+
if (set(operation) != fields
|
|
283
|
+
or not isinstance(operation.get("nativeSourceId"), str)
|
|
284
|
+
or not operation["nativeSourceId"]
|
|
285
|
+
or (engine == "codex" and operation.get("turnId") !=
|
|
286
|
+
ref[len("ccx-turn:codex:v1:"):])):
|
|
287
|
+
raise SessionUnavailable("receiptless native turn identity invalid")
|
|
288
|
+
elif (operation.get("nativeProof") is not None
|
|
289
|
+
or not valid_delivery_id(operation.get("deliveryId"))):
|
|
290
|
+
raise SessionUnavailable("standalone selected original delivery invalid")
|
|
291
|
+
else:
|
|
292
|
+
raise SessionUnavailable("standalone observation operation unsupported")
|
|
293
|
+
# The child is always an exactly owned original UUID; only the caller's
|
|
294
|
+
# return-address kind varies. No native parent ID is a worker target.
|
|
295
|
+
worker_owner = store.resolve(f"{engine}:{worker_id}")
|
|
296
|
+
if worker_owner.worker_id != worker_id:
|
|
297
|
+
raise SessionUnavailable("Notification worker is invalid; verify its handle")
|
|
298
|
+
# This is a read-only original source/binding gate. It is lifecycle
|
|
299
|
+
# independent and permits stopped workers, but never terminalOnly or
|
|
300
|
+
# pending provider bindings; no model native action is performed.
|
|
301
|
+
store.authorize_native_action(worker_owner.worker, "send")
|
|
302
|
+
if source == "compact" or (source == "notify"
|
|
303
|
+
and operation.get("kind") == f"{engine}-compact"):
|
|
304
|
+
receipt = _compact_effect_receipt(
|
|
305
|
+
store, worker_owner.worker, engine, worker_id, operation)
|
|
306
|
+
if source == "notify" and (receipt is None or receipt.get("state") not in {
|
|
307
|
+
"submitted-unknown", "accepted"}):
|
|
308
|
+
raise SessionUnavailable("standalone compact has no authenticated original effect")
|
|
309
|
+
if source == "input":
|
|
310
|
+
# Lazy import avoids a module-load cycle, and one original input
|
|
311
|
+
# digest layout is shared by Claude, Codex and the public protocol.
|
|
312
|
+
from ccx.ops.input import _input_intent_digest
|
|
313
|
+
record = store.record(worker_owner.worker)
|
|
314
|
+
native = record.get("native") or {}
|
|
315
|
+
source_id = operation["nativeSourceId"]
|
|
316
|
+
expected_source = (f"claude-session:{source_id}" if engine == "claude"
|
|
317
|
+
else store.codex_source(worker_owner.worker))
|
|
318
|
+
hashed_input = (engine == "claude"
|
|
319
|
+
and operation["kind"] in {"claude-input-answer", "claude-input-respond"})
|
|
320
|
+
expected_sha = _input_intent_digest(
|
|
321
|
+
worker_id=worker_id, engine=engine, generation=store.generation,
|
|
322
|
+
native_id=source_id, native_source=expected_source,
|
|
323
|
+
kind=(operation["kind"].removeprefix("claude-input-") if hashed_input else "decide"),
|
|
324
|
+
request_id=operation["gateId"], response_id=operation["responseId"],
|
|
325
|
+
**({"payload_sha256": operation["payloadSha256"]} if hashed_input else
|
|
326
|
+
{"payload": {"decision": operation["decision"],
|
|
327
|
+
"reason": operation["reason"] if engine == "claude" else None}}),
|
|
328
|
+
)
|
|
329
|
+
intent = store.read_receipt(worker_owner.worker, "answer", operation["responseId"])
|
|
330
|
+
if (native.get("sessionId" if engine == "claude" else "threadId") != source_id
|
|
331
|
+
or (engine == "codex" and native.get("threadSource") != expected_source)
|
|
332
|
+
or not isinstance(intent, dict)
|
|
333
|
+
or intent.get("workerId") != worker_id
|
|
334
|
+
or intent.get("engine") != engine
|
|
335
|
+
or intent.get("generation") != store.generation
|
|
336
|
+
or intent.get("family") != "answer"
|
|
337
|
+
or intent.get("action") != "answer"
|
|
338
|
+
or intent.get("operationId") != operation["responseId"]
|
|
339
|
+
or operation["intentSha256"] != expected_sha
|
|
340
|
+
or intent.get("intentSha256") != expected_sha):
|
|
341
|
+
raise SessionUnavailable("original input receipt/source/decision proof invalid")
|
|
342
|
+
if engine == "claude":
|
|
343
|
+
from ccx.claude.managed_turns import report_window_for_turn_ref
|
|
344
|
+
window, available = report_window_for_turn_ref(
|
|
345
|
+
worker_id, operation["selectedTurnRef"])
|
|
346
|
+
offset = window.get("startOffset") if isinstance(window, dict) else None
|
|
347
|
+
if not available or type(offset) is not int or offset < 0:
|
|
348
|
+
raise SessionUnavailable("original Claude input R lacks native start/window proof")
|
|
349
|
+
if source == "notify" and operation["kind"] == "claude-next-turn":
|
|
350
|
+
native = store.record(worker_owner.worker).get("native") or {}
|
|
351
|
+
if (not isinstance(operation.get("nativeSourceId"), str)
|
|
352
|
+
or not operation["nativeSourceId"]
|
|
353
|
+
or operation["nativeSourceId"] != native.get("sessionId")):
|
|
354
|
+
raise SessionUnavailable("future Claude native source changed")
|
|
355
|
+
ref = operation.get("selectedTurnRef")
|
|
356
|
+
if ref is not None and not _future_first_ref_matches(
|
|
357
|
+
worker_id, operation["armCursor"], ref):
|
|
358
|
+
raise SessionUnavailable("future Claude selected R is not first post-arm start")
|
|
359
|
+
elif (source == "notify" and operation["kind"] == f"{engine}-observed-turn"
|
|
360
|
+
and operation.get("nativeProof") == "original-native-turn"):
|
|
361
|
+
record = store.record(worker_owner.worker)
|
|
362
|
+
native = record.get("native") or {}
|
|
363
|
+
original = operation["nativeSourceId"]
|
|
364
|
+
if engine == "claude":
|
|
365
|
+
from ccx.claude.managed_turns import report_window_for_turn_ref
|
|
366
|
+
window, available = report_window_for_turn_ref(
|
|
367
|
+
worker_id, operation["selectedTurnRef"])
|
|
368
|
+
start = window.get("startOffset") if isinstance(window, dict) else None
|
|
369
|
+
valid = (native.get("sessionId") == original and available
|
|
370
|
+
and type(start) is int and start >= 0)
|
|
371
|
+
else:
|
|
372
|
+
valid = (native.get("threadId") == original and
|
|
373
|
+
native.get("threadSource") == store.codex_source(worker_owner.worker))
|
|
374
|
+
if valid:
|
|
375
|
+
valid = _codex_native_turn_evidence(
|
|
376
|
+
worker_owner.worker, record, original, operation["turnId"]) is not None
|
|
377
|
+
if not valid:
|
|
378
|
+
raise SessionUnavailable("receiptless selected original native source or turn proof changed")
|
|
379
|
+
elif source == "notify" and operation["kind"] == f"{engine}-observed-turn":
|
|
380
|
+
token = operation["deliveryId"]
|
|
381
|
+
if engine == "codex":
|
|
382
|
+
from ccx.codex import delivery as codex_delivery
|
|
383
|
+
receipt = codex_delivery.read_receipt(worker_owner.worker, token)
|
|
384
|
+
valid = (isinstance(receipt, dict) and receipt.get("state") == "accepted"
|
|
385
|
+
and codex_delivery.route(receipt) == "new_turn"
|
|
386
|
+
and codex_delivery.placement(receipt) == "new_turn"
|
|
387
|
+
and receipt.get("turnId") == operation.get("turnId")
|
|
388
|
+
and operation["selectedTurnRef"] == codex_delivery.turn_ref_for(receipt["turnId"])
|
|
389
|
+
and all(receipt.get(field) == operation.get(field)
|
|
390
|
+
for field in ("requestedMode", "promptSha256",
|
|
391
|
+
"callerMetadata", "clientUserMessageId")))
|
|
392
|
+
else:
|
|
393
|
+
from ccx.claude import delivery as claude_delivery
|
|
394
|
+
receipt = claude_delivery.read_receipt(worker_owner.worker, token)
|
|
395
|
+
valid = (isinstance(receipt, dict) and receipt.get("state") == "accepted"
|
|
396
|
+
and receipt.get("route") == "new_turn"
|
|
397
|
+
and receipt.get("placement") == "new_turn"
|
|
398
|
+
and receipt.get("turnRef") == operation["selectedTurnRef"]
|
|
399
|
+
and all(receipt.get(field) == operation.get(field)
|
|
400
|
+
for field in ("requestedMode", "promptSha256",
|
|
401
|
+
"promptByteCount", "callerMetadata", "cap")))
|
|
402
|
+
if not valid or receipt.get("requestedMode") not in {"new_turn_only", "auto"}:
|
|
403
|
+
raise SessionUnavailable("selected turn differs from its original opener receipt")
|
|
404
|
+
if engine == "claude":
|
|
405
|
+
native = store.record(worker_owner.worker).get("native") or {}
|
|
406
|
+
if (not isinstance(native.get("sessionId"), str)
|
|
407
|
+
or not native["sessionId"]):
|
|
408
|
+
raise SessionUnavailable("original Claude notification child native session is unbound")
|
|
409
|
+
return store, _original_notification_parent(store, parent, worker_id), worker_owner.worker
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _active_compact_selection(store, owner, record: dict[str, Any]) -> dict[str, Any] | None:
|
|
413
|
+
"""Observe only an already-active, original-effect-backed native compact."""
|
|
414
|
+
from ccx.errors import CcxError, OperationConflict
|
|
415
|
+
from ccx.store.root import GenerationError
|
|
416
|
+
|
|
417
|
+
engine, child, wid = owner.engine, owner.worker, owner.worker_id
|
|
418
|
+
if engine == "claude":
|
|
419
|
+
from ccx.ops.compact_ops import active_operation
|
|
420
|
+
try:
|
|
421
|
+
active = active_operation(wid)
|
|
422
|
+
except (OSError, ValueError, RuntimeError) as error:
|
|
423
|
+
raise SessionUnavailable(f"Claude compact journal unavailable: {error}") from error
|
|
424
|
+
if active is None:
|
|
425
|
+
return None
|
|
426
|
+
token = active.get("operation_id")
|
|
427
|
+
else:
|
|
428
|
+
active = record.get("activeCompact")
|
|
429
|
+
if active is None:
|
|
430
|
+
return None
|
|
431
|
+
token = active.get("operationId") if isinstance(active, dict) else None
|
|
432
|
+
native = record.get("native") or {}
|
|
433
|
+
native_id = native.get("sessionId" if engine == "claude" else "threadId")
|
|
434
|
+
if (not isinstance(token, str) or not 0 < len(token) <= 128 or "\x00" in token
|
|
435
|
+
or not isinstance(native_id, str) or not native_id):
|
|
436
|
+
raise OperationConflict("Compaction operation cannot be verified; inspect the worker")
|
|
437
|
+
operation = {"kind": f"{engine}-compact", "selectionMode": "active-compact",
|
|
438
|
+
"operationId": token, "nativeSourceId": native_id}
|
|
439
|
+
receipt = _compact_effect_receipt(store, child, engine, wid, operation)
|
|
440
|
+
if not isinstance(receipt, dict) or receipt.get("state") not in {
|
|
441
|
+
"submitted-unknown", "accepted"}:
|
|
442
|
+
raise OperationConflict("Compaction has no recorded submission; inspect its operation")
|
|
443
|
+
try:
|
|
444
|
+
if engine == "claude":
|
|
445
|
+
from ccx.ops.compact import compact_status
|
|
446
|
+
status = compact_status(child, operation_id=token, publish=False).get("operation_status")
|
|
447
|
+
valid = status in {"accepted", "pasted", "submitted"}
|
|
448
|
+
else:
|
|
449
|
+
from ccx.codex.ops import compact_observation
|
|
450
|
+
valid = compact_observation(child, token).get("status") == "submitted"
|
|
451
|
+
except (CcxError, GenerationError, OSError, ValueError):
|
|
452
|
+
valid = False
|
|
453
|
+
if not valid:
|
|
454
|
+
raise OperationConflict("Compaction outcome is unknown; inspect the operation")
|
|
455
|
+
# Freeze exactly the original ID, never a later compact or a model-turn R.
|
|
456
|
+
latest = store.record(child)
|
|
457
|
+
if engine == "claude":
|
|
458
|
+
from ccx.ops.compact_ops import active_operation
|
|
459
|
+
try:
|
|
460
|
+
current = active_operation(wid)
|
|
461
|
+
except (OSError, ValueError, RuntimeError) as error:
|
|
462
|
+
raise SessionUnavailable(f"Claude compact changed during selection: {error}") from error
|
|
463
|
+
same = isinstance(current, dict) and current.get("operation_id") == token
|
|
464
|
+
else:
|
|
465
|
+
current = latest.get("activeCompact")
|
|
466
|
+
same = isinstance(current, dict) and current.get("operationId") == token
|
|
467
|
+
if (not same or (latest.get("native") or {}).get(
|
|
468
|
+
"sessionId" if engine == "claude" else "threadId") != native_id):
|
|
469
|
+
raise OperationConflict("Compaction changed during selection; retry notification")
|
|
470
|
+
store.revalidate()
|
|
471
|
+
return operation
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _claude_prompt_opening(child: str) -> bool:
|
|
475
|
+
"""A Claude send is still being pasted (owned, not yet accepted)."""
|
|
476
|
+
from ccx.claude import delivery as claude_delivery
|
|
477
|
+
try:
|
|
478
|
+
return any(row.get("state") == "owned"
|
|
479
|
+
for row in claude_delivery.iter_receipts(child))
|
|
480
|
+
except (claude_delivery.ClaudeDeliveryError, OSError):
|
|
481
|
+
return False
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def select_current_turn(target: str) -> dict[str, Any]:
|
|
485
|
+
"""Read one original managed turn or positively proven stable no-turn."""
|
|
486
|
+
from ccx.claude import delivery as claude_delivery
|
|
487
|
+
from ccx.codex import delivery as codex_delivery
|
|
488
|
+
from ccx import constants
|
|
489
|
+
from ccx.errors import OperationConflict
|
|
490
|
+
from ccx.ops.wait import wait_worker
|
|
491
|
+
from ccx.store.root import require_ccx_active
|
|
492
|
+
|
|
493
|
+
store = require_ccx_active(constants.CCX_ROOT)
|
|
494
|
+
owner = store.resolve(target)
|
|
495
|
+
child = owner.worker
|
|
496
|
+
store.authorize_native_action(child, "send") # original binding check only
|
|
497
|
+
record = store.record(child)
|
|
498
|
+
compact = _active_compact_selection(store, owner, record)
|
|
499
|
+
if compact is not None:
|
|
500
|
+
return {"worker": child, "worker_id": owner.worker_id,
|
|
501
|
+
"engine": owner.engine, "name": record.get("name"),
|
|
502
|
+
"state": "compacting", "operation": compact}
|
|
503
|
+
# One bounded read: a zero-second call gives the live daemon barely .01s
|
|
504
|
+
# and may falsely label a healthy just-settled worker inconclusive.
|
|
505
|
+
snapshot = wait_worker(child, timeout=.75, expected_generation=store.generation)
|
|
506
|
+
# Right after `new/send --no-wait` the worker is idle while the prompt is
|
|
507
|
+
# still being delivered; wait briefly for that turn to open instead of
|
|
508
|
+
# selecting the previous one.
|
|
509
|
+
settle_by = time.monotonic() + 15
|
|
510
|
+
while (snapshot.get("state") == "idle" and snapshot.get("active_turn_ref") is None
|
|
511
|
+
and (snapshot.get("status") == "timeout"
|
|
512
|
+
or owner.engine == "claude" and _claude_prompt_opening(child))
|
|
513
|
+
and time.monotonic() < settle_by):
|
|
514
|
+
time.sleep(.25)
|
|
515
|
+
snapshot = wait_worker(child, timeout=.75, expected_generation=store.generation)
|
|
516
|
+
if (owner.engine == "claude" and snapshot.get("active_turn_ref") is None
|
|
517
|
+
and _claude_prompt_opening(child)):
|
|
518
|
+
raise SessionUnavailable(
|
|
519
|
+
"the last message to this worker is still being delivered; "
|
|
520
|
+
"retry notify once its turn has started")
|
|
521
|
+
state = snapshot.get("state")
|
|
522
|
+
active = snapshot.get("active_turn_ref")
|
|
523
|
+
live_idle = (snapshot.get("status") == "idle"
|
|
524
|
+
and snapshot.get("reason") == "worker is idle")
|
|
525
|
+
if owner.engine == "claude" and (active is not None or live_idle):
|
|
526
|
+
# The host can be compacting even before the journal is visible.
|
|
527
|
+
from ccx.rpc.rpc import _rpc
|
|
528
|
+
try:
|
|
529
|
+
status = _rpc(child, {"op": "status"}, timeout=.5)
|
|
530
|
+
except (OSError, ValueError, TimeoutError) as error:
|
|
531
|
+
raise SessionUnavailable(f"Claude compact status read unavailable: {error}") from error
|
|
532
|
+
if (status.get("ok") is not True or status.get("compacting") is not False
|
|
533
|
+
or status.get("compactJournalError")):
|
|
534
|
+
raise OperationConflict("Claude compact state unavailable or active")
|
|
535
|
+
if status.get("activeTurnRef") != active:
|
|
536
|
+
raise SessionUnavailable("original Claude active turn changed while selecting")
|
|
537
|
+
if state in {"busy", "waiting"}:
|
|
538
|
+
if not isinstance(active, str) or not active:
|
|
539
|
+
raise SessionUnavailable("busy/waiting worker has no proven original active turn")
|
|
540
|
+
ref, selection = active, "active"
|
|
541
|
+
elif state in {"idle", "idle-killed", "dead", "crashed", "budget-stopped"}:
|
|
542
|
+
if state == "idle" and active is not None:
|
|
543
|
+
raise SessionUnavailable("idle worker retains an untrusted active turn")
|
|
544
|
+
if state != "idle" and (record.get("lifecycle") or {}).get("state") != state:
|
|
545
|
+
raise SessionUnavailable("terminal worker state differs from durable source")
|
|
546
|
+
settled = snapshot.get("last_settled_turn")
|
|
547
|
+
ref = settled.get("turn_ref") if isinstance(settled, dict) else None
|
|
548
|
+
selection = "last-settled"
|
|
549
|
+
else:
|
|
550
|
+
raise SessionUnavailable("worker state cannot select an original model turn")
|
|
551
|
+
|
|
552
|
+
receipt = None
|
|
553
|
+
if isinstance(ref, str) and ref.startswith(f"ccx-turn:{owner.engine}:v1:"):
|
|
554
|
+
if owner.engine == "codex":
|
|
555
|
+
turn_id = ref[len("ccx-turn:codex:v1:"):]
|
|
556
|
+
try:
|
|
557
|
+
receipt = codex_delivery.receipt_for_turn(child, turn_id)
|
|
558
|
+
except (codex_delivery.DeliveryReceiptError, OSError) as error:
|
|
559
|
+
raise SessionUnavailable(f"Codex original opener receipt untrusted: {error}") from error
|
|
560
|
+
if (not isinstance(receipt, dict) or receipt.get("state") != "accepted"
|
|
561
|
+
or codex_delivery.route(receipt) != "new_turn"
|
|
562
|
+
or codex_delivery.placement(receipt) != "new_turn"
|
|
563
|
+
or receipt.get("turnId") != turn_id):
|
|
564
|
+
receipt = None
|
|
565
|
+
else:
|
|
566
|
+
# Claude's existing receipt inventory is best effort. Match only
|
|
567
|
+
# unique eligible valid openers, then strictly reread that token.
|
|
568
|
+
try:
|
|
569
|
+
candidates = [row for row in claude_delivery.iter_receipts(child)
|
|
570
|
+
if row.get("state") == "accepted"
|
|
571
|
+
and row.get("route") == "new_turn"
|
|
572
|
+
and row.get("placement") == "new_turn"
|
|
573
|
+
and row.get("turnRef") == ref]
|
|
574
|
+
if len(candidates) == 1:
|
|
575
|
+
receipt = claude_delivery.read_receipt(child, candidates[0]["deliveryId"])
|
|
576
|
+
except (claude_delivery.ClaudeDeliveryError, OSError) as error:
|
|
577
|
+
raise SessionUnavailable(f"Claude original opener receipt untrusted: {error}") from error
|
|
578
|
+
if receipt is not None:
|
|
579
|
+
if (receipt.get("turnRef") != ref
|
|
580
|
+
or receipt.get("state") != "accepted"
|
|
581
|
+
or receipt.get("route") != "new_turn"
|
|
582
|
+
or receipt.get("placement") != "new_turn"):
|
|
583
|
+
receipt = None
|
|
584
|
+
if receipt is None:
|
|
585
|
+
native = record.get("native") or {}
|
|
586
|
+
original = native.get("sessionId" if owner.engine == "claude" else "threadId")
|
|
587
|
+
proven = False
|
|
588
|
+
if isinstance(ref, str) and isinstance(original, str) and original:
|
|
589
|
+
if owner.engine == "claude":
|
|
590
|
+
from ccx.claude.managed_turns import report_window_for_turn_ref
|
|
591
|
+
window, available = report_window_for_turn_ref(owner.worker_id, ref)
|
|
592
|
+
start = window.get("startOffset") if isinstance(window, dict) else None
|
|
593
|
+
proven = (available and type(start) is int and start >= 0)
|
|
594
|
+
elif native.get("threadSource") == store.codex_source(owner.worker):
|
|
595
|
+
proven = _codex_native_turn_evidence(child, record, original,
|
|
596
|
+
ref[len("ccx-turn:codex:v1:"):]) is not None
|
|
597
|
+
if proven and state in {"busy", "waiting"}:
|
|
598
|
+
from ccx.codex import runtime as codex_runtime
|
|
599
|
+
served = codex_runtime.observe_worker(owner.worker_id, timeout=.5)
|
|
600
|
+
reply = served.get("reply") or {}
|
|
601
|
+
proven = (served.get("daemon_served") is True and
|
|
602
|
+
reply.get("ok") is True and reply.get("loaded") is True
|
|
603
|
+
and reply.get("activeTurnId") == ref[len("ccx-turn:codex:v1:"):])
|
|
604
|
+
if proven:
|
|
605
|
+
operation = {"kind": f"{owner.engine}-observed-turn",
|
|
606
|
+
"selectionMode": selection, "selectedTurnRef": ref,
|
|
607
|
+
"nativeProof": "original-native-turn", "nativeSourceId": original}
|
|
608
|
+
if owner.engine == "codex":
|
|
609
|
+
operation["turnId"] = ref[len("ccx-turn:codex:v1:"):]
|
|
610
|
+
else:
|
|
611
|
+
if state in {"busy", "waiting"}:
|
|
612
|
+
raise SessionUnavailable("current original turn lacks an eligible opening receipt or native proof")
|
|
613
|
+
if state == "idle" and not live_idle:
|
|
614
|
+
raise SessionUnavailable(
|
|
615
|
+
"Cannot notify yet: the worker's idle state could not be confirmed. "
|
|
616
|
+
"Check its status and retry."
|
|
617
|
+
)
|
|
618
|
+
operation = {"kind": f"{owner.engine}-stable",
|
|
619
|
+
"selectionMode": "status-only", "selectedTurnRef": None,
|
|
620
|
+
"stableState": state}
|
|
621
|
+
else:
|
|
622
|
+
operation = {"kind": f"{owner.engine}-observed-turn",
|
|
623
|
+
"selectionMode": selection, "selectedTurnRef": ref,
|
|
624
|
+
"deliveryId": receipt["deliveryId"],
|
|
625
|
+
"requestedMode": receipt.get("requestedMode"),
|
|
626
|
+
"promptSha256": receipt.get("promptSha256"),
|
|
627
|
+
"callerMetadata": receipt.get("callerMetadata")}
|
|
628
|
+
if owner.engine == "codex":
|
|
629
|
+
operation.update(turnId=receipt["turnId"],
|
|
630
|
+
clientUserMessageId=receipt.get("clientUserMessageId"))
|
|
631
|
+
else:
|
|
632
|
+
operation.update(promptByteCount=receipt.get("promptByteCount"),
|
|
633
|
+
cap=receipt.get("cap"))
|
|
634
|
+
store.revalidate()
|
|
635
|
+
return {"worker": child, "worker_id": owner.worker_id,
|
|
636
|
+
"engine": owner.engine, "name": record.get("name"),
|
|
637
|
+
"state": state, "operation": operation}
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def select_future_turn(target: str) -> dict[str, Any]:
|
|
641
|
+
"""Arm one future Claude turn only from a positively served idle host."""
|
|
642
|
+
from ccx import constants
|
|
643
|
+
from ccx.ops.compact_ops import active_operation
|
|
644
|
+
from ccx.ops.event_feed import read_page, decode_cursor
|
|
645
|
+
from ccx.store.paths import events_ndjson_path
|
|
646
|
+
from ccx.ops.wait import wait_worker
|
|
647
|
+
from ccx.rpc.rpc import _rpc
|
|
648
|
+
from ccx.store.root import require_ccx_active
|
|
649
|
+
|
|
650
|
+
store = require_ccx_active(constants.CCX_ROOT)
|
|
651
|
+
owner = store.resolve(target)
|
|
652
|
+
if owner.engine != "claude":
|
|
653
|
+
raise SessionUnavailable("notify --next-turn requires managed Claude")
|
|
654
|
+
store.authorize_native_action(owner.worker, "send") # binding only
|
|
655
|
+
record = store.record(owner.worker)
|
|
656
|
+
native = record.get("native") or {}
|
|
657
|
+
sid = native.get("sessionId")
|
|
658
|
+
if (not isinstance(sid, str) or not sid
|
|
659
|
+
or (record.get("lifecycle") or {}).get("state") != "idle"):
|
|
660
|
+
raise SessionUnavailable("notify --next-turn requires a live idle managed worker")
|
|
661
|
+
if active_operation(owner.worker_id) is not None:
|
|
662
|
+
raise SessionUnavailable("notify --next-turn requires a live idle managed worker (compact active)")
|
|
663
|
+
if not events_ndjson_path(owner.worker_id).is_file():
|
|
664
|
+
raise SessionUnavailable("Cannot find an upcoming Claude turn; retry notification")
|
|
665
|
+
cursor = read_page("claude", owner.worker_id, tail=0)["next_cursor"]
|
|
666
|
+
if decode_cursor(cursor, engine="claude", short=owner.worker_id).get("i") is None:
|
|
667
|
+
raise SessionUnavailable("future Claude event baseline not retained")
|
|
668
|
+
snapshot = wait_worker(owner.worker, timeout=.75,
|
|
669
|
+
expected_generation=store.generation)
|
|
670
|
+
try:
|
|
671
|
+
live = _rpc(owner.worker, {"op": "status"}, timeout=.5)
|
|
672
|
+
except (OSError, ValueError, TimeoutError) as error:
|
|
673
|
+
raise SessionUnavailable(f"future Claude original host unavailable: {error}") from error
|
|
674
|
+
if (snapshot.get("worker") != owner.worker
|
|
675
|
+
or snapshot.get("state") != "idle"
|
|
676
|
+
or snapshot.get("status") != "idle"
|
|
677
|
+
or snapshot.get("reason") != "worker is idle"
|
|
678
|
+
or snapshot.get("active_turn_ref") is not None
|
|
679
|
+
or live.get("ok") is not True or live.get("state") != "idle"
|
|
680
|
+
or live.get("activeTurnRef") is not None
|
|
681
|
+
or live.get("waitingFor") is not None
|
|
682
|
+
or live.get("compacting") is not False or live.get("compactJournalError")
|
|
683
|
+
or active_operation(owner.worker_id) is not None
|
|
684
|
+
or (store.record(owner.worker).get("native") or {}).get("sessionId") != sid):
|
|
685
|
+
raise SessionUnavailable("notify --next-turn requires a live idle managed worker")
|
|
686
|
+
changed = read_page("claude", owner.worker_id, after=cursor, native=True)
|
|
687
|
+
if changed["gap"] or changed["has_more"] or changed["events"]:
|
|
688
|
+
raise SessionUnavailable("future Claude baseline changed during idle admission")
|
|
689
|
+
store.revalidate()
|
|
690
|
+
return {"worker": owner.worker, "worker_id": owner.worker_id,
|
|
691
|
+
"engine": "claude", "name": record.get("name"), "state": "idle",
|
|
692
|
+
"operation": {"kind": "claude-next-turn", "selectionMode": "future",
|
|
693
|
+
"nativeSourceId": sid, "armCursor": cursor,
|
|
694
|
+
"selectedTurnRef": None}}
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _future_events(worker_id: str, cursor: str | None) -> list[dict] | None:
|
|
698
|
+
"""One existing physical-order event-feed walk, or None on a cursor gap."""
|
|
699
|
+
from ccx.ops.event_feed import read_page
|
|
700
|
+
rows = []
|
|
701
|
+
while True:
|
|
702
|
+
page = read_page("claude", worker_id, after=cursor, native=True,
|
|
703
|
+
limit=1000)
|
|
704
|
+
if page["gap"]:
|
|
705
|
+
return None
|
|
706
|
+
rows.extend(page["events"])
|
|
707
|
+
if not page["has_more"]:
|
|
708
|
+
return rows
|
|
709
|
+
cursor = page["next_cursor"]
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _future_ref_before(worker_id: str, cursor: str, ref: str) -> bool:
|
|
713
|
+
"""Missing baseline or older evidence disqualifies a first future start."""
|
|
714
|
+
from ccx.ops.event_feed import decode_cursor, read_page
|
|
715
|
+
at = decode_cursor(cursor, engine="claude", short=worker_id)
|
|
716
|
+
if at["p"] is None and at["q"] == 0:
|
|
717
|
+
return False
|
|
718
|
+
next_cursor = None
|
|
719
|
+
while True:
|
|
720
|
+
page = read_page("claude", worker_id, after=next_cursor,
|
|
721
|
+
native=True, limit=1000)
|
|
722
|
+
if page["gap"]:
|
|
723
|
+
return True
|
|
724
|
+
for row in page["events"]:
|
|
725
|
+
if row.get("turn_ref") == ref:
|
|
726
|
+
return True
|
|
727
|
+
if row.get("epoch") == at["p"] and row.get("seq") == at["q"]:
|
|
728
|
+
return False
|
|
729
|
+
if not page["has_more"]:
|
|
730
|
+
return True # baseline rotated out, never adopt later old R
|
|
731
|
+
next_cursor = page["next_cursor"]
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _future_first_ref_matches(worker_id: str, cursor: str, ref: str) -> bool:
|
|
735
|
+
"""Select the first eligible future turn, so later starts cannot replace it."""
|
|
736
|
+
events = _future_events(worker_id, cursor)
|
|
737
|
+
if events is None:
|
|
738
|
+
return False
|
|
739
|
+
seen: set[str] = set()
|
|
740
|
+
for row in events:
|
|
741
|
+
candidate = row.get("turn_ref")
|
|
742
|
+
if not isinstance(candidate, str) or not candidate.startswith("ccx-turn:claude:v1:"):
|
|
743
|
+
continue
|
|
744
|
+
if candidate in seen:
|
|
745
|
+
continue
|
|
746
|
+
seen.add(candidate)
|
|
747
|
+
if (row.get("type") in {"turn_start", "turn_identity_recovered"}
|
|
748
|
+
and not _future_ref_before(worker_id, cursor, candidate)):
|
|
749
|
+
return candidate == ref
|
|
750
|
+
return False
|