@oneciel-ai/ciel-runtime 0.2.37 → 0.2.39
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/CHANGELOG.md +85 -0
- package/README.md +5 -2
- package/ciel_runtime.py +94 -96
- package/ciel_runtime_support/anthropic_model_policy.py +20 -3
- package/ciel_runtime_support/architecture.py +5 -0
- package/ciel_runtime_support/channel_inflight.py +4 -4
- package/ciel_runtime_support/channel_injection.py +56 -1
- package/ciel_runtime_support/channel_mcp_tools.py +34 -0
- package/ciel_runtime_support/channel_message_policy.py +9 -1
- package/ciel_runtime_support/channel_message_prompt.py +23 -0
- package/ciel_runtime_support/channel_pending_injection.py +86 -32
- package/ciel_runtime_support/channel_pending_poll.py +6 -2
- package/ciel_runtime_support/channel_runtime_environment.py +0 -11
- package/ciel_runtime_support/channel_terminal_context.py +0 -6
- package/ciel_runtime_support/channel_terminal_proxy.py +3 -42
- package/ciel_runtime_support/channel_transcript_repository.py +16 -1
- package/ciel_runtime_support/channel_wake_claim_repository.py +0 -19
- package/ciel_runtime_support/channel_wake_context.py +6 -13
- package/ciel_runtime_support/channel_wake_delivery_repository.py +50 -0
- package/ciel_runtime_support/chat_http_controller.py +88 -5
- package/ciel_runtime_support/claude_environment.py +100 -3
- package/ciel_runtime_support/claude_launch_assembly.py +4 -0
- package/ciel_runtime_support/claude_session_socket.py +181 -0
- package/ciel_runtime_support/cli_application_context.py +18 -10
- package/ciel_runtime_support/cli_dispatch.py +19 -3
- package/ciel_runtime_support/cli_parser.py +2 -0
- package/ciel_runtime_support/cli_usage.py +3 -2
- package/ciel_runtime_support/codex_backend_context.py +8 -3
- package/ciel_runtime_support/codex_completion_gate.py +198 -0
- package/ciel_runtime_support/codex_turn_recovery.py +240 -83
- package/ciel_runtime_support/compatibility_protocol.py +5 -2
- package/ciel_runtime_support/config_migrations.py +79 -0
- package/ciel_runtime_support/config_repository.py +7 -0
- package/ciel_runtime_support/context_summary_policy.py +24 -4
- package/ciel_runtime_support/external_event_menu.py +107 -0
- package/ciel_runtime_support/external_event_receiver.py +19 -1
- package/ciel_runtime_support/launch_state.py +2 -0
- package/ciel_runtime_support/managed_tool_injection.py +81 -0
- package/ciel_runtime_support/muse_runtime_context.py +291 -0
- package/ciel_runtime_support/ollama_thinking.py +12 -26
- package/ciel_runtime_support/prelaunch.py +13 -0
- package/ciel_runtime_support/prelaunch_launch_panel.py +2 -1
- package/ciel_runtime_support/prompt_compaction.py +39 -0
- package/ciel_runtime_support/provider_files_proxy.py +248 -0
- package/ciel_runtime_support/provider_model_identity.py +10 -2
- package/ciel_runtime_support/provider_option_cli.py +1 -1
- package/ciel_runtime_support/provider_request_access.py +54 -1
- package/ciel_runtime_support/provider_responses_passthrough.py +152 -15
- package/ciel_runtime_support/providers/alibaba.py +34 -1
- package/ciel_runtime_support/providers/anthropic.py +18 -1
- package/ciel_runtime_support/providers/meta.py +252 -21
- package/ciel_runtime_support/pseudo_tool_parser.py +17 -1
- package/ciel_runtime_support/remote_bridge.py +4 -0
- package/ciel_runtime_support/remote_instructions.py +5 -0
- package/ciel_runtime_support/remote_memory.py +1 -1
- package/ciel_runtime_support/responses_cache_diagnostics.py +82 -0
- package/ciel_runtime_support/responses_custom_tool_bridge.py +282 -0
- package/ciel_runtime_support/responses_input_compatibility.py +19 -3
- package/ciel_runtime_support/router_http.py +301 -4
- package/ciel_runtime_support/router_observability_context.py +26 -0
- package/ciel_runtime_support/router_server_context.py +1 -0
- package/ciel_runtime_support/runtime_adapters.py +31 -0
- package/ciel_runtime_support/runtime_constants.py +23 -1
- package/ciel_runtime_support/runtime_input_gateway.py +85 -5
- package/ciel_runtime_support/runtime_input_status.py +150 -0
- package/ciel_runtime_support/runtime_launch.py +35 -4
- package/ciel_runtime_support/runtime_paths.py +1 -0
- package/ciel_runtime_support/speech_http_controller.py +3 -2
- package/ciel_runtime_support/streaming_anthropic.py +3 -0
- package/ciel_runtime_support/tool_call_events.py +98 -0
- package/ciel_runtime_support/transcript_delta_delivery.py +140 -1
- package/ciel_runtime_support/ui_text.py +1 -0
- package/ciel_runtime_support/web_search_result_events.py +130 -0
- package/ciel_runtime_support/web_ui.py +1 -0
- package/ciel_runtime_support/windows_conpty.py +92 -8
- package/ciel_runtime_support/workspace_mcp.py +4 -1
- package/ciel_runtime_support/workspace_state.py +1 -0
- package/docs/Configuration.md +20 -4
- package/docs/MCP-Channels.md +79 -6
- package/docs/Managed-Tool-Injection.md +25 -0
- package/docs/Module-Map.md +4 -0
- package/docs/Muse-Code.md +73 -0
- package/docs/Observability.md +39 -0
- package/docs/Providers.md +29 -2
- package/docs/Remote-Bridge.md +12 -0
- package/docs/Router.md +1 -0
- package/docs/journal/2026/09/01/diagnostics/anthropic/router/fable51-usage-credits.okf +70 -0
- package/docs/journal/2026/09/01/implementation/claude/session-socket/all-input-paths/default-delivery.okf +133 -0
- package/docs/journal/2026/09/01/implementation/claude/session-socket/windows-direct-input.okf +112 -0
- package/docs/journal/2026/09/02/diagnostics/cache/alibaba/qwen38-post-restart-hit-rate.okf +107 -0
- package/docs/journal/2026/09/02/diagnostics/cache/ollama-cloud/kimi-k3-codex-hit-rate.okf +91 -0
- package/docs/journal/2026/09/02/diagnostics/codex/tui/statusline-cache-metrics.okf +44 -0
- package/docs/journal/2026/09/02/diagnostics/input/transport/fallback-behavior.okf +31 -0
- package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-agent-turn-recovery.okf +125 -0
- package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-parameters.okf +128 -0
- package/docs/journal/2026/09/02/diagnostics/remote/mia/socket-tui-visibility.okf +47 -0
- package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-latency.okf +108 -0
- package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-vs-gpt56-latency.okf +92 -0
- package/docs/journal/2026/09/02/implementation/anthropic/context/one-million-defaults.okf +85 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.okf +104 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.png +0 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/responses-session-cache.okf +97 -0
- package/docs/journal/2026/09/02/implementation/providers/alibaba/qwen38/0902-parameter-alignment.okf +97 -0
- package/docs/journal/2026/09/02/implementation/providers/meta/multimodal-tools/contributor-protocol-adoption.okf +140 -0
- package/docs/journal/2026/09/02/implementation/providers/meta/muse-spark-1.3-support.okf +53 -0
- package/docs/journal/2026/09/02/monitoring/cache/alibaba/cielarvis-qwen38-live-hit-rate.okf +858 -0
- package/docs/journal/2026/09/02/operations/release/main-merge-local-deploy.okf +36 -0
- package/docs/journal/2026/09/02/operations/release/nightly/local-cache-deployment.okf +54 -0
- package/docs/journal/2026/09/02/operations/release/nightly/ollama-kimi-recovery-model-audit-deployment.okf +58 -0
- package/docs/journal/2026/09/02/operations/release/nightly/qwen38-cache-improvement-deployment.okf +72 -0
- package/docs/journal/2026/09/02/research/cache/alibaba/qwen38-hit-rate-improvement.okf +233 -0
- package/docs/journal/2026/09/02/research/cache/codex/provider-scope.okf +77 -0
- package/docs/journal/2026/09/02/research/providers/ollama-cloud/desktop-model-exhaustive-audit.okf +159 -0
- package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-cache-hit.okf +148 -0
- package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-required-schema.okf +127 -0
- package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-resumed-session-stall.okf +160 -0
- package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-substantive-dangling-action.okf +94 -0
- package/docs/journal/2026/09/03/diagnostics/remote/kevin/codex-gpt-early-turn-completion.okf +162 -0
- package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/default-yolo.okf +40 -0
- package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/native-subscription-router.okf +86 -0
- package/docs/journal/2026/09/03/operations/deployment/local/session-socket-default-fallback.okf +41 -0
- package/docs/journal/2026/09/03/research/deployment/colab/tailscale/credential-storage.okf +41 -0
- package/docs/journal/2026/09/03/research/providers/meta/muse-code/subscription-billing-boundary.okf +64 -0
- package/docs/journal/2026/09/03/research/web-chat/voice/instruction-format.okf +135 -0
- package/docs/journal/2026/09/04/diagnostics/claude/context/early-auto-compaction.okf +75 -0
- package/docs/journal/2026/09/04/diagnostics/claude/context/fable-51-status-200k.okf +54 -0
- package/docs/journal/2026/09/04/diagnostics/runtime/codex/responses/replayed-item-id-validation.okf +74 -0
- package/docs/journal/2026/09/04/diagnostics/workspaces/onecieldmsui/codex/replay-stall.okf +100 -0
- package/docs/journal/2026/09/04/implementation/observability/tool-calls/websocket-stream.okf +49 -0
- package/docs/journal/2026/09/04/implementation/web-chat/input/raw-injection.okf +42 -0
- package/docs/journal/2026/09/05/diagnostics/tty/claude-raw-idle-render.okf +25 -0
- package/docs/journal/2026/09/05/implementation/events/search-response-urls.okf +23 -0
- package/docs/journal/2026/09/05/implementation/tools/native-injection.okf +19 -0
- package/docs/journal/2026/09/05/implementation/windows/conpty/prompt-delivery-lifecycle.okf +43 -0
- package/docs/journal/2026/09/05/release/nightly/conpty-native-tools.okf +13 -0
- package/docs/journal/2026/09/05/release/nightly/search-result-urls.okf +14 -0
- package/docs/journal/2026/09/06/implementation/codex/inherited-web-mcp.okf +22 -0
- package/docs/journal/2026/09/07/fixes/codex/native-web-transport.okf +39 -0
- package/docs/journal/2026/09/07/integration/main/nightly-merge.okf +13 -0
- package/docs/journal/2026/09/07/release/stable/0.2.38.okf +10 -0
- package/docs/journal/2026/09/07/release/stable/0.2.39.okf +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Claude Code cross-session socket transport for admitted runtime inputs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import posixpath
|
|
10
|
+
import re
|
|
11
|
+
import secrets
|
|
12
|
+
import socket
|
|
13
|
+
import threading
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Any, Callable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
SESSION_KEY_LIMIT = 4096
|
|
19
|
+
TOKEN_HEX_LENGTH = 32
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generated_socket_path(platform_name: str | None = None) -> str:
|
|
23
|
+
platform_name = platform_name or os.name
|
|
24
|
+
nonce = secrets.token_hex(16)
|
|
25
|
+
if platform_name == "nt":
|
|
26
|
+
return rf"\\.\pipe\LOCAL\cc-msg-{nonce}"
|
|
27
|
+
uid = os.getuid() if hasattr(os, "getuid") else 0
|
|
28
|
+
temp_root = os.environ.get("TMPDIR") or "/tmp"
|
|
29
|
+
if not temp_root.startswith("/"):
|
|
30
|
+
temp_root = "/tmp"
|
|
31
|
+
return posixpath.join(temp_root, f"cc-socks-{uid}", f"ciel-{nonce}.sock")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def canonical_socket_path(path: str, platform_name: str | None = None) -> str:
|
|
35
|
+
if (platform_name or os.name) == "nt":
|
|
36
|
+
match = re.fullmatch(
|
|
37
|
+
r"[\\/]{2}[.?][\\/]pipe[\\/](?:(LOCAL)[\\/])?([^\\/]+)",
|
|
38
|
+
path,
|
|
39
|
+
flags=re.IGNORECASE,
|
|
40
|
+
)
|
|
41
|
+
if match is None:
|
|
42
|
+
return path
|
|
43
|
+
local_prefix = "LOCAL\\" if match.group(1) else ""
|
|
44
|
+
canonical = rf"\\.\pipe\{local_prefix}{match.group(2)}"
|
|
45
|
+
return re.sub(r"[A-Z]", lambda value: value.group(0).lower(), canonical)
|
|
46
|
+
return str(Path(path).expanduser().resolve(strict=False))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def session_key_hash(path: str, platform_name: str | None = None) -> str:
|
|
50
|
+
canonical = canonical_socket_path(path, platform_name)
|
|
51
|
+
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def prepared_socket_path(config: dict[str, Any], passthrough: list[str]) -> str:
|
|
55
|
+
for index, value in enumerate(passthrough):
|
|
56
|
+
if value == "--messaging-socket-path" and index + 1 < len(passthrough):
|
|
57
|
+
return str(passthrough[index + 1])
|
|
58
|
+
text = str(value)
|
|
59
|
+
if text.startswith("--messaging-socket-path="):
|
|
60
|
+
return text.partition("=")[2]
|
|
61
|
+
claude_config = config.get("claude_code") if isinstance(config, dict) else {}
|
|
62
|
+
if isinstance(claude_config, dict) and claude_config.get("session_socket_input") is False:
|
|
63
|
+
return ""
|
|
64
|
+
return generated_socket_path()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ClaudeSessionSocketClient:
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
home: Path,
|
|
71
|
+
log: Callable[[str, str], None],
|
|
72
|
+
*,
|
|
73
|
+
platform_name: str | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
self._home = home
|
|
76
|
+
self._log = log
|
|
77
|
+
self._platform_name = platform_name or os.name
|
|
78
|
+
self._target = ""
|
|
79
|
+
self._lock = threading.Lock()
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def target(self) -> str:
|
|
83
|
+
with self._lock:
|
|
84
|
+
return self._target
|
|
85
|
+
|
|
86
|
+
def configure(self, path: str | None) -> None:
|
|
87
|
+
target = str(path or "").strip()
|
|
88
|
+
with self._lock:
|
|
89
|
+
self._target = target
|
|
90
|
+
self._log(
|
|
91
|
+
"INFO",
|
|
92
|
+
"claude_session_socket_%s%s"
|
|
93
|
+
% ("configured" if target else "cleared", f" target={target}" if target else ""),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def available(self) -> bool:
|
|
97
|
+
return bool(self.target)
|
|
98
|
+
|
|
99
|
+
def send(self, prompt: str, _messages: list[dict[str, Any]] | None = None) -> bool:
|
|
100
|
+
target = self.target
|
|
101
|
+
if not target:
|
|
102
|
+
self._log("INFO", "claude_session_socket_deferred reason=target_unconfigured")
|
|
103
|
+
return False
|
|
104
|
+
token = self._read_peer_token(target)
|
|
105
|
+
if not token:
|
|
106
|
+
self._log("INFO", "claude_session_socket_deferred reason=auth_key_unavailable")
|
|
107
|
+
return False
|
|
108
|
+
frames = (
|
|
109
|
+
json.dumps({"type": "auth", "token": token}, separators=(",", ":"))
|
|
110
|
+
+ "\n"
|
|
111
|
+
+ json.dumps(
|
|
112
|
+
{
|
|
113
|
+
"type": "user",
|
|
114
|
+
"uuid": str(uuid.uuid4()),
|
|
115
|
+
"message": {"role": "user", "content": prompt},
|
|
116
|
+
"priority": "next",
|
|
117
|
+
},
|
|
118
|
+
ensure_ascii=False,
|
|
119
|
+
separators=(",", ":"),
|
|
120
|
+
)
|
|
121
|
+
+ "\n"
|
|
122
|
+
).encode("utf-8")
|
|
123
|
+
try:
|
|
124
|
+
self._write(target, frames)
|
|
125
|
+
except OSError as exc:
|
|
126
|
+
self._log(
|
|
127
|
+
"INFO",
|
|
128
|
+
f"claude_session_socket_deferred reason=connect_failed error={type(exc).__name__}: {exc}",
|
|
129
|
+
)
|
|
130
|
+
return False
|
|
131
|
+
self._log(
|
|
132
|
+
"INFO",
|
|
133
|
+
f"claude_session_socket_injected bytes={len(frames)} target={target}",
|
|
134
|
+
)
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
def _read_peer_token(self, target: str) -> str:
|
|
138
|
+
digest = session_key_hash(target, self._platform_name)
|
|
139
|
+
directory = self._home / ".claude" / "sessions"
|
|
140
|
+
try:
|
|
141
|
+
candidates = sorted(
|
|
142
|
+
directory.glob(f"*.{digest}.key"),
|
|
143
|
+
key=lambda item: item.stat().st_mtime_ns,
|
|
144
|
+
reverse=True,
|
|
145
|
+
)
|
|
146
|
+
except OSError:
|
|
147
|
+
return ""
|
|
148
|
+
for path in candidates:
|
|
149
|
+
try:
|
|
150
|
+
if path.stat().st_size > SESSION_KEY_LIMIT:
|
|
151
|
+
continue
|
|
152
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
153
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
154
|
+
continue
|
|
155
|
+
token = str(payload.get("peerToken") or "") if isinstance(payload, dict) else ""
|
|
156
|
+
if len(token) == TOKEN_HEX_LENGTH and all(char in "0123456789abcdefABCDEF" for char in token):
|
|
157
|
+
return token
|
|
158
|
+
return ""
|
|
159
|
+
|
|
160
|
+
def _write(self, target: str, frames: bytes) -> None:
|
|
161
|
+
if self._platform_name == "nt":
|
|
162
|
+
with open(target, "wb", buffering=0) as stream:
|
|
163
|
+
stream.write(frames)
|
|
164
|
+
return
|
|
165
|
+
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
166
|
+
try:
|
|
167
|
+
client.settimeout(2.0)
|
|
168
|
+
client.connect(target)
|
|
169
|
+
client.sendall(frames)
|
|
170
|
+
client.shutdown(socket.SHUT_WR)
|
|
171
|
+
finally:
|
|
172
|
+
client.close()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
__all__ = [
|
|
176
|
+
"ClaudeSessionSocketClient",
|
|
177
|
+
"canonical_socket_path",
|
|
178
|
+
"generated_socket_path",
|
|
179
|
+
"prepared_socket_path",
|
|
180
|
+
"session_key_hash",
|
|
181
|
+
]
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from typing import Any, Callable, Sequence
|
|
7
|
+
from typing import Any, Callable, Mapping, Sequence
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@dataclass(frozen=True, slots=True)
|
|
@@ -17,8 +17,7 @@ class CliApplicationDispatchPorts:
|
|
|
17
17
|
launch_agy: Callable[[list[str]], int]
|
|
18
18
|
launch_kimi: Callable[[list[str]], int]
|
|
19
19
|
kimi_login: Callable[[], int]
|
|
20
|
-
|
|
21
|
-
launch_zcode: Callable[[list[str]], int] = lambda _argv: 127
|
|
20
|
+
extra_launchers: Mapping[str, Callable[[list[str]], int]]
|
|
22
21
|
|
|
23
22
|
|
|
24
23
|
@dataclass(frozen=True, slots=True)
|
|
@@ -59,10 +58,13 @@ class CliApplicationContext:
|
|
|
59
58
|
raise SystemExit(self.dispatch.launch_agy(args.argv))
|
|
60
59
|
|
|
61
60
|
def cmd_launch_grok(self, args: argparse.Namespace) -> None:
|
|
62
|
-
raise SystemExit(self.dispatch.
|
|
61
|
+
raise SystemExit(self.dispatch.extra_launchers["grok"](args.argv))
|
|
63
62
|
|
|
64
63
|
def cmd_launch_zcode(self, args: argparse.Namespace) -> None:
|
|
65
|
-
raise SystemExit(self.dispatch.
|
|
64
|
+
raise SystemExit(self.dispatch.extra_launchers["zcode"](args.argv))
|
|
65
|
+
|
|
66
|
+
def cmd_launch_muse(self, args: argparse.Namespace) -> None:
|
|
67
|
+
raise SystemExit(self.dispatch.extra_launchers["muse"](args.argv))
|
|
66
68
|
|
|
67
69
|
def cmd_version(self, _: argparse.Namespace) -> None:
|
|
68
70
|
self.presentation.output(f"ciel-runtime {self.presentation.version}")
|
|
@@ -85,11 +87,14 @@ class CliApplicationContext:
|
|
|
85
87
|
"kimi": self.dispatch.launch_kimi,
|
|
86
88
|
"kimi-code": self.dispatch.launch_kimi,
|
|
87
89
|
"launch-kimi": self.dispatch.launch_kimi,
|
|
88
|
-
"grok": self.dispatch.
|
|
89
|
-
"grok-build": self.dispatch.
|
|
90
|
-
"launch-grok": self.dispatch.
|
|
91
|
-
"zcode": self.dispatch.
|
|
92
|
-
"launch-zcode": self.dispatch.
|
|
90
|
+
"grok": self.dispatch.extra_launchers["grok"],
|
|
91
|
+
"grok-build": self.dispatch.extra_launchers["grok"],
|
|
92
|
+
"launch-grok": self.dispatch.extra_launchers["grok"],
|
|
93
|
+
"zcode": self.dispatch.extra_launchers["zcode"],
|
|
94
|
+
"launch-zcode": self.dispatch.extra_launchers["zcode"],
|
|
95
|
+
"muse": self.dispatch.extra_launchers["muse"],
|
|
96
|
+
"muse-code": self.dispatch.extra_launchers["muse"],
|
|
97
|
+
"launch-muse": self.dispatch.extra_launchers["muse"],
|
|
93
98
|
}
|
|
94
99
|
if len(arguments) >= 2 and arguments[1] in routes:
|
|
95
100
|
raise SystemExit(routes[arguments[1]](arguments[2:]))
|
|
@@ -133,6 +138,9 @@ class CliApplicationCompatibilityApi:
|
|
|
133
138
|
def cmd_launch_zcode(self, args: argparse.Namespace) -> None:
|
|
134
139
|
self.context().cmd_launch_zcode(args)
|
|
135
140
|
|
|
141
|
+
def cmd_launch_muse(self, args: argparse.Namespace) -> None:
|
|
142
|
+
self.context().cmd_launch_muse(args)
|
|
143
|
+
|
|
136
144
|
def cmd_version(self, args: argparse.Namespace) -> None:
|
|
137
145
|
self.context().cmd_version(args)
|
|
138
146
|
|
|
@@ -35,6 +35,7 @@ class CliRuntime:
|
|
|
35
35
|
native_codex_enabled: Any
|
|
36
36
|
launch_grok: Any = lambda *_args, **_kwargs: 127
|
|
37
37
|
launch_zcode: Any = lambda *_args, **_kwargs: 127
|
|
38
|
+
launch_muse: Any = lambda *_args, **_kwargs: 127
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
@dataclass(frozen=True)
|
|
@@ -123,6 +124,7 @@ def dispatch_cli(argv: list[str], services: CliServices) -> int:
|
|
|
123
124
|
launch_codex_app_server = services.runtime.launch_codex_app_server
|
|
124
125
|
launch_grok = services.runtime.launch_grok
|
|
125
126
|
launch_zcode = services.runtime.launch_zcode
|
|
127
|
+
launch_muse = services.runtime.launch_muse
|
|
126
128
|
load_config = services.core.load_config
|
|
127
129
|
native_agy_enabled = services.runtime.native_agy_enabled
|
|
128
130
|
native_codex_enabled = services.runtime.native_codex_enabled
|
|
@@ -148,6 +150,8 @@ def dispatch_cli(argv: list[str], services: CliServices) -> int:
|
|
|
148
150
|
return launch_grok(rest)
|
|
149
151
|
if head in ("zcode", "launch-zcode"):
|
|
150
152
|
return launch_zcode(rest)
|
|
153
|
+
if head in ("muse", "muse-code", "launch-muse"):
|
|
154
|
+
return launch_muse(rest)
|
|
151
155
|
if head in ("version", "--version", "-v"):
|
|
152
156
|
print(f"ciel-runtime {VERSION}")
|
|
153
157
|
return 0
|
|
@@ -278,6 +282,8 @@ def dispatch_cli(argv: list[str], services: CliServices) -> int:
|
|
|
278
282
|
return launch_codex(argv)
|
|
279
283
|
if runtime == "agy":
|
|
280
284
|
return launch_agy(argv)
|
|
285
|
+
if runtime == "muse":
|
|
286
|
+
return launch_muse(argv)
|
|
281
287
|
if codex_passthrough_has_command(argv):
|
|
282
288
|
cfg = load_config()
|
|
283
289
|
provider, _ = get_current_provider(cfg)
|
|
@@ -324,8 +330,10 @@ def dispatch_cli(argv: list[str], services: CliServices) -> int:
|
|
|
324
330
|
runtime = str(value or "").strip().lower()
|
|
325
331
|
if runtime in ("codex-app", "codex-appserver"):
|
|
326
332
|
runtime = "codex-app-server"
|
|
327
|
-
if runtime
|
|
328
|
-
|
|
333
|
+
if runtime in ("muse-code", "musecode"):
|
|
334
|
+
runtime = "muse"
|
|
335
|
+
if runtime not in ("claude", "codex", "codex-app-server", "agy", "grok", "zcode", "muse", "last"):
|
|
336
|
+
raise SystemExit("--ca-runtime must be claude, codex, codex-app-server, agy, grok, zcode, muse, or last")
|
|
329
337
|
elif arg in ("--ca-no-launch", "--ca-configure-only", "--ca-setup-only"):
|
|
330
338
|
configure_only = True
|
|
331
339
|
skip_menu = True
|
|
@@ -696,7 +704,7 @@ def dispatch_cli(argv: list[str], services: CliServices) -> int:
|
|
|
696
704
|
return 0
|
|
697
705
|
if runtime == "last":
|
|
698
706
|
remembered_runtime = str(last_launch_runtime() or "").strip().lower()
|
|
699
|
-
runtime = remembered_runtime if remembered_runtime in {"claude", "codex", "agy", "grok", "zcode"} else "claude"
|
|
707
|
+
runtime = remembered_runtime if remembered_runtime in {"claude", "codex", "agy", "grok", "zcode", "muse"} else "claude"
|
|
700
708
|
if runtime == "agy":
|
|
701
709
|
return launch_agy(
|
|
702
710
|
passthrough,
|
|
@@ -737,6 +745,14 @@ def dispatch_cli(argv: list[str], services: CliServices) -> int:
|
|
|
737
745
|
update_check=update_check,
|
|
738
746
|
self_update_check=self_update_check,
|
|
739
747
|
)
|
|
748
|
+
if runtime == "muse":
|
|
749
|
+
return launch_muse(
|
|
750
|
+
passthrough,
|
|
751
|
+
skip_menu=skip_menu,
|
|
752
|
+
force_menu=force_menu,
|
|
753
|
+
update_check=update_check,
|
|
754
|
+
self_update_check=self_update_check,
|
|
755
|
+
)
|
|
740
756
|
return launch_claude(
|
|
741
757
|
passthrough,
|
|
742
758
|
skip_menu=skip_menu,
|
|
@@ -18,6 +18,7 @@ class CliParserLaunch:
|
|
|
18
18
|
remote_bridge: CliHandler
|
|
19
19
|
launch_grok: CliHandler = lambda _args: None
|
|
20
20
|
launch_zcode: CliHandler = lambda _args: None
|
|
21
|
+
launch_muse: CliHandler = lambda _args: None
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
@dataclass(frozen=True)
|
|
@@ -87,6 +88,7 @@ def build_cli_parser(services: CliParserServices) -> argparse.ArgumentParser:
|
|
|
87
88
|
_add_remainder_command(commands, "launch-agy", services.launch.launch_agy)
|
|
88
89
|
_add_remainder_command(commands, "launch-grok", services.launch.launch_grok)
|
|
89
90
|
_add_remainder_command(commands, "launch-zcode", services.launch.launch_zcode)
|
|
91
|
+
_add_remainder_command(commands, "launch-muse", services.launch.launch_muse)
|
|
90
92
|
commands.add_parser("serve").set_defaults(func=services.launch.serve)
|
|
91
93
|
bridge = commands.add_parser("bridge")
|
|
92
94
|
bridge.add_argument(
|
|
@@ -8,6 +8,7 @@ def cli_usage_text() -> str:
|
|
|
8
8
|
ciel-runtime agy [args...] Launch Google Antigravity CLI through ciel-runtime
|
|
9
9
|
ciel-runtime grok [args...] Launch the official Grok Build CLI
|
|
10
10
|
ciel-runtime zcode [args...] Launch ZCode through ciel-runtime router
|
|
11
|
+
ciel-runtime muse [args...] Launch Muse Code with its subscription login
|
|
11
12
|
ciel-runtime resume Resume Codex/AGY when that runtime provider is selected
|
|
12
13
|
|
|
13
14
|
Control plane, runs before Claude Code and does not require LLM connectivity:
|
|
@@ -45,8 +46,8 @@ Headless setup flags, namespaced to avoid Claude CLI collisions:
|
|
|
45
46
|
Publish localhost through Tailscale Serve HTTPS
|
|
46
47
|
ciel-runtime --ca-provider PROVIDER Set provider, then launch
|
|
47
48
|
ciel-runtime --ca-env-file PATH Load CIEL_RUNTIME_* values from a .env file
|
|
48
|
-
ciel-runtime --ca-runtime claude|codex|codex-app-server|agy|grok|zcode
|
|
49
|
-
Select Claude Code, Codex, Codex app-server, AGY, or
|
|
49
|
+
ciel-runtime --ca-runtime claude|codex|codex-app-server|agy|grok|zcode|muse
|
|
50
|
+
Select Claude Code, Codex, Codex app-server, AGY, Grok Build, ZCode, or Muse Code
|
|
50
51
|
ciel-runtime --ca-menu Apply setup values, then open the menu
|
|
51
52
|
ciel-runtime --ca-language en|ko|ja|zh
|
|
52
53
|
ciel-runtime --ca-base-url URL Set current provider base URL, then launch
|
|
@@ -236,16 +236,19 @@ class CodexBackendContext:
|
|
|
236
236
|
provider: str,
|
|
237
237
|
pcfg: dict[str, Any],
|
|
238
238
|
body: dict[str, Any],
|
|
239
|
+
protocol: str | None = None,
|
|
239
240
|
) -> dict[str, Any]:
|
|
240
241
|
return self.provider_projection.apply_request_policy(
|
|
241
|
-
provider, pcfg, dict(body)
|
|
242
|
+
provider, pcfg, dict(body), protocol
|
|
242
243
|
)
|
|
243
244
|
|
|
244
245
|
def chat_passthrough(self) -> OpenAIChatPassthrough:
|
|
245
246
|
return OpenAIChatPassthrough(
|
|
246
247
|
OpenAIChatPassthroughPorts(
|
|
247
248
|
normalize_model=self.normalize_model,
|
|
248
|
-
normalize_request=self.normalize_request
|
|
249
|
+
normalize_request=lambda provider, pcfg, body: self.normalize_request(
|
|
250
|
+
provider, pcfg, body, "openai_chat"
|
|
251
|
+
),
|
|
249
252
|
upstream_base=self.provider_transport.upstream_base,
|
|
250
253
|
join_url=self.provider_transport.join_url,
|
|
251
254
|
headers=self.provider_projection.chat_headers,
|
|
@@ -266,7 +269,9 @@ class CodexBackendContext:
|
|
|
266
269
|
project_channel_context=self.project_channel_context,
|
|
267
270
|
begin_channel_delivery=self.channel.begin_delivery,
|
|
268
271
|
normalize_model=self.normalize_model,
|
|
269
|
-
normalize_request=self.normalize_request
|
|
272
|
+
normalize_request=lambda provider, pcfg, body: self.normalize_request(
|
|
273
|
+
provider, pcfg, body, "openai_responses"
|
|
274
|
+
),
|
|
270
275
|
upstream_base=self.provider_transport.upstream_base,
|
|
271
276
|
join_url=self.provider_transport.join_url,
|
|
272
277
|
headers=self.provider_projection.responses_headers,
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Language-independent completion validation for Responses agent turns."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .codex_turn_recovery import (
|
|
11
|
+
CODEX_COMPLETION_TOOL_NAME,
|
|
12
|
+
CODEX_STRICT_CONTINUATION_NUDGE,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_MAX_SSE_EVENT_BYTES = 8 * 1024 * 1024
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _event_payload(block: bytes) -> dict[str, Any] | None:
|
|
20
|
+
data = b"\n".join(
|
|
21
|
+
line[5:].lstrip()
|
|
22
|
+
for line in block.splitlines()
|
|
23
|
+
if line.startswith(b"data:")
|
|
24
|
+
)
|
|
25
|
+
if not data or data == b"[DONE]":
|
|
26
|
+
return None
|
|
27
|
+
try:
|
|
28
|
+
decoded = json.loads(data)
|
|
29
|
+
except (UnicodeDecodeError, ValueError):
|
|
30
|
+
return None
|
|
31
|
+
return decoded if isinstance(decoded, dict) else None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _message_text(item: dict[str, Any]) -> str:
|
|
35
|
+
if item.get("type") != "message":
|
|
36
|
+
return ""
|
|
37
|
+
parts: list[str] = []
|
|
38
|
+
for part in item.get("content") or []:
|
|
39
|
+
if not isinstance(part, dict):
|
|
40
|
+
continue
|
|
41
|
+
if part.get("type") not in {"output_text", "text", "refusal"}:
|
|
42
|
+
continue
|
|
43
|
+
value = part.get("text") or part.get("refusal")
|
|
44
|
+
if isinstance(value, str):
|
|
45
|
+
parts.append(value)
|
|
46
|
+
return "\n".join(parts)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(slots=True)
|
|
50
|
+
class ResponsesCompletionObservation:
|
|
51
|
+
"""Incrementally retain only terminal Responses protocol state."""
|
|
52
|
+
|
|
53
|
+
response_id: str = ""
|
|
54
|
+
status: str = ""
|
|
55
|
+
output: list[dict[str, Any]] = field(default_factory=list)
|
|
56
|
+
parseable: bool = True
|
|
57
|
+
_pending: bytearray = field(default_factory=bytearray, repr=False)
|
|
58
|
+
_items: dict[int, dict[str, Any]] = field(default_factory=dict, repr=False)
|
|
59
|
+
|
|
60
|
+
def feed(self, chunk: bytes) -> None:
|
|
61
|
+
if not self.parseable or not chunk:
|
|
62
|
+
return
|
|
63
|
+
self._pending.extend(chunk)
|
|
64
|
+
if (
|
|
65
|
+
len(self._pending) > _MAX_SSE_EVENT_BYTES
|
|
66
|
+
and b"\n\n" not in self._pending
|
|
67
|
+
and b"\r\n\r\n" not in self._pending
|
|
68
|
+
):
|
|
69
|
+
self.parseable = False
|
|
70
|
+
self._pending.clear()
|
|
71
|
+
return
|
|
72
|
+
while True:
|
|
73
|
+
candidates = [
|
|
74
|
+
(index, len(separator))
|
|
75
|
+
for separator in (b"\n\n", b"\r\n\r\n")
|
|
76
|
+
if (index := self._pending.find(separator)) >= 0
|
|
77
|
+
]
|
|
78
|
+
marker, marker_size = min(candidates, default=(-1, 0))
|
|
79
|
+
if marker < 0:
|
|
80
|
+
return
|
|
81
|
+
block = bytes(self._pending[:marker])
|
|
82
|
+
del self._pending[: marker + marker_size]
|
|
83
|
+
self._accept(_event_payload(block))
|
|
84
|
+
|
|
85
|
+
def finish(self) -> None:
|
|
86
|
+
if self.parseable and self._pending:
|
|
87
|
+
self._accept(_event_payload(bytes(self._pending)))
|
|
88
|
+
self._pending.clear()
|
|
89
|
+
if not self.output and self._items:
|
|
90
|
+
self.output = [self._items[index] for index in sorted(self._items)]
|
|
91
|
+
|
|
92
|
+
def _accept(self, event: dict[str, Any] | None) -> None:
|
|
93
|
+
if not event:
|
|
94
|
+
return
|
|
95
|
+
event_type = event.get("type")
|
|
96
|
+
if event_type == "response.output_item.done":
|
|
97
|
+
item = event.get("item")
|
|
98
|
+
index = event.get("output_index")
|
|
99
|
+
if isinstance(item, dict) and isinstance(index, int):
|
|
100
|
+
self._items[index] = item
|
|
101
|
+
return
|
|
102
|
+
if event_type not in {"response.completed", "response.incomplete", "response.failed"}:
|
|
103
|
+
return
|
|
104
|
+
response = event.get("response")
|
|
105
|
+
if not isinstance(response, dict):
|
|
106
|
+
return
|
|
107
|
+
self.response_id = str(response.get("id") or "")
|
|
108
|
+
self.status = str(response.get("status") or "")
|
|
109
|
+
output = response.get("output")
|
|
110
|
+
if isinstance(output, list):
|
|
111
|
+
self.output = [item for item in output if isinstance(item, dict)]
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def has_reasoning(self) -> bool:
|
|
115
|
+
return any(item.get("type") == "reasoning" for item in self.output)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def has_action(self) -> bool:
|
|
119
|
+
# Responses output items other than messages/reasoning are protocol-level
|
|
120
|
+
# actions or state. Treat unknown future item types conservatively as work.
|
|
121
|
+
return any(item.get("type") not in {"message", "reasoning"} for item in self.output)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def visible_text(self) -> str:
|
|
125
|
+
return "\n".join(filter(None, (_message_text(item) for item in self.output)))
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def completion_confirmed(self) -> bool:
|
|
129
|
+
actions = [
|
|
130
|
+
item for item in self.output if item.get("type") not in {"message", "reasoning"}
|
|
131
|
+
]
|
|
132
|
+
return bool(actions) and all(
|
|
133
|
+
item.get("name") == CODEX_COMPLETION_TOOL_NAME for item in actions
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def request_requires_completion_check(
|
|
138
|
+
body: dict[str, Any], observation: ResponsesCompletionObservation
|
|
139
|
+
) -> bool:
|
|
140
|
+
"""Use response structure only; never classify natural-language wording."""
|
|
141
|
+
|
|
142
|
+
return bool(
|
|
143
|
+
body.get("tools")
|
|
144
|
+
and observation.parseable
|
|
145
|
+
and observation.status == "completed"
|
|
146
|
+
and observation.has_reasoning
|
|
147
|
+
and not observation.has_action
|
|
148
|
+
and observation.visible_text.strip()
|
|
149
|
+
and observation.output
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def completion_check_body(
|
|
154
|
+
body: dict[str, Any], observation: ResponsesCompletionObservation
|
|
155
|
+
) -> dict[str, Any]:
|
|
156
|
+
"""Continue from a candidate final response using the official state shapes."""
|
|
157
|
+
|
|
158
|
+
projected = copy.deepcopy(body)
|
|
159
|
+
prompt = {
|
|
160
|
+
"type": "message",
|
|
161
|
+
"role": "user",
|
|
162
|
+
"content": [{"type": "input_text", "text": CODEX_STRICT_CONTINUATION_NUDGE}],
|
|
163
|
+
}
|
|
164
|
+
if projected.get("conversation"):
|
|
165
|
+
projected["input"] = [prompt]
|
|
166
|
+
elif bool(projected.get("store")) and observation.response_id:
|
|
167
|
+
projected["previous_response_id"] = observation.response_id
|
|
168
|
+
projected["input"] = [prompt]
|
|
169
|
+
else:
|
|
170
|
+
current = projected.get("input")
|
|
171
|
+
items = list(current) if isinstance(current, list) else ([current] if current else [])
|
|
172
|
+
projected["input"] = [*items, *copy.deepcopy(observation.output), prompt]
|
|
173
|
+
projected["stream"] = True
|
|
174
|
+
tools = list(projected.get("tools") or [])
|
|
175
|
+
tools.append(
|
|
176
|
+
{
|
|
177
|
+
"type": "function",
|
|
178
|
+
"name": CODEX_COMPLETION_TOOL_NAME,
|
|
179
|
+
"description": "Confirm that every action requested by the user is complete.",
|
|
180
|
+
"parameters": {
|
|
181
|
+
"type": "object",
|
|
182
|
+
"properties": {},
|
|
183
|
+
"required": [],
|
|
184
|
+
"additionalProperties": False,
|
|
185
|
+
},
|
|
186
|
+
"strict": True,
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
projected["tools"] = tools
|
|
190
|
+
projected["tool_choice"] = "required"
|
|
191
|
+
return projected
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
__all__ = [
|
|
195
|
+
"ResponsesCompletionObservation",
|
|
196
|
+
"completion_check_body",
|
|
197
|
+
"request_requires_completion_check",
|
|
198
|
+
]
|