ccx-native 0.0.1 → 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 -4
- package/bin/ccx +55 -2
- 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 +39 -7
- 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,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
|
+
)
|