ccx-native 0.0.1 → 0.2.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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -4
  3. package/bin/ccx +55 -2
  4. package/ccx/__init__.py +167 -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 +807 -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 +269 -0
  16. package/ccx/claude/host/session.py +5244 -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 +2535 -0
  26. package/ccx/claude/usage.py +796 -0
  27. package/ccx/claude/usage_control.py +204 -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/models.py +53 -0
  35. package/ccx/cli/commands/read.py +1104 -0
  36. package/ccx/cli/commands/turns.py +1081 -0
  37. package/ccx/cli/controller.py +757 -0
  38. package/ccx/cli/main.py +302 -0
  39. package/ccx/cli/parser.py +1439 -0
  40. package/ccx/cli/skills.py +215 -0
  41. package/ccx/cli/statusline.py +160 -0
  42. package/ccx/client.py +1904 -0
  43. package/ccx/codex/__init__.py +0 -0
  44. package/ccx/codex/attach_tty.py +267 -0
  45. package/ccx/codex/client.py +738 -0
  46. package/ccx/codex/completion.py +121 -0
  47. package/ccx/codex/control.py +288 -0
  48. package/ccx/codex/daemon/__init__.py +0 -0
  49. package/ccx/codex/daemon/__main__.py +4 -0
  50. package/ccx/codex/daemon/daemon.py +1558 -0
  51. package/ccx/codex/daemon/models.py +140 -0
  52. package/ccx/codex/daemon/native.py +1590 -0
  53. package/ccx/codex/daemon/release.py +367 -0
  54. package/ccx/codex/daemon/worker.py +2424 -0
  55. package/ccx/codex/delivery.py +758 -0
  56. package/ccx/codex/delivery_recovery.py +232 -0
  57. package/ccx/codex/evidence.py +827 -0
  58. package/ccx/codex/history.py +158 -0
  59. package/ccx/codex/history_store.py +131 -0
  60. package/ccx/codex/notify_history.py +140 -0
  61. package/ccx/codex/ops.py +666 -0
  62. package/ccx/codex/paths.py +70 -0
  63. package/ccx/codex/protocol.py +805 -0
  64. package/ccx/codex/provenance.py +105 -0
  65. package/ccx/codex/read.py +29 -0
  66. package/ccx/codex/records.py +51 -0
  67. package/ccx/codex/reports.py +177 -0
  68. package/ccx/codex/runtime.py +1454 -0
  69. package/ccx/codex/settle.py +363 -0
  70. package/ccx/codex/telemetry.py +792 -0
  71. package/ccx/codex/types.py +659 -0
  72. package/ccx/codex/worker_environment.py +25 -0
  73. package/ccx/constants.py +288 -0
  74. package/ccx/controllers/__init__.py +0 -0
  75. package/ccx/controllers/controller.py +1449 -0
  76. package/ccx/controllers/live_vars.py +1706 -0
  77. package/ccx/controllers/live_vars_runner.py +216 -0
  78. package/ccx/controllers/notify.py +213 -0
  79. package/ccx/controllers/orchestrator.py +193 -0
  80. package/ccx/controllers/policy.py +57 -0
  81. package/ccx/controllers/store.py +681 -0
  82. package/ccx/errors.py +634 -0
  83. package/ccx/models.py +225 -0
  84. package/ccx/notify/__init__.py +0 -0
  85. package/ccx/notify/__main__.py +5 -0
  86. package/ccx/notify/client.py +116 -0
  87. package/ccx/notify/daemon.py +480 -0
  88. package/ccx/notify/deliver.py +304 -0
  89. package/ccx/notify/paths.py +51 -0
  90. package/ccx/notify/registry.py +649 -0
  91. package/ccx/notify/select.py +750 -0
  92. package/ccx/notify/settle.py +684 -0
  93. package/ccx/ops/__init__.py +0 -0
  94. package/ccx/ops/background.py +115 -0
  95. package/ccx/ops/cancel.py +296 -0
  96. package/ccx/ops/codex_import.py +96 -0
  97. package/ccx/ops/common.py +397 -0
  98. package/ccx/ops/compact.py +337 -0
  99. package/ccx/ops/compact_ops.py +550 -0
  100. package/ccx/ops/context_events.py +48 -0
  101. package/ccx/ops/conversation.py +1593 -0
  102. package/ccx/ops/conversation_paging.py +1339 -0
  103. package/ccx/ops/create.py +56 -0
  104. package/ccx/ops/creation.py +952 -0
  105. package/ccx/ops/creation_steps.py +575 -0
  106. package/ccx/ops/event_feed.py +710 -0
  107. package/ccx/ops/forking.py +711 -0
  108. package/ccx/ops/history.py +1041 -0
  109. package/ccx/ops/input.py +556 -0
  110. package/ccx/ops/maintenance.py +295 -0
  111. package/ccx/ops/report.py +773 -0
  112. package/ccx/ops/semantics.py +490 -0
  113. package/ccx/ops/send.py +961 -0
  114. package/ccx/ops/steer_ops.py +867 -0
  115. package/ccx/ops/terminate.py +383 -0
  116. package/ccx/ops/turn_reports.py +348 -0
  117. package/ccx/ops/wait.py +762 -0
  118. package/ccx/ops/worker_events.py +640 -0
  119. package/ccx/rpc/__init__.py +0 -0
  120. package/ccx/rpc/local_rpc.py +1602 -0
  121. package/ccx/rpc/rpc.py +559 -0
  122. package/ccx/rpc/serve.py +374 -0
  123. package/ccx/rpc/transport.py +409 -0
  124. package/ccx/rpc/view.py +125 -0
  125. package/ccx/store/__init__.py +0 -0
  126. package/ccx/store/discovery.py +170 -0
  127. package/ccx/store/identity.py +105 -0
  128. package/ccx/store/journal_io.py +79 -0
  129. package/ccx/store/liveness.py +194 -0
  130. package/ccx/store/paths.py +250 -0
  131. package/ccx/store/record_cache.py +40 -0
  132. package/ccx/store/records.py +309 -0
  133. package/ccx/store/root.py +789 -0
  134. package/ccx/store/runtime_presence.py +137 -0
  135. package/ccx/store/store.py +2332 -0
  136. package/ccx/store/worker_state.py +86 -0
  137. package/ccx/system/__init__.py +0 -0
  138. package/ccx/system/bounded.py +296 -0
  139. package/ccx/system/cwd_paths.py +25 -0
  140. package/ccx/system/engine_binary.py +59 -0
  141. package/ccx/system/executable.py +50 -0
  142. package/ccx/system/oslayer.py +667 -0
  143. package/ccx/system/pipes.py +46 -0
  144. package/ccx/system/private_json.py +54 -0
  145. package/ccx/system/sanitize.py +43 -0
  146. package/ccx/types.py +809 -0
  147. package/ccx/version.py +29 -0
  148. package/ccx/workers.py +2093 -0
  149. package/ccx.py +9 -0
  150. package/package.json +39 -7
  151. package/pyproject.toml +87 -0
  152. package/skills/ccx/SKILL.md +166 -0
  153. package/skills/ccx/references/controllers.md +244 -0
  154. package/skills/ccx/references/history.md +50 -0
  155. package/skills/ccx/references/troubleshooting.md +94 -0
  156. package/skills/ccx/references/variables.md +62 -0
@@ -0,0 +1,1602 @@
1
+ """Local JSON protocol validation and dispatch.
2
+
3
+ Pure validation and capabilities precede the active-generation gate.
4
+ One-shot and persistent transports share the same full-UUID dispatcher."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import math
10
+ import sys
11
+ import time
12
+ from typing import Any, Callable
13
+
14
+ from ccx.ops.report import account_usage_read, conversation_page, read_transcript, report_worker, telemetry_worker
15
+ from ccx.ops.input import answer_input, decide_input, respond_input
16
+ from ccx.ops.cancel import cancel_turn
17
+ from ccx.ops.compact import compact_worker, compact_status
18
+ from ccx.ops.create import fork_worker
19
+ from ccx.ops.wait import inspect_notification, park_worker, reconcile_turn, wait_worker
20
+ from ccx.ops.common import recorded_inspector
21
+ from ccx.ops.send import send_turn
22
+ from ccx.constants import DEFAULT_IDLE_TIMEOUT_S
23
+ from ccx.errors import (
24
+ CcxError,
25
+ BudgetStopped,
26
+ CapabilityUnsupported,
27
+ CompactFailed,
28
+ DeliveryRejected,
29
+ InvalidRequest,
30
+ OperationConflict,
31
+ CursorInvalidated,
32
+ CreatePartialFailure,
33
+ OperationTimeout,
34
+ ProviderProtocolError,
35
+ SendIndeterminate,
36
+ SessionNotFound,
37
+ SessionUnavailable,
38
+ SessionWaiting,
39
+ TurnIncomplete,
40
+ WorkerApiError,
41
+ WorkerRateLimited,
42
+ )
43
+ from ccx.store.worker_state import normalize_native_state
44
+ from ccx.version import ccx_version
45
+
46
+ PROTOCOL_VERSION = 2
47
+ SUPPORTED_PROTOCOL_VERSIONS = (2,)
48
+ MAX_MESSAGE_BYTES = 4 * 1024 * 1024
49
+ # Budget a wait sets aside for the snapshot it was asked to include.
50
+ _INCLUDE_RESERVE_S = 1.0
51
+ _DEFAULT_REPORT_MAX_CHARS = 64 * 1024
52
+ # Re-read cadence of a reconcile held while its receipt is pending.
53
+ _HOLD_POLL_S = 0.25
54
+ MAX_REQUEST_ID_CHARS = 256
55
+ # Budget of a request that sends no `timeout_ms`, in either transport.
56
+ DEFAULT_TIMEOUT_S = 90.0
57
+
58
+ # Answered by the `ccx rpc --serve` parent itself; one-shot `ccx rpc` has no
59
+ # other request in flight and reports it as an unsupported operation.
60
+ CANCEL_OP = "request.cancel"
61
+
62
+ _ALLOWED_PARAMS: dict[str, frozenset[str]] = {
63
+ "system.info": frozenset(),
64
+ "worker.create": frozenset({
65
+ "worker_id",
66
+ "name",
67
+ "cwd",
68
+ "engine",
69
+ "model",
70
+ "effort",
71
+ "idle_timeout",
72
+ "permissions",
73
+ "sandbox",
74
+ "approval_policy",
75
+ }),
76
+ "worker.fork": frozenset({"target", "worker_id", "name", "model", "effort"}),
77
+ "worker.inspect": frozenset({"target"}),
78
+ "worker.wait": frozenset({
79
+ "target", "next_turn", "after_turn", "known_input", "include_report", "report_max_chars",
80
+ }),
81
+ "worker.kill": frozenset({"target"}),
82
+ "worker.compact": frozenset({"target", "operation_id"}),
83
+ "worker.compact.status": frozenset({"target", "operation_id"}),
84
+ "turn.send": frozenset({
85
+ "target",
86
+ "text",
87
+ "delivery_id",
88
+ "mode",
89
+ "notify_parent",
90
+ "caller_metadata",
91
+ }),
92
+ "turn.reconcile": frozenset({
93
+ "target", "delivery_id", "turn_id", "hold_while_pending"
94
+ }),
95
+ "turn.cancel": frozenset({
96
+ "target", "cancellation_id", "turn_ref"
97
+ }),
98
+ "input.answer": frozenset({
99
+ "target", "request_id", "response_id", "answers", "notify_parent"
100
+ }),
101
+ "input.decide": frozenset({
102
+ "target", "request_id", "response_id", "decision", "reason"
103
+ }),
104
+ "input.respond": frozenset({
105
+ "target", "request_id", "response_id", "action", "content"
106
+ }),
107
+ "notification.inspect": frozenset({"event_id"}),
108
+ "worker.report": frozenset({"target", "max_chars", "turn_ref"}),
109
+ "worker.telemetry": frozenset({"target", "after_cursor", "limit"}),
110
+ "worker.events": frozenset({
111
+ "target", "after_cursor", "tail", "limit", "kinds", "wait_ms",
112
+ }),
113
+ "account.usage.read": frozenset({"engine"}),
114
+ "transcript.read": frozenset({
115
+ "locator",
116
+ "cursor",
117
+ "limit",
118
+ "max_tool_result_bytes",
119
+ }),
120
+ "conversation.page": frozenset({
121
+ "target",
122
+ "cursor",
123
+ "source_record_target",
124
+ "max_tool_result_bytes",
125
+ }),
126
+ CANCEL_OP: frozenset({"cancel_request_id"}),
127
+ }
128
+
129
+
130
+ def _error(
131
+ request_id: str | None,
132
+ code: str,
133
+ message: str,
134
+ *,
135
+ details: dict[str, Any] | None = None,
136
+ ) -> dict[str, Any]:
137
+ # One code per existing CLI exit category; the original precise reason
138
+ # remains in details without adding a second public error vocabulary.
139
+ legacy = {
140
+ "unsupported_protocol_version": "invalid_request",
141
+ "unsupported_operation": "invalid_request",
142
+ "unsupported_capability": "unsupported",
143
+ "state_unavailable": "unavailable",
144
+ "runtime_unavailable": "unavailable",
145
+ "partial_failure": "unavailable",
146
+ "local_refused": "refused",
147
+ "native_rejected": "refused",
148
+ "busy": "refused",
149
+ "conflict": "refused",
150
+ "cursor_invalidated": "refused",
151
+ "definitive_rejection": "refused",
152
+ "approval_waiting": "waiting",
153
+ "reconcile_required": "incomplete",
154
+ "timeout": "incomplete",
155
+ "provider_protocol_error": "invalid_request",
156
+ "response_too_large": "invalid_request",
157
+ "internal_error": "invalid_request",
158
+ "ccx_error": "invalid_request",
159
+ }
160
+ public_code = legacy.get(code, code)
161
+ public_details = dict(details or {})
162
+ if public_code != code:
163
+ public_details.setdefault("reason", code)
164
+ if public_code == "unsupported":
165
+ public_details.setdefault("engine", "unknown")
166
+ public_details["operation"] = (
167
+ public_details.get("operation")
168
+ or public_details.pop("capability", "unknown")
169
+ )
170
+ return {
171
+ "protocol_version": PROTOCOL_VERSION,
172
+ "request_id": request_id,
173
+ "ok": False,
174
+ "error": {
175
+ "code": public_code,
176
+ "message": str(message)[:4000],
177
+ "details": public_details,
178
+ },
179
+ }
180
+
181
+
182
+ def _success(request_id: str, result: dict[str, Any]) -> dict[str, Any]:
183
+ return {
184
+ "protocol_version": PROTOCOL_VERSION,
185
+ "request_id": request_id,
186
+ "ok": True,
187
+ "result": result,
188
+ }
189
+
190
+
191
+ def _request_id(value: Any) -> str:
192
+ if (
193
+ not isinstance(value, str)
194
+ or not value
195
+ or len(value) > MAX_REQUEST_ID_CHARS
196
+ or "\x00" in value
197
+ ):
198
+ raise InvalidRequest(
199
+ f"request_id must be a non-empty string of at most "
200
+ f"{MAX_REQUEST_ID_CHARS} characters"
201
+ )
202
+ return value
203
+
204
+
205
+ def _params(request: dict[str, Any]) -> dict[str, Any]:
206
+ value = request.get("params")
207
+ if value is None:
208
+ return {}
209
+ if not isinstance(value, dict):
210
+ raise InvalidRequest("params must be a JSON object")
211
+ return value
212
+
213
+
214
+ def _validate_operation_params(
215
+ op: str,
216
+ params: dict[str, Any],
217
+ ) -> None:
218
+ allowed = _ALLOWED_PARAMS.get(op)
219
+ if allowed is None:
220
+ return
221
+ unknown = sorted(set(params) - allowed)
222
+ if not unknown:
223
+ return
224
+ allowed_names = sorted(allowed)
225
+ raise InvalidRequest(
226
+ f"{op} received unsupported params {unknown}; "
227
+ f"allowed params are {allowed_names}"
228
+ )
229
+
230
+
231
+ def _timeout_seconds(request: dict[str, Any], default: float) -> float:
232
+ raw = request.get("timeout_ms")
233
+ if raw is None:
234
+ return default
235
+ if not isinstance(raw, (int, float)) or isinstance(raw, bool):
236
+ raise InvalidRequest("timeout_ms must be a positive number")
237
+ try:
238
+ timeout_ms = float(raw)
239
+ except (OverflowError, ValueError) as error:
240
+ raise InvalidRequest("timeout_ms must be a finite positive number") from error
241
+ if not math.isfinite(timeout_ms) or timeout_ms <= 0:
242
+ raise InvalidRequest("timeout_ms must be a finite positive number")
243
+ return timeout_ms / 1000.0
244
+
245
+
246
+ def _required_text(params: dict[str, Any], key: str) -> str:
247
+ value = params.get(key)
248
+ if not isinstance(value, str) or not value:
249
+ raise InvalidRequest(f"{key} is required")
250
+ return value
251
+
252
+
253
+ def _optional_text(
254
+ params: dict[str, Any],
255
+ key: str,
256
+ default: str | None = None,
257
+ *,
258
+ allow_null: bool = True,
259
+ ) -> str | None:
260
+ if key not in params:
261
+ return default
262
+ value = params.get(key)
263
+ if value is None and allow_null:
264
+ return default
265
+ if not isinstance(value, str) or not value:
266
+ raise InvalidRequest(f"{key} must be a non-empty string")
267
+ return value
268
+
269
+
270
+ def _optional_nullable_int(
271
+ params: dict[str, Any], key: str, default: int | None
272
+ ) -> int | None:
273
+ if key not in params:
274
+ return default
275
+ value = params.get(key)
276
+ if value is None:
277
+ return None
278
+ if not isinstance(value, int) or isinstance(value, bool):
279
+ raise InvalidRequest(f"{key} must be an integer or null")
280
+ return value
281
+
282
+
283
+ def _optional_int(
284
+ params: dict[str, Any], key: str, default: int
285
+ ) -> int:
286
+ value = params.get(key, default)
287
+ if not isinstance(value, int) or isinstance(value, bool):
288
+ raise InvalidRequest(f"{key} must be an integer")
289
+ return value
290
+
291
+
292
+ def _optional_bool(
293
+ params: dict[str, Any], key: str, default: bool = False
294
+ ) -> bool:
295
+ value = params.get(key, default)
296
+ if not isinstance(value, bool):
297
+ raise InvalidRequest(f"{key} must be a boolean")
298
+ return value
299
+
300
+
301
+ def _optional_object(
302
+ params: dict[str, Any], key: str
303
+ ) -> dict[str, Any] | None:
304
+ if key not in params:
305
+ return None
306
+ value = params.get(key)
307
+ if not isinstance(value, dict):
308
+ raise InvalidRequest(f"{key} must be a JSON object")
309
+ return value
310
+
311
+
312
+ def _rpc_handle(value: Any, target: str | None = None) -> str | None:
313
+ """One qualified identity from an original operation's return value."""
314
+ if isinstance(value, str):
315
+ return value
316
+ if isinstance(value, dict):
317
+ candidate = value.get("worker")
318
+ if isinstance(candidate, str):
319
+ return candidate
320
+ return target
321
+
322
+
323
+ def _rpc_worker(store, source: dict, *, context: bool = True,
324
+ settled: dict | None = None) -> dict:
325
+ """Build V2 recorded-only worker without a new native observation."""
326
+ from ccx.rpc.view import context_view, project_worker_view
327
+ handle = _rpc_handle(source)
328
+ if not isinstance(handle, str) or ":" not in handle:
329
+ raise InvalidRequest("Request has no valid worker handle; include a target")
330
+ try:
331
+ record = store.record(handle)
332
+ except Exception:
333
+ # An accepted original native effect must not become a false refusal
334
+ # merely because its subsequent recorded view was unavailable.
335
+ state = source.get("state")
336
+ state = state.get("state") if isinstance(state, dict) else None
337
+ native = source.get("native") or {}
338
+ engine = handle.split(":", 1)[0]
339
+ record = {
340
+ "lifecycle": {"state": state},
341
+ "native": {"sessionId": native.get("session_id"),
342
+ "threadId": native.get("thread_id")},
343
+ "name": source.get("name"), "cwd": source.get("cwd"),
344
+ "createArgs": {"model": source.get("model"),
345
+ "effort": source.get("effort")},
346
+ "createdAt": source.get("created_at"),
347
+ "lastTurnAt": source.get("last_turn_at"),
348
+ "stats": {"turns": source.get("managed_turns")},
349
+ }
350
+ context_data = None
351
+ if context:
352
+ from ccx.cli.commands.read import _status_context_for_record
353
+ try:
354
+ context_data = context_view(_status_context_for_record(handle, record))
355
+ except Exception:
356
+ context_data = context_view(None)
357
+ if "background_tasks_supported" in source:
358
+ background = source
359
+ else:
360
+ # Once, after the native operation/wait has returned. This is the same
361
+ # recorded/owner-pinned background read used by report; never poll it.
362
+ from ccx.workers import _background_task_block_for_worker
363
+ try:
364
+ background = _background_task_block_for_worker(store, store.resolve(handle))
365
+ except Exception:
366
+ background = {"background_tasks_supported": False}
367
+ return project_worker_view(
368
+ handle, record, context=context_data, background=background,
369
+ last_settled_turn=settled,
370
+ )
371
+
372
+
373
+ REPORT_KEYS = (
374
+ "delivery_id", "turn_ref", "turn_id", "phase", "status", "outcome", "text",
375
+ "truncated", "has_final_message", "settled_at_ms", "native_evidence_sha256",
376
+ "delivery", "source",
377
+ )
378
+
379
+
380
+ def _report_view(handle: str | None, report: dict) -> dict:
381
+ """One V2 report shape for worker.report and the report bundled in wait."""
382
+ return {"worker": handle, **{key: report.get(key) for key in REPORT_KEYS}}
383
+
384
+
385
+ def _rpc_result(op: str, result: dict, store, params: dict) -> dict:
386
+ """Only the Local JSON boundary is V2; never mutate shared operation output."""
387
+ if not isinstance(result, dict):
388
+ return result
389
+ source = result
390
+ original = source.get("worker")
391
+ target = params.get("target")
392
+ handle = _rpc_handle(original, target) or _rpc_handle(source, target)
393
+ # Telemetry's original worker is only {engine, short, native_id}; a
394
+ # source caller may also supply the bare full UUID. Resolve to the
395
+ # generation-owned canonical qualified handle before publication.
396
+ if isinstance(handle, str) and ":" not in handle:
397
+ handle = store.resolve(handle).worker
398
+
399
+ if op in {"worker.create", "worker.fork", "worker.inspect"}:
400
+ view = _rpc_worker(store, source)
401
+ if op == "worker.inspect":
402
+ return {"worker": handle, "worker_view": view,
403
+ "transcript_locator": source.get("transcript_locator")}
404
+ return {"worker": handle, "worker_view": view,
405
+ "created": source.get("created"),
406
+ "reconciled": source.get("reconciled")}
407
+
408
+ if op == "worker.wait":
409
+ view = _rpc_worker(store, {"worker": handle},
410
+ settled=source.get("last_settled_turn"))
411
+ raw = source.get("status")
412
+ from ccx.store.worker_state import normalize_native_state
413
+ status = ("timeout" if raw == "timeout" else
414
+ "unavailable" if raw == "host-dead" else
415
+ "stopped" if raw == "killed" else
416
+ raw if raw in {"starting", "idle", "busy", "waiting", "stopped",
417
+ "failed", "unavailable"} else
418
+ normalize_native_state(raw))
419
+ cause = (raw if raw == "host-dead" or
420
+ raw not in {status, "timeout"} and isinstance(raw, str)
421
+ else source.get("reason"))
422
+ return {
423
+ "worker": handle, "worker_view": view,
424
+ "status": status,
425
+ "timed_out": bool(source.get("timed_out")),
426
+ "reason": cause,
427
+ "wait_mode": source.get("wait_mode", "stable"),
428
+ "next_turn_observed": source.get("next_turn_observed", False),
429
+ "pending_input": source.get("pending_input"),
430
+ "active_turn_ref": source.get("active_turn_ref"),
431
+ "report": (_report_view(handle, source["report"])
432
+ if isinstance(source.get("report"), dict) else None),
433
+ "report_error": source.get("report_error"),
434
+ }
435
+
436
+ if op == "worker.kill":
437
+ return {
438
+ "worker": handle,
439
+ "killed": source.get("killed"),
440
+ "changed": source.get("changed"),
441
+ "interrupted": source.get("interrupted"),
442
+ # Existing confirmed teardown returns resume_available, not resumable.
443
+ "resumable": source.get("resume_available"),
444
+ "state": source.get("state"),
445
+ }
446
+
447
+ if op in {"worker.compact", "worker.compact.status"}:
448
+ keys = ("operation_id", "status", "receipt_status", "compacted",
449
+ "recommended_action", "context_before", "context_after",
450
+ "compact_turn_id", "receipt", "reason", "evidence", "input_surface")
451
+ out = {"worker": handle, **{key: source.get(key) for key in keys}}
452
+ if isinstance(out["evidence"], dict):
453
+ out["evidence"] = {key: value for key, value in out["evidence"].items()
454
+ if key != "compact_turn_id"}
455
+ return out
456
+
457
+ if op in {"turn.send", "turn.reconcile"}:
458
+ receipt = dict(source.get("delivery") or {})
459
+ for field in ("delivery_id", "status", "pending", "owned", "settled",
460
+ "accepted", "route", "placement", "turn_ref", "turn_id",
461
+ "caller_metadata", "recommended_action"):
462
+ if (field in source
463
+ and (op == "turn.reconcile" or field not in receipt)
464
+ and not (op == "turn.send" and field == "settled"
465
+ and "settlement" in receipt)):
466
+ receipt[field] = source[field]
467
+ if "status" not in receipt:
468
+ receipt["status"] = "unknown"
469
+ out = {"worker": handle, "delivery": receipt}
470
+ if op == "turn.send":
471
+ out["notification"] = source.get("notification")
472
+ return out
473
+
474
+ if op == "turn.cancel":
475
+ receipt = dict(source.get("receipt") or {})
476
+ for key in ("cancellation_id", "turn_ref", "status", "confirmed",
477
+ "owned", "accepted", "recommended_action", "outcome", "reason"):
478
+ if key in source:
479
+ receipt[key] = source[key]
480
+ return {"worker": handle, "cancellation": receipt}
481
+
482
+ if op in {"input.answer", "input.decide", "input.respond"}:
483
+ receipt = dict(source.get("receipt") or {})
484
+ for key in ("response_id", "request_id", "kind", "status", "owned",
485
+ "recommended_action", "reason", "evidence"):
486
+ if key in source:
487
+ receipt[key] = source[key]
488
+ return {"worker": handle, "input": receipt,
489
+ "notification": source.get("notification")}
490
+
491
+ if op == "notification.inspect":
492
+ return {"notification": dict(source)}
493
+
494
+ if op == "worker.report":
495
+ return _report_view(handle, source)
496
+
497
+ if op == "worker.telemetry":
498
+ # Its native per-engine identity blocks are intentionally not WorkerView.
499
+ keys = ("telemetry_kind", "turns", "compactions", "totals", "coverage",
500
+ "next_cursor", "has_more")
501
+ return {"worker": handle,
502
+ **{key: source.get(key) for key in keys}}
503
+
504
+ if op == "worker.events":
505
+ keys = ("events", "next_cursor", "has_more", "gap", "source", "timed_out",
506
+ "runtime")
507
+ return {"worker": handle,
508
+ **{key: source.get(key) for key in keys}}
509
+
510
+ if op == "account.usage.read":
511
+ return {key: source.get(key) for key in
512
+ ("provider", "observed_at_ms", "windows")}
513
+
514
+ if op == "transcript.read":
515
+ keys = ("transcript_locator", "conversation_schema_version", "turns",
516
+ "completeness", "evidence", "source_revision", "source_changed",
517
+ "next_cursor", "has_more")
518
+ return {"worker": handle,
519
+ **{key: source.get(key) for key in keys}}
520
+
521
+ if op == "conversation.page":
522
+ keys = ("records", "older_cursor", "completeness")
523
+ return {"worker": handle,
524
+ **{key: source.get(key) for key in keys}}
525
+
526
+ return dict(source)
527
+
528
+
529
+ def _op_worker_fork(
530
+ params: dict[str, Any], timeout: float, *,
531
+ expected_generation: str | None = None,
532
+ deadline: float | None = None,
533
+ ) -> dict[str, Any]:
534
+ return fork_worker(
535
+ target=_required_text(params, "target"),
536
+ worker_id=_optional_text(params, "worker_id", allow_null=False),
537
+ name=_optional_text(params, "name"),
538
+ model=_optional_text(params, "model"),
539
+ effort=_optional_text(params, "effort"),
540
+ timeout=timeout, expected_generation=expected_generation,
541
+ deadline=deadline, created_via="local-json-v1",
542
+ )
543
+
544
+
545
+ def _op_worker_wait(
546
+ params: dict[str, Any], timeout: float
547
+ ) -> dict[str, Any]:
548
+ selectors: dict[str, Any] = {}
549
+ if "next_turn" in params:
550
+ selectors["next_turn"] = _optional_bool(params, "next_turn", False)
551
+ if "after_turn" in params:
552
+ selectors["after_turn"] = _optional_text(params, "after_turn")
553
+ if "known_input" in params:
554
+ selectors["known_input"] = _optional_text(params, "known_input")
555
+ if selectors.get("after_turn") is not None and not selectors.get("next_turn"):
556
+ raise InvalidRequest("after_turn requires next_turn=true")
557
+ if selectors.get("known_input") is not None and not selectors.get("next_turn"):
558
+ raise InvalidRequest("known_input requires next_turn=true")
559
+ target = _required_text(params, "target")
560
+ include_report = _optional_bool(params, "include_report", False)
561
+ if "report_max_chars" in params and not include_report:
562
+ raise InvalidRequest("report_max_chars requires include_report=true")
563
+ report_max_chars = _optional_nullable_int(
564
+ params, "report_max_chars", _DEFAULT_REPORT_MAX_CHARS
565
+ )
566
+ if not include_report:
567
+ return wait_worker(target, timeout=timeout, **selectors)
568
+ # Inclusions are read after the verdict, inside the same envelope budget,
569
+ # so the caller needs no further process to learn them.
570
+ deadline = time.monotonic() + timeout
571
+ result = wait_worker(
572
+ target,
573
+ timeout=timeout - min(_INCLUDE_RESERVE_S, timeout / 4),
574
+ **selectors,
575
+ )
576
+ if include_report:
577
+ settled = result.get("last_settled_turn")
578
+ turn_ref = (settled.get("turn_ref") if isinstance(settled, dict)
579
+ and settled.get("outcome") == "completed" else None)
580
+ if turn_ref is None:
581
+ result["report"] = None
582
+ result["report_error"] = (
583
+ "no positively completed exact turn" if settled else None
584
+ )
585
+ else:
586
+ _include(result, "report", deadline, lambda remaining: report_worker(
587
+ target,
588
+ turn_ref=turn_ref,
589
+ max_chars=report_max_chars,
590
+ timeout=remaining,
591
+ ))
592
+ return result
593
+
594
+
595
+ def _include(
596
+ result: dict[str, Any], key: str, deadline: float,
597
+ read: Callable[[float], Any],
598
+ ) -> None:
599
+ """Attach one inclusion; the wait's verdict stands even when it fails.
600
+
601
+ ``read`` gets what is left of the envelope budget; none left is a
602
+ ``timeout`` inclusion error rather than a read past the caller's deadline.
603
+ """
604
+ try:
605
+ remaining = deadline - time.monotonic()
606
+ if remaining <= 0:
607
+ raise OperationTimeout(f"no time left in the request budget for {key}")
608
+ result[key] = read(remaining)
609
+ result[f"{key}_error"] = None
610
+ except Exception as error:
611
+ result[key] = None
612
+ result[f"{key}_error"] = _error_from_exception(None, error)["error"]
613
+
614
+
615
+ def _op_worker_park(
616
+ params: dict[str, Any], timeout: float
617
+ ) -> dict[str, Any]:
618
+ return park_worker(_required_text(params, "target"), timeout=timeout)
619
+
620
+
621
+ def _rpc_compact_result(store, owner, operation_id: str, row: dict | None,
622
+ *, observing: bool) -> dict[str, Any]:
623
+ """Project verified Claude compaction evidence."""
624
+ from ccx.ops.compact_ops import get_operation, public_operation
625
+
626
+ row = row or {}
627
+ retained = get_operation(owner.worker, operation_id)
628
+ raw = (retained or {}).get("status")
629
+ proven = row.get("operation_status") or "unknown"
630
+ if observing:
631
+ status = "pending" if proven in ("accepted", "pasted") else proven
632
+ compacted = (status == "succeeded"
633
+ if status in ("succeeded", "noop", "failed") else None)
634
+ action = ("wait" if status in ("pending", "submitted")
635
+ else "inspect" if status in ("unknown", "not_found")
636
+ else "none")
637
+ else:
638
+ # Indeterminate compaction returns pending or unknown data,
639
+ # and a native noop is settled succeeded without compacting.
640
+ status = ("submitted" if proven in ("accepted", "pasted", "submitted")
641
+ else "succeeded" if proven == "noop" else proven)
642
+ # Compaction reports a plain boolean; tri-state is status-only.
643
+ compacted = proven == "succeeded"
644
+ action = "wait" if status == "submitted" else "none"
645
+
646
+ def native_tokens(name: str) -> dict | None:
647
+ value = row.get(name)
648
+ return {"tokens_used": value} if type(value) is int else None
649
+
650
+ return {
651
+ "worker": recorded_inspector(store, owner.worker),
652
+ "operation_id": operation_id,
653
+ "status": status,
654
+ "receipt_status": raw,
655
+ "compacted": compacted,
656
+ "recommended_action": action,
657
+ "context_before": native_tokens("pre_tokens"),
658
+ "context_after": native_tokens("post_tokens"),
659
+ "compact_turn_id": None,
660
+ "receipt": public_operation(retained),
661
+ "reason": row.get("reason"),
662
+ "evidence": {
663
+ "native_marker": "manual" if row.get("evidence_uuid") else "absent",
664
+ "compact_turn_id": None, "event_id": row.get("evidence_uuid"),
665
+ },
666
+ # Without optional read-only host observation, never assert
667
+ # current input-surface release, loaded state or native termination.
668
+ "input_surface": {"compaction_ownership": "unknown",
669
+ "operation_id": None, "runtime_id": None},
670
+ }
671
+
672
+
673
+ def _rpc_codex_compact_result(store, owner, operation_id: str,
674
+ row: dict, *, observing: bool) -> dict[str, Any]:
675
+ """Project original Codex compaction evidence."""
676
+ native_status = row.get("status") or "unknown"
677
+ status = ("pending" if native_status == "prepared"
678
+ else "failed" if native_status == "rejected"
679
+ else native_status)
680
+ if not observing and status == "pending":
681
+ # Prepared means no proven native admission, not completed.
682
+ status = "unknown"
683
+ compacted = (status == "succeeded" if not observing else
684
+ True if status == "succeeded" else
685
+ False if status == "failed" else None)
686
+ action = ("wait" if status in ("pending", "submitted")
687
+ else "inspect" if observing and status in ("unknown", "not_found")
688
+ else "none")
689
+ before = row.get("contextBefore")
690
+ after = row.get("contextAfter") if status == "succeeded" else None
691
+ turn_id = row.get("compactTurnId")
692
+ item_id = row.get("contextItemId")
693
+ return {
694
+ "worker": recorded_inspector(store, owner.worker),
695
+ "operation_id": operation_id,
696
+ "status": status,
697
+ "receipt_status": row.get("receipt_status"),
698
+ "compacted": compacted,
699
+ "recommended_action": action,
700
+ "context_before": before if isinstance(before, dict) else None,
701
+ "context_after": after if isinstance(after, dict) else None,
702
+ "compact_turn_id": turn_id,
703
+ "receipt": row.get("receipt"),
704
+ "reason": row.get("reason"),
705
+ "evidence": {
706
+ "native_marker": "contextCompaction" if isinstance(item_id, str)
707
+ and bool(item_id) else "unproven",
708
+ "compact_turn_id": turn_id,
709
+ "event_id": item_id,
710
+ },
711
+ "input_surface": {
712
+ "compaction_ownership": "unknown",
713
+ "operation_id": None, "runtime_id": None,
714
+ },
715
+ }
716
+
717
+
718
+ def _rpc_codex_compact(store, owner, operation_id: str, timeout: float) -> dict:
719
+ """One core wait and at most ONE pure original-ID observation on loss."""
720
+ from ccx.codex.ops import compact_worker as codex_compact_worker
721
+ from ccx.codex.ops import compact_observation
722
+ from ccx.store.root import NativeEffectIndeterminate
723
+
724
+ try:
725
+ row = codex_compact_worker(
726
+ owner.worker, operation_id=operation_id, wait=True, timeout=timeout,
727
+ )
728
+ except NativeEffectIndeterminate:
729
+ row = compact_observation(owner.worker, operation_id)
730
+ if row["status"] == "not_found":
731
+ row = {**row, "status": "unknown"}
732
+ if row["status"] == "failed":
733
+ cause = CompactFailed(
734
+ row.get("reason") or "Codex native compact turn failed",
735
+ short=owner.short, operation_id=operation_id,
736
+ )
737
+ raise DeliveryRejected(
738
+ str(cause), short=owner.short, reason=str(cause),
739
+ refusal="compact-failed", operation_id=operation_id,
740
+ ) from cause
741
+ if row["status"] == "rejected":
742
+ raise DeliveryRejected(
743
+ row.get("reason") or "Codex compact was refused before native admission",
744
+ short=owner.short, refusal="native-rejected",
745
+ operation_id=operation_id,
746
+ )
747
+ return _rpc_codex_compact_result(
748
+ store, owner, operation_id, row, observing=False,
749
+ )
750
+
751
+
752
+ def _op_worker_compact(
753
+ params: dict[str, Any], timeout: float, *, store=None,
754
+ ) -> dict[str, Any]:
755
+ """Dispatch Claude compaction through its durable operation."""
756
+ from ccx.constants import COMPACT_WAIT_DEFAULT_S
757
+ from ccx.ops.compact_ops import get_operation
758
+ from ccx.errors import CompactIndeterminate
759
+
760
+ if store is None:
761
+ raise SessionUnavailable("V1 compact requires the exact ACTIVE UUID dispatcher")
762
+ owner = store.resolve(_required_text(params, "target"))
763
+ token = _required_text(params, "operation_id")
764
+ if owner.engine == "codex":
765
+ return _rpc_codex_compact(store, owner, token, timeout)
766
+ try:
767
+ try:
768
+ row = compact_worker(
769
+ owner.worker, operation_id=token, timeout=timeout,
770
+ execution_timeout=max(timeout or 0, COMPACT_WAIT_DEFAULT_S),
771
+ )
772
+ except CompactIndeterminate:
773
+ # Exactly ONE existing original-UUID status reconciliation, not
774
+ # a second paste, retry loop or new status evaluator.
775
+ try:
776
+ row = compact_status(owner.worker, operation_id=token)
777
+ except (CompactIndeterminate, SessionNotFound):
778
+ row = {"operation_status": "unknown", "reason":
779
+ "original compact terminal evidence unavailable"}
780
+ # A boundary/failure race can be confirmed by that final status read.
781
+ if row.get("operation_status") == "failed":
782
+ from ccx.client import _compact_failed
783
+ failure = _compact_failed(
784
+ get_operation(owner.worker, token),
785
+ short=owner.short, state=row.get("state"), operation_id=token,
786
+ )
787
+ raise failure
788
+ except CompactFailed as failed:
789
+ raise DeliveryRejected(
790
+ str(failed), short=owner.short, reason=str(failed),
791
+ state=getattr(failed, "state", None),
792
+ refusal="compact-failed", operation_id=token,
793
+ ) from failed
794
+ return _rpc_compact_result(store, owner, token, row, observing=False)
795
+
796
+
797
+ def _op_turn_send(
798
+ params: dict[str, Any], timeout: float
799
+ ) -> dict[str, Any]:
800
+ kwargs: dict[str, Any] = {
801
+ "delivery_id": _required_text(params, "delivery_id"),
802
+ "timeout": timeout,
803
+ }
804
+ if "mode" in params:
805
+ mode = _optional_text(params, "mode", allow_null=False)
806
+ if mode not in {"new_turn_only", "auto"}:
807
+ raise InvalidRequest(
808
+ "turn.send cannot steer a running turn; use turn.steer instead"
809
+ )
810
+ kwargs["mode"] = mode
811
+ if "notify_parent" in params:
812
+ kwargs["notify_parent"] = _optional_bool(
813
+ params, "notify_parent", False
814
+ )
815
+ if "caller_metadata" in params:
816
+ kwargs["caller_metadata"] = _optional_object(
817
+ params, "caller_metadata"
818
+ )
819
+ return send_turn(
820
+ _required_text(params, "target"),
821
+ _required_text(params, "text"),
822
+ **kwargs,
823
+ )
824
+
825
+
826
+ def _op_turn_reconcile(
827
+ params: dict[str, Any], timeout: float
828
+ ) -> dict[str, Any]:
829
+ target = _required_text(params, "target")
830
+ identity = {
831
+ "delivery_id": _optional_text(params, "delivery_id"),
832
+ "turn_id": _optional_text(params, "turn_id"),
833
+ }
834
+ if not _optional_bool(params, "hold_while_pending", False):
835
+ return reconcile_turn(target, **identity, timeout=timeout)
836
+ # Reconciliation is read-only, so holding is a bounded re-read of the
837
+ # receipt; only the final answer pays for the worker snapshot.
838
+ deadline = time.monotonic() + timeout
839
+ while True:
840
+ remaining = deadline - time.monotonic()
841
+ if remaining <= _HOLD_POLL_S:
842
+ return reconcile_turn(
843
+ target, **identity, timeout=max(remaining, _HOLD_POLL_S)
844
+ )
845
+ probe = reconcile_turn(
846
+ target, **identity, timeout=remaining, include_worker=False
847
+ )
848
+ # Only an in-flight delivery (`wait`) can progress on its own; a
849
+ # `retry_same_id` receipt needs the caller, so holding it is pointless.
850
+ if probe.get("pending") is not True or probe.get("recommended_action") != "wait":
851
+ return reconcile_turn(
852
+ target, **identity,
853
+ timeout=max(deadline - time.monotonic(), _HOLD_POLL_S),
854
+ )
855
+ time.sleep(_HOLD_POLL_S)
856
+
857
+
858
+ def _op_turn_cancel(
859
+ params: dict[str, Any], timeout: float
860
+ ) -> dict[str, Any]:
861
+ return cancel_turn(
862
+ _required_text(params, "target"),
863
+ cancellation_id=_required_text(params, "cancellation_id"),
864
+ turn_ref=_required_text(params, "turn_ref"),
865
+ timeout=timeout,
866
+ )
867
+
868
+
869
+ def _op_input_answer(
870
+ params: dict[str, Any], timeout: float
871
+ ) -> dict[str, Any]:
872
+ answers = params.get("answers")
873
+ if not isinstance(answers, dict):
874
+ raise InvalidRequest("answers must be a JSON object")
875
+ return answer_input(
876
+ _required_text(params, "target"),
877
+ request_id=_required_text(params, "request_id"),
878
+ response_id=_required_text(params, "response_id"),
879
+ answers=answers,
880
+ timeout=timeout,
881
+ notify_parent=_optional_bool(params, "notify_parent", False),
882
+ )
883
+
884
+
885
+ def _op_input_decide(
886
+ params: dict[str, Any], timeout: float
887
+ ) -> dict[str, Any]:
888
+ return decide_input(
889
+ _required_text(params, "target"),
890
+ request_id=_required_text(params, "request_id"),
891
+ response_id=_required_text(params, "response_id"),
892
+ decision=_required_text(params, "decision"),
893
+ reason=_optional_text(params, "reason"),
894
+ timeout=timeout,
895
+ )
896
+
897
+
898
+ def _op_input_respond(
899
+ params: dict[str, Any], timeout: float
900
+ ) -> dict[str, Any]:
901
+ return respond_input(
902
+ _required_text(params, "target"),
903
+ request_id=_required_text(params, "request_id"),
904
+ response_id=_required_text(params, "response_id"),
905
+ action=_required_text(params, "action"),
906
+ content=_optional_object(params, "content"),
907
+ timeout=timeout,
908
+ )
909
+
910
+
911
+ def _op_notification_inspect(
912
+ params: dict[str, Any], timeout: float
913
+ ) -> dict[str, Any]:
914
+ del timeout
915
+ return inspect_notification(_required_text(params, "event_id"))
916
+
917
+
918
+ def _op_worker_report(
919
+ params: dict[str, Any], timeout: float
920
+ ) -> dict[str, Any]:
921
+ max_chars = _optional_nullable_int(params, "max_chars", _DEFAULT_REPORT_MAX_CHARS)
922
+ return report_worker(
923
+ _required_text(params, "target"),
924
+ timeout=timeout,
925
+ max_chars=max_chars,
926
+ turn_ref=_optional_text(params, "turn_ref"),
927
+ )
928
+
929
+
930
+ def _op_worker_telemetry(
931
+ params: dict[str, Any], timeout: float
932
+ ) -> dict[str, Any]:
933
+ return telemetry_worker(
934
+ _required_text(params, "target"),
935
+ after_cursor=_optional_text(params, "after_cursor"),
936
+ limit=_optional_int(params, "limit", 200),
937
+ timeout=timeout,
938
+ )
939
+
940
+
941
+ def _op_account_usage(
942
+ params: dict[str, Any], timeout: float
943
+ ) -> dict[str, Any]:
944
+ return account_usage_read(
945
+ engine=_required_text(params, "engine"),
946
+ timeout=timeout,
947
+ )
948
+
949
+
950
+ def _op_transcript_read(
951
+ params: dict[str, Any], timeout: float
952
+ ) -> dict[str, Any]:
953
+ max_tool = _optional_nullable_int(
954
+ params, "max_tool_result_bytes", 64 * 1024
955
+ )
956
+ return read_transcript(
957
+ _required_text(params, "locator"),
958
+ cursor=_optional_text(params, "cursor"),
959
+ limit=_optional_int(params, "limit", 20),
960
+ max_tool_result_bytes=max_tool,
961
+ timeout=timeout,
962
+ )
963
+
964
+
965
+ def _op_conversation_page(
966
+ params: dict[str, Any], timeout: float
967
+ ) -> dict[str, Any]:
968
+ max_tool = _optional_nullable_int(
969
+ params, "max_tool_result_bytes", 16 * 1024
970
+ )
971
+ return conversation_page(
972
+ _required_text(params, "target"),
973
+ cursor=_optional_text(params, "cursor"),
974
+ source_record_target=_optional_int(
975
+ params, "source_record_target", 40
976
+ ),
977
+ max_tool_result_bytes=max_tool,
978
+ timeout=timeout,
979
+ )
980
+
981
+
982
+ def _op_compact_status(params: dict[str, Any], timeout: float,
983
+ *, store=None) -> dict[str, Any]:
984
+ """Strictly observational protocol status: original evidence, no journal write."""
985
+ from ccx.errors import CompactIndeterminate
986
+
987
+ if store is None:
988
+ raise SessionUnavailable("V1 compact status requires the exact ACTIVE UUID dispatcher")
989
+ owner = store.resolve(_required_text(params, "target"))
990
+ token = _required_text(params, "operation_id")
991
+ if owner.engine == "codex":
992
+ from ccx.codex.ops import compact_observation
993
+ from ccx.codex.runtime import daemon_rpc
994
+ from ccx.constants import CODEXD_RPC_GENERATION
995
+
996
+ row = compact_observation(owner.worker, token)
997
+ result = _rpc_codex_compact_result(
998
+ store, owner, token, row, observing=True,
999
+ )
1000
+ # The existing codexd compact-observe is read-only, never starts
1001
+ # a daemon or changes native outcome. It may describe only current
1002
+ # input-surface ownership, and an unavailable daemon proves nothing.
1003
+ budget = min(.75, timeout)
1004
+ if budget > 0:
1005
+ try:
1006
+ live = daemon_rpc(
1007
+ {"op": "compact-observe", "short": owner.worker_id},
1008
+ timeout=budget,
1009
+ )
1010
+ except Exception:
1011
+ live = {}
1012
+ if (isinstance(live, dict) and live.get("ok") is True
1013
+ and live.get("transport") == "served"
1014
+ and live.get("rpcGeneration") == CODEXD_RPC_GENERATION
1015
+ and isinstance(live.get("input_surface"), dict)):
1016
+ result["input_surface"] = {
1017
+ **result["input_surface"],
1018
+ **live["input_surface"], "source": "live",
1019
+ }
1020
+ return result
1021
+ try:
1022
+ row = compact_status(owner.worker, operation_id=token,
1023
+ timeout=timeout, publish=False)
1024
+ except SessionNotFound:
1025
+ # A missing OPERATION is DATA; the OWNER was already proven above.
1026
+ row = {"operation_status": "not_found"}
1027
+ except CompactIndeterminate:
1028
+ row = {"operation_status": "unknown"}
1029
+ result = _rpc_compact_result(store, owner, token, row, observing=True)
1030
+ # Main's bounded read-only snapshot of a host that is already running:
1031
+ # compact-observe never reconciles, settles or writes. A cold or
1032
+ # unreachable host leaves the conservative unknown surface.
1033
+ from ccx.store.liveness import lifecycle_host_alive
1034
+ from ccx.rpc.rpc import _rpc
1035
+
1036
+ lifecycle = store.record(owner.worker).get("lifecycle") or {}
1037
+ budget = min(.75, timeout)
1038
+ if budget > 0 and lifecycle.get("hostPid") and lifecycle_host_alive(lifecycle):
1039
+ try:
1040
+ live = _rpc(owner.worker, {"op": "compact-observe"}, timeout=budget)
1041
+ except Exception:
1042
+ live = {}
1043
+ if (isinstance(live, dict) and live.get("ok")
1044
+ and isinstance(live.get("input_surface"), dict)):
1045
+ result["input_surface"] = {**result["input_surface"],
1046
+ **live["input_surface"], "source": "live"}
1047
+ return result
1048
+
1049
+
1050
+ def _api_error_details(error: WorkerApiError) -> dict[str, Any]:
1051
+ """The same snake_case shape ``worker.wait`` reports as ``api_error``."""
1052
+ return {
1053
+ "kind": error.kind,
1054
+ "status": error.status,
1055
+ "transient": error.transient,
1056
+ "rate_limited": isinstance(error, WorkerRateLimited),
1057
+ "request_id": error.request_id,
1058
+ "message": error.message,
1059
+ "reset_text": getattr(error, "reset_text", None),
1060
+ }
1061
+
1062
+
1063
+ def _error_from_exception(
1064
+ request_id: str | None, error: BaseException, *,
1065
+ operation: str | None = None, engine: str | None = None,
1066
+ ) -> dict[str, Any]:
1067
+ details: dict[str, Any] = {}
1068
+ short = getattr(error, "short", None)
1069
+ if short is not None:
1070
+ details["short"] = short
1071
+
1072
+ if isinstance(error, SendIndeterminate):
1073
+ details.update({
1074
+ "delivery_id": getattr(error, "delivery_id", None),
1075
+ "delivery_status": getattr(error, "delivery_status", None),
1076
+ })
1077
+ return _error(
1078
+ request_id, "reconcile_required", str(error), details=details
1079
+ )
1080
+ if isinstance(error, CursorInvalidated):
1081
+ return _error(
1082
+ request_id, "cursor_invalidated", str(error), details=details
1083
+ )
1084
+ if isinstance(error, OperationConflict):
1085
+ if error.reason is not None:
1086
+ details["reason"] = error.reason
1087
+ if error.worker_id is not None:
1088
+ details["worker_id"] = error.worker_id
1089
+ return _error(request_id, "conflict", str(error), details=details)
1090
+ if isinstance(error, CreatePartialFailure):
1091
+ details.update(worker_id=error.worker_id, worker=error.worker,
1092
+ engine=error.engine, phase=error.phase,
1093
+ delivery_id=error.delivery_id,
1094
+ delivery_status=error.delivery_status,
1095
+ notification_intent=error.notification_intent)
1096
+ # The claim flags are committed before post-create inspection; their
1097
+ # meaning is new/reused claim, not whether native activation ran.
1098
+ code = "partial_failure"
1099
+ if isinstance(error.info, dict):
1100
+ for key in ("created", "reconciled", "activation_state", "native_start_error"):
1101
+ if key in error.info:
1102
+ details[key] = error.info[key]
1103
+ rejected = error.info.get("native_rejection")
1104
+ local = error.info.get("native_local_refusal")
1105
+ if isinstance(rejected, dict) and error.info.get("activation_state") == "rejected":
1106
+ details["native_rejection"] = dict(rejected)
1107
+ code = "native_rejected"
1108
+ elif (isinstance(local, dict) and local.get("proven") is True
1109
+ and error.info.get("activation_state") == "local-refused"):
1110
+ details["native_local_refusal"] = dict(local)
1111
+ code = "local_refused"
1112
+ if error.cause is not None:
1113
+ details["cause_type"] = type(error.cause).__name__
1114
+ if isinstance(error.info, dict) and error.info.get("integrity_error") is True:
1115
+ details["integrity_error"] = True
1116
+ return _error(request_id, code, str(error), details=details)
1117
+ if isinstance(error, CapabilityUnsupported):
1118
+ engine = getattr(error, "engine", None)
1119
+ capability = getattr(error, "capability", None)
1120
+ if engine is not None:
1121
+ details["engine"] = engine
1122
+ if capability is not None:
1123
+ details["capability"] = capability
1124
+ details["engine"] = details.get("engine") or engine or "unknown"
1125
+ details["operation"] = operation or capability or "unknown"
1126
+ return _error(
1127
+ request_id,
1128
+ "unsupported_capability",
1129
+ str(error),
1130
+ details=details,
1131
+ )
1132
+ if isinstance(error, SessionNotFound):
1133
+ return _error(request_id, "not_found", str(error), details=details)
1134
+ if isinstance(error, SessionWaiting):
1135
+ waiting = getattr(error, "waiting_for", None)
1136
+ if waiting is not None:
1137
+ details["waiting_for"] = waiting
1138
+ return _error(
1139
+ request_id, "approval_waiting", str(error), details=details
1140
+ )
1141
+ if isinstance(error, DeliveryRejected):
1142
+ state = getattr(error, "state", None)
1143
+ if state is not None:
1144
+ details["state"] = normalize_native_state(state)
1145
+ details["native_state"] = state
1146
+ refusal = getattr(error, "refusal", None)
1147
+ if refusal is not None:
1148
+ details["refusal"] = refusal
1149
+ cause = error.__cause__
1150
+ if isinstance(cause, CompactFailed) and cause.api_error is not None:
1151
+ # A proven compaction failure caused by the API keeps its
1152
+ # classification, so a caller can tell a rate limit from a
1153
+ # conversation that can never compact.
1154
+ details["api_error"] = _api_error_details(cause.api_error)
1155
+ if refusal == "compaction-busy":
1156
+ details.update(retryable=True, recommended_action="wait")
1157
+ for key in ("delivery_id", "delivery_status", "operation_id"):
1158
+ if getattr(error, key, None) is not None:
1159
+ details[key] = getattr(error, key)
1160
+ code = (
1161
+ "busy" if state == "busy" or refusal == "compaction-busy"
1162
+ else "approval_waiting" if state == "waiting"
1163
+ else "definitive_rejection"
1164
+ )
1165
+ return _error(request_id, code, str(error), details=details)
1166
+ if isinstance(error, ProviderProtocolError):
1167
+ return _error(
1168
+ request_id, "provider_protocol_error", str(error), details=details
1169
+ )
1170
+ if isinstance(error, (OperationTimeout, TurnIncomplete)):
1171
+ return _error(request_id, "timeout", str(error), details=details)
1172
+ if isinstance(error, WorkerApiError):
1173
+ details["api_error"] = _api_error_details(error)
1174
+ code = ("rate_limited" if isinstance(error, WorkerRateLimited)
1175
+ else "transient_api_error" if error.transient
1176
+ else "runtime_unavailable")
1177
+ return _error(request_id, code, str(error), details=details)
1178
+ if isinstance(error, BudgetStopped):
1179
+ return _error(request_id, "budget_stopped", str(error), details=details)
1180
+ if isinstance(error, SessionUnavailable):
1181
+ return _error(
1182
+ request_id, "runtime_unavailable", str(error), details=details
1183
+ )
1184
+ if isinstance(error, InvalidRequest):
1185
+ return _error(
1186
+ request_id, "invalid_request", str(error), details=details
1187
+ )
1188
+ if isinstance(error, CcxError):
1189
+ return _error(request_id, "ccx_error", str(error), details=details)
1190
+ return _error(
1191
+ request_id,
1192
+ "internal_error",
1193
+ f"{error.__class__.__name__}: {error}",
1194
+ )
1195
+
1196
+
1197
+ def dispatch(request: Any, *, expected_generation: str | None = None) -> dict[str, Any]:
1198
+ """Direct Python Local protocol uses the shared full-UUID backend.
1199
+
1200
+ Only the exact-owned operations advertised by dispatch_full_uuid run;
1201
+ no short-keyed or external-provider fallback is permitted.
1202
+ """
1203
+ from ccx.constants import CCX_ROOT
1204
+ from ccx.store.root import GenerationError, require_ccx_active, open_or_initialize
1205
+ # No filesystem access or legacy handler before a pure, typed protocol reply.
1206
+ pure = _full_uuid_pure_reply(request)
1207
+ if pure is not None:
1208
+ return pure
1209
+ request_id = request["request_id"] # _full_uuid_pure_reply validated it
1210
+ deadline = time.monotonic() + _timeout_seconds(request, DEFAULT_TIMEOUT_S)
1211
+ try:
1212
+ store = (require_ccx_active(CCX_ROOT, expected_generation=expected_generation)
1213
+ if expected_generation is not None
1214
+ else open_or_initialize(CCX_ROOT)[0])
1215
+ except (GenerationError, OSError) as exc:
1216
+ return _error(
1217
+ request_id, "state_unavailable",
1218
+ f"full-UUID ACTIVE generation required: {exc}",
1219
+ )
1220
+ return dispatch_full_uuid(request, store, deadline=deadline)
1221
+
1222
+
1223
+ def _serialized(response: dict[str, Any]) -> bytes:
1224
+ try:
1225
+ raw = (
1226
+ json.dumps(
1227
+ response,
1228
+ ensure_ascii=False,
1229
+ allow_nan=False,
1230
+ separators=(",", ":"),
1231
+ sort_keys=True,
1232
+ )
1233
+ + "\n"
1234
+ ).encode("utf-8")
1235
+ except (TypeError, ValueError):
1236
+ request_id = response.get("request_id")
1237
+ response = _error(
1238
+ request_id if isinstance(request_id, str) else None,
1239
+ "internal_error",
1240
+ "protocol response contains a value that cannot be encoded as JSON",
1241
+ )
1242
+ raw = (
1243
+ json.dumps(
1244
+ response, allow_nan=False, separators=(",", ":"), sort_keys=True
1245
+ )
1246
+ + "\n"
1247
+ ).encode("utf-8")
1248
+ if len(raw) <= MAX_MESSAGE_BYTES:
1249
+ return raw
1250
+ request_id = response.get("request_id")
1251
+ smaller = _error(
1252
+ request_id if isinstance(request_id, str) else None,
1253
+ "response_too_large",
1254
+ "protocol response exceeds the 4 MiB protocol limit",
1255
+ details={"limit_bytes": MAX_MESSAGE_BYTES},
1256
+ )
1257
+ return (
1258
+ json.dumps(
1259
+ smaller, allow_nan=False, separators=(",", ":"), sort_keys=True
1260
+ ) + "\n"
1261
+ ).encode("utf-8")
1262
+
1263
+
1264
+ def _read_request() -> dict[str, Any]:
1265
+ stream = getattr(sys.stdin, "buffer", sys.stdin)
1266
+ raw = stream.read(MAX_MESSAGE_BYTES + 1)
1267
+ if isinstance(raw, str):
1268
+ raw = raw.encode("utf-8")
1269
+ return parse_request(raw)
1270
+
1271
+
1272
+ def parse_request(raw: bytes) -> dict[str, Any]:
1273
+ """Decode one request envelope with the protocol size and JSON rules."""
1274
+ if len(raw) > MAX_MESSAGE_BYTES:
1275
+ raise InvalidRequest(
1276
+ "protocol request exceeds the 4 MiB protocol limit"
1277
+ )
1278
+ if not raw.strip():
1279
+ raise InvalidRequest("protocol request is empty")
1280
+ try:
1281
+ value = json.loads(
1282
+ raw,
1283
+ parse_constant=lambda value: (_ for _ in ()).throw(
1284
+ ValueError(f"non-standard JSON constant: {value}")
1285
+ ),
1286
+ )
1287
+ except (ValueError, UnicodeError) as error:
1288
+ raise InvalidRequest("protocol request is not valid JSON") from error
1289
+ if not isinstance(value, dict):
1290
+ raise InvalidRequest("request must be a JSON object")
1291
+ return value
1292
+
1293
+
1294
+ def respond(
1295
+ read: Callable[[], dict[str, Any]], *, expected_generation: str | None = None,
1296
+ ) -> bytes:
1297
+ """One serialized response line for the request ``read`` yields."""
1298
+ request_id = None
1299
+ try:
1300
+ request = read()
1301
+ raw_id = request.get("request_id")
1302
+ if isinstance(raw_id, str):
1303
+ request_id = raw_id
1304
+ response = dispatch(request, expected_generation=expected_generation)
1305
+ except Exception as error:
1306
+ response = _error_from_exception(request_id, error)
1307
+ return _serialized(response)
1308
+
1309
+
1310
+ def cmd_rpc(args) -> None:
1311
+ """CLI adapter for Local protocol requests on stdin."""
1312
+ if getattr(args, "serve", False):
1313
+ from ccx.rpc.serve import serve
1314
+
1315
+ raise SystemExit(serve())
1316
+ out = getattr(sys.stdout, "buffer", sys.stdout)
1317
+ payload = respond(_read_request)
1318
+ if hasattr(out, "write"):
1319
+ try:
1320
+ out.write(payload)
1321
+ except TypeError:
1322
+ out.write(payload.decode("utf-8"))
1323
+ if hasattr(out, "flush"):
1324
+ out.flush()
1325
+
1326
+
1327
+ __all__ = [
1328
+ "MAX_MESSAGE_BYTES",
1329
+ "PROTOCOL_VERSION",
1330
+ "SUPPORTED_PROTOCOL_VERSIONS",
1331
+ "cmd_rpc",
1332
+ "dispatch",
1333
+ "parse_request",
1334
+ "respond",
1335
+ ]
1336
+
1337
+
1338
+ # --- Full-UUID Local protocol operations and native receipt dispatch -----------
1339
+ FULL_UUID_OBSERVATIONAL_OPS = frozenset({
1340
+ "system.info", "worker.inspect", "worker.events", "worker.wait",
1341
+ "worker.compact.status", "account.usage.read", "worker.telemetry",
1342
+ "conversation.page", "transcript.read", "notification.inspect",
1343
+ })
1344
+ # A create request is permitted to reserve one original identity and report
1345
+ # an explicit PARTIAL native-activation outcome, never native success.
1346
+ FULL_UUID_PREPARATION_OPS = frozenset({"worker.create", "worker.fork"})
1347
+ # Reused handlers for the full-UUID native/receipt operations.
1348
+ FULL_UUID_NATIVE_OPS = frozenset({
1349
+ "turn.send", "turn.reconcile", "turn.cancel", "worker.report", "worker.kill",
1350
+ "worker.compact", "input.answer", "input.decide", "input.respond",
1351
+ })
1352
+
1353
+
1354
+ _FULL_UUID_SUPPORTED_OPS = (
1355
+ FULL_UUID_OBSERVATIONAL_OPS | FULL_UUID_PREPARATION_OPS | FULL_UUID_NATIVE_OPS
1356
+ )
1357
+
1358
+
1359
+ def _full_uuid_pure_reply(request: Any) -> dict[str, Any] | None:
1360
+ """protocol envelope/error precedence and capabilities, without root I/O.
1361
+
1362
+ Returns None only for validated, supported STATEFUL operations. The existing
1363
+ dispatch_full_uuid remains their sole native/receipt dispatcher. Preserve
1364
+ the old order: ID, protocol version, op, params shape, supported-op check,
1365
+ allowed params, timeout. An unsupported op is not a legacy fallback.
1366
+ """
1367
+ request_id = None
1368
+ try:
1369
+ if not isinstance(request, dict):
1370
+ raise InvalidRequest("request must be a JSON object")
1371
+ request_id = _request_id(request.get("request_id"))
1372
+ version = request.get("protocol_version")
1373
+ if version != PROTOCOL_VERSION:
1374
+ return _error(
1375
+ request_id, "unsupported_protocol_version",
1376
+ f"unsupported protocol_version {version!r}",
1377
+ details={"supported_protocol_versions": [2]},
1378
+ )
1379
+ op = request.get("op")
1380
+ if not isinstance(op, str) or not op:
1381
+ raise InvalidRequest("op is required")
1382
+ params = _params(request)
1383
+ if op not in _FULL_UUID_SUPPORTED_OPS:
1384
+ return _error(
1385
+ request_id, "unsupported_operation",
1386
+ f"operation {op!r} is not supported",
1387
+ details={"operation": op},
1388
+ )
1389
+ _validate_operation_params(op, params)
1390
+ if op in ("worker.compact", "worker.compact.status"):
1391
+ # The protocol owner needs the caller's exact operation identity even
1392
+ # before an ACTIVE store exists; never silently mint one.
1393
+ _required_text(params, "target")
1394
+ _required_text(params, "operation_id")
1395
+ if op in ("input.answer", "input.decide", "input.respond"):
1396
+ # The original gate and caller-known response identity precede
1397
+ # any ACTIVE root or native input side effect.
1398
+ _required_text(params, "target")
1399
+ _required_text(params, "request_id")
1400
+ _required_text(params, "response_id")
1401
+ _timeout_seconds(request, DEFAULT_TIMEOUT_S)
1402
+ if op == "system.info":
1403
+ return _success(request_id, {"protocol_version": 2, "ccx_version": ccx_version()})
1404
+ return None
1405
+ except InvalidRequest as error:
1406
+ return _error(request_id, "invalid_request", str(error))
1407
+
1408
+
1409
+ def dispatch_full_uuid(request: Any, store, *, deadline: float | None = None) -> dict[str, Any]:
1410
+ """Exact-owned Local protocol native receipt subset with truthful capabilities.
1411
+
1412
+ Reuses the existing protocol turn.send/reconcile/worker.report operation
1413
+ parsers with the globally owned full-UUID host/codexd native receipt
1414
+ implementation. No external Claude or short/name alias fallback.
1415
+ """
1416
+ from ccx.store.root import GenerationError, StaleGeneration, StorageIntegrityError
1417
+ from ccx.store.store import GenerationStore
1418
+ from ccx.store.identity import (
1419
+ EngineMismatch, InvalidTarget, UnknownWorker,
1420
+ )
1421
+ from ccx.ops.worker_events import EventCursorInvalid, wait_full_uuid_event_page
1422
+
1423
+ request_id = None
1424
+ if deadline is None and isinstance(request, dict):
1425
+ # Direct one-shot protocol calls use the same validated end-to-end budget.
1426
+ try:
1427
+ deadline = time.monotonic() + _timeout_seconds(request, DEFAULT_TIMEOUT_S)
1428
+ except InvalidRequest as exc:
1429
+ return _error(request.get("request_id"), "invalid_request", str(exc))
1430
+ try:
1431
+ if not isinstance(store, GenerationStore):
1432
+ raise StorageIntegrityError("CCX worker records are unavailable; check CCX_ROOT")
1433
+ store.revalidate()
1434
+ pure = _full_uuid_pure_reply(request)
1435
+ if pure is not None:
1436
+ return pure
1437
+ # A typed/complete stateful protocol envelope was validated above. Runtime
1438
+ # generation/owner/source and receipt checks remain required below.
1439
+ request_id = request["request_id"]
1440
+ op = request["op"]
1441
+ params = _params(request)
1442
+ if op == "worker.create":
1443
+ from ccx.ops.create import create_worker
1444
+ from ccx.store.identity import InvalidTarget, canonical_worker_id
1445
+ from ccx.errors import CreatePartialFailure, OperationConflict
1446
+ if "worker_id" in params:
1447
+ try:
1448
+ canonical_worker_id(params["worker_id"])
1449
+ except InvalidTarget as exc:
1450
+ raise InvalidRequest(
1451
+ f"worker_id must be complete canonical UUIDv4: {exc}"
1452
+ ) from exc
1453
+ engine = params.get("engine")
1454
+ cwd = params.get("cwd")
1455
+ if engine not in ("claude", "codex"):
1456
+ raise InvalidRequest("worker.create engine must be claude or codex")
1457
+ if not isinstance(cwd, str) or not cwd:
1458
+ raise InvalidRequest("worker.create cwd must be explicit")
1459
+ # For a newly minted ID the durable returned partial error must
1460
+ # carry the exact chosen worker_id, not a display-name retry.
1461
+ budget = (deadline - time.monotonic()) if deadline is not None else DEFAULT_TIMEOUT_S
1462
+ if budget <= 0:
1463
+ raise OperationTimeout("worker.create request deadline elapsed before native creation")
1464
+ result = create_worker(
1465
+ engine=engine, cwd=cwd, worker_id=params.get("worker_id"),
1466
+ name=params.get("name"), model=params.get("model"),
1467
+ effort=params.get("effort"),
1468
+ idle_timeout=params.get("idle_timeout", DEFAULT_IDLE_TIMEOUT_S),
1469
+ permissions=params.get("permissions"),
1470
+ sandbox=params.get("sandbox"),
1471
+ approval_policy=params.get("approval_policy"),
1472
+ created_via="local-json-v1", timeout=budget,
1473
+ expected_generation=store.generation, deadline=deadline,
1474
+ )
1475
+ elif op == "worker.fork":
1476
+ from ccx.store.identity import InvalidTarget, canonical_worker_id
1477
+ if "worker_id" in params:
1478
+ try:
1479
+ canonical_worker_id(params["worker_id"])
1480
+ except InvalidTarget as exc:
1481
+ raise InvalidRequest(
1482
+ f"worker_id must be complete canonical UUIDv4: {exc}"
1483
+ ) from exc
1484
+ budget = (deadline - time.monotonic()) if deadline is not None else DEFAULT_TIMEOUT_S
1485
+ if budget <= 0:
1486
+ raise OperationTimeout("worker.fork request deadline elapsed before native creation")
1487
+ result = _op_worker_fork(
1488
+ params, budget, expected_generation=store.generation, deadline=deadline,
1489
+ )
1490
+ elif op in FULL_UUID_NATIVE_OPS:
1491
+ # Exact original protocol schema validation ran above; the same old
1492
+ # operation functions now invoke protocol_ops' real UUID-native
1493
+ # host/codexd and semantic receipt implementations.
1494
+ original_handlers = {
1495
+ "turn.send": _op_turn_send,
1496
+ "turn.reconcile": _op_turn_reconcile,
1497
+ "turn.cancel": _op_turn_cancel,
1498
+ "worker.kill": _op_worker_park,
1499
+ "worker.report": _op_worker_report,
1500
+ "input.answer": _op_input_answer,
1501
+ "input.decide": _op_input_decide,
1502
+ "input.respond": _op_input_respond,
1503
+ }
1504
+ budget = _timeout_seconds(request, DEFAULT_TIMEOUT_S)
1505
+ if op == "worker.compact":
1506
+ result = _op_worker_compact(params, budget, store=store)
1507
+ else:
1508
+ result = original_handlers[op](params, budget)
1509
+ elif op == "worker.compact.status":
1510
+ result = _op_compact_status(
1511
+ params, _timeout_seconds(request, DEFAULT_TIMEOUT_S), store=store,
1512
+ )
1513
+ elif op == "notification.inspect":
1514
+ result = _op_notification_inspect(
1515
+ params, _timeout_seconds(request, DEFAULT_TIMEOUT_S),
1516
+ )
1517
+ elif op == "account.usage.read":
1518
+ result = _op_account_usage(
1519
+ params, _timeout_seconds(request, DEFAULT_TIMEOUT_S),
1520
+ )
1521
+ elif op == "worker.telemetry":
1522
+ result = _op_worker_telemetry(
1523
+ params, _timeout_seconds(request, DEFAULT_TIMEOUT_S),
1524
+ )
1525
+ elif op == "worker.wait":
1526
+ budget = max(0.0, deadline - time.monotonic())
1527
+ result = _op_worker_wait(params, budget)
1528
+ elif op == "conversation.page":
1529
+ remaining = deadline - time.monotonic()
1530
+ if remaining <= 0:
1531
+ raise OperationTimeout("conversation page request deadline expired")
1532
+ result = _op_conversation_page(params, remaining)
1533
+ elif op == "transcript.read":
1534
+ remaining = deadline - time.monotonic()
1535
+ if remaining <= 0:
1536
+ raise OperationTimeout("transcript read request deadline expired")
1537
+ result = _op_transcript_read(params, remaining)
1538
+ elif op == "worker.inspect":
1539
+ target = params.get("target")
1540
+ if not isinstance(target, str) or not target:
1541
+ raise InvalidRequest("Target must be a complete CCX worker handle")
1542
+ result = recorded_inspector(store, target)
1543
+ else:
1544
+ target = params.get("target")
1545
+ if not isinstance(target, str) or not target:
1546
+ raise InvalidRequest("Target must be a complete CCX worker handle")
1547
+ wait_ms = params.get("wait_ms", 0)
1548
+ if type(wait_ms) is not int or wait_ms < 0:
1549
+ raise InvalidRequest("worker.events wait_ms must be a nonnegative integer")
1550
+ remaining = max(0.0, deadline - time.monotonic())
1551
+ reserve = min(1.0, remaining / 4)
1552
+ result = wait_full_uuid_event_page(
1553
+ store, target,
1554
+ after=params.get("after_cursor"), tail=params.get("tail"),
1555
+ limit=params.get("limit", 200), kinds=params.get("kinds"),
1556
+ wait=min(wait_ms / 1000.0, max(0.0, remaining - reserve)),
1557
+ )
1558
+ store.revalidate()
1559
+ return _success(request_id, _rpc_result(op, result, store, params))
1560
+ except EventCursorInvalid as error:
1561
+ return _error(request_id, "cursor_invalidated", str(error))
1562
+ except UnknownWorker as error:
1563
+ return _error(request_id, "not_found", str(error))
1564
+ except Exception as error:
1565
+ from ccx.store.root import NativeEffectIndeterminate
1566
+ from ccx.claude.delivery import ClaudeDeliveryConflict
1567
+ from ccx.codex.delivery import DeliveryReceiptConflict
1568
+ from ccx.errors import CcxError
1569
+ if isinstance(error, NativeEffectIndeterminate):
1570
+ params = request.get("params") if isinstance(request,dict) else {}
1571
+ params = params if isinstance(params,dict) else {}
1572
+ return _error(
1573
+ request_id, "reconcile_required", str(error),
1574
+ details={
1575
+ "delivery_id": params.get("delivery_id"),
1576
+ "delivery_status": "submitted-unknown",
1577
+ "target": params.get("target"),
1578
+ "recommended_action": "reconcile_original_never_resend",
1579
+ },
1580
+ )
1581
+ if isinstance(error,(ClaudeDeliveryConflict,DeliveryReceiptConflict)):
1582
+ return _error(request_id,"conflict",str(error))
1583
+ if isinstance(error,CcxError):
1584
+ try:
1585
+ engine = (getattr(error, "engine", None)
1586
+ or params.get("engine")
1587
+ or store.resolve(params["target"]).engine
1588
+ if isinstance(params, dict) and params.get("target")
1589
+ else getattr(error, "engine", None)
1590
+ or (params.get("engine") if isinstance(params, dict) else None))
1591
+ except Exception:
1592
+ engine = getattr(error, "engine", None)
1593
+ return _error_from_exception(request_id,error,
1594
+ operation=op, engine=engine)
1595
+ if isinstance(error, (InvalidTarget, EngineMismatch, InvalidRequest)):
1596
+ return _error(request_id,"invalid_request",str(error))
1597
+ if isinstance(error,(StaleGeneration,GenerationError)):
1598
+ return _error(request_id,"state_unavailable",str(error))
1599
+ return _error(
1600
+ request_id,"internal_error",
1601
+ f"{error.__class__.__name__}: {error}",
1602
+ )