@oneciel-ai/ciel-runtime 0.2.37 → 0.2.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +5 -2
  3. package/ciel_runtime.py +94 -96
  4. package/ciel_runtime_support/anthropic_model_policy.py +20 -3
  5. package/ciel_runtime_support/architecture.py +5 -0
  6. package/ciel_runtime_support/channel_inflight.py +4 -4
  7. package/ciel_runtime_support/channel_injection.py +56 -1
  8. package/ciel_runtime_support/channel_mcp_tools.py +34 -0
  9. package/ciel_runtime_support/channel_message_policy.py +9 -1
  10. package/ciel_runtime_support/channel_message_prompt.py +23 -0
  11. package/ciel_runtime_support/channel_pending_injection.py +86 -32
  12. package/ciel_runtime_support/channel_pending_poll.py +6 -2
  13. package/ciel_runtime_support/channel_runtime_environment.py +0 -11
  14. package/ciel_runtime_support/channel_terminal_context.py +0 -6
  15. package/ciel_runtime_support/channel_terminal_proxy.py +3 -42
  16. package/ciel_runtime_support/channel_transcript_repository.py +16 -1
  17. package/ciel_runtime_support/channel_wake_claim_repository.py +0 -19
  18. package/ciel_runtime_support/channel_wake_context.py +6 -13
  19. package/ciel_runtime_support/channel_wake_delivery_repository.py +50 -0
  20. package/ciel_runtime_support/chat_http_controller.py +88 -5
  21. package/ciel_runtime_support/claude_environment.py +100 -3
  22. package/ciel_runtime_support/claude_launch_assembly.py +4 -0
  23. package/ciel_runtime_support/claude_session_socket.py +181 -0
  24. package/ciel_runtime_support/cli_application_context.py +18 -10
  25. package/ciel_runtime_support/cli_dispatch.py +19 -3
  26. package/ciel_runtime_support/cli_parser.py +2 -0
  27. package/ciel_runtime_support/cli_usage.py +3 -2
  28. package/ciel_runtime_support/codex_backend_context.py +8 -3
  29. package/ciel_runtime_support/codex_completion_gate.py +198 -0
  30. package/ciel_runtime_support/codex_turn_recovery.py +240 -83
  31. package/ciel_runtime_support/compatibility_protocol.py +5 -2
  32. package/ciel_runtime_support/config_migrations.py +79 -0
  33. package/ciel_runtime_support/config_repository.py +7 -0
  34. package/ciel_runtime_support/context_summary_policy.py +24 -4
  35. package/ciel_runtime_support/external_event_menu.py +107 -0
  36. package/ciel_runtime_support/external_event_receiver.py +19 -1
  37. package/ciel_runtime_support/launch_state.py +2 -0
  38. package/ciel_runtime_support/managed_tool_injection.py +33 -0
  39. package/ciel_runtime_support/muse_runtime_context.py +291 -0
  40. package/ciel_runtime_support/ollama_thinking.py +12 -26
  41. package/ciel_runtime_support/prelaunch.py +13 -0
  42. package/ciel_runtime_support/prelaunch_launch_panel.py +2 -1
  43. package/ciel_runtime_support/prompt_compaction.py +39 -0
  44. package/ciel_runtime_support/provider_files_proxy.py +248 -0
  45. package/ciel_runtime_support/provider_model_identity.py +10 -2
  46. package/ciel_runtime_support/provider_option_cli.py +1 -1
  47. package/ciel_runtime_support/provider_request_access.py +54 -1
  48. package/ciel_runtime_support/provider_responses_passthrough.py +152 -15
  49. package/ciel_runtime_support/providers/alibaba.py +34 -1
  50. package/ciel_runtime_support/providers/anthropic.py +18 -1
  51. package/ciel_runtime_support/providers/meta.py +252 -21
  52. package/ciel_runtime_support/pseudo_tool_parser.py +17 -1
  53. package/ciel_runtime_support/remote_bridge.py +4 -0
  54. package/ciel_runtime_support/remote_instructions.py +5 -0
  55. package/ciel_runtime_support/remote_memory.py +1 -1
  56. package/ciel_runtime_support/responses_cache_diagnostics.py +82 -0
  57. package/ciel_runtime_support/responses_custom_tool_bridge.py +282 -0
  58. package/ciel_runtime_support/responses_input_compatibility.py +19 -3
  59. package/ciel_runtime_support/router_http.py +301 -4
  60. package/ciel_runtime_support/router_observability_context.py +26 -0
  61. package/ciel_runtime_support/router_server_context.py +1 -0
  62. package/ciel_runtime_support/runtime_adapters.py +31 -0
  63. package/ciel_runtime_support/runtime_constants.py +23 -1
  64. package/ciel_runtime_support/runtime_input_gateway.py +85 -5
  65. package/ciel_runtime_support/runtime_input_status.py +150 -0
  66. package/ciel_runtime_support/runtime_launch.py +27 -4
  67. package/ciel_runtime_support/runtime_paths.py +1 -0
  68. package/ciel_runtime_support/speech_http_controller.py +3 -2
  69. package/ciel_runtime_support/streaming_anthropic.py +3 -0
  70. package/ciel_runtime_support/tool_call_events.py +98 -0
  71. package/ciel_runtime_support/transcript_delta_delivery.py +140 -1
  72. package/ciel_runtime_support/ui_text.py +1 -0
  73. package/ciel_runtime_support/web_search_result_events.py +130 -0
  74. package/ciel_runtime_support/web_ui.py +1 -0
  75. package/ciel_runtime_support/windows_conpty.py +92 -8
  76. package/ciel_runtime_support/workspace_mcp.py +4 -1
  77. package/ciel_runtime_support/workspace_state.py +1 -0
  78. package/docs/Configuration.md +20 -4
  79. package/docs/MCP-Channels.md +79 -6
  80. package/docs/Managed-Tool-Injection.md +25 -0
  81. package/docs/Module-Map.md +4 -0
  82. package/docs/Muse-Code.md +73 -0
  83. package/docs/Observability.md +39 -0
  84. package/docs/Providers.md +29 -2
  85. package/docs/Remote-Bridge.md +12 -0
  86. package/docs/Router.md +1 -0
  87. package/docs/journal/2026/09/01/diagnostics/anthropic/router/fable51-usage-credits.okf +70 -0
  88. package/docs/journal/2026/09/01/implementation/claude/session-socket/all-input-paths/default-delivery.okf +133 -0
  89. package/docs/journal/2026/09/01/implementation/claude/session-socket/windows-direct-input.okf +112 -0
  90. package/docs/journal/2026/09/02/diagnostics/cache/alibaba/qwen38-post-restart-hit-rate.okf +107 -0
  91. package/docs/journal/2026/09/02/diagnostics/cache/ollama-cloud/kimi-k3-codex-hit-rate.okf +91 -0
  92. package/docs/journal/2026/09/02/diagnostics/codex/tui/statusline-cache-metrics.okf +44 -0
  93. package/docs/journal/2026/09/02/diagnostics/input/transport/fallback-behavior.okf +31 -0
  94. package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-agent-turn-recovery.okf +125 -0
  95. package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-parameters.okf +128 -0
  96. package/docs/journal/2026/09/02/diagnostics/remote/mia/socket-tui-visibility.okf +47 -0
  97. package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-latency.okf +108 -0
  98. package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-vs-gpt56-latency.okf +92 -0
  99. package/docs/journal/2026/09/02/implementation/anthropic/context/one-million-defaults.okf +85 -0
  100. package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.okf +104 -0
  101. package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.png +0 -0
  102. package/docs/journal/2026/09/02/implementation/cache/alibaba/responses-session-cache.okf +97 -0
  103. package/docs/journal/2026/09/02/implementation/providers/alibaba/qwen38/0902-parameter-alignment.okf +97 -0
  104. package/docs/journal/2026/09/02/implementation/providers/meta/multimodal-tools/contributor-protocol-adoption.okf +140 -0
  105. package/docs/journal/2026/09/02/implementation/providers/meta/muse-spark-1.3-support.okf +53 -0
  106. package/docs/journal/2026/09/02/monitoring/cache/alibaba/cielarvis-qwen38-live-hit-rate.okf +858 -0
  107. package/docs/journal/2026/09/02/operations/release/main-merge-local-deploy.okf +36 -0
  108. package/docs/journal/2026/09/02/operations/release/nightly/local-cache-deployment.okf +54 -0
  109. package/docs/journal/2026/09/02/operations/release/nightly/ollama-kimi-recovery-model-audit-deployment.okf +58 -0
  110. package/docs/journal/2026/09/02/operations/release/nightly/qwen38-cache-improvement-deployment.okf +72 -0
  111. package/docs/journal/2026/09/02/research/cache/alibaba/qwen38-hit-rate-improvement.okf +233 -0
  112. package/docs/journal/2026/09/02/research/cache/codex/provider-scope.okf +77 -0
  113. package/docs/journal/2026/09/02/research/providers/ollama-cloud/desktop-model-exhaustive-audit.okf +159 -0
  114. package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-cache-hit.okf +148 -0
  115. package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-required-schema.okf +127 -0
  116. package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-resumed-session-stall.okf +160 -0
  117. package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-substantive-dangling-action.okf +94 -0
  118. package/docs/journal/2026/09/03/diagnostics/remote/kevin/codex-gpt-early-turn-completion.okf +162 -0
  119. package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/default-yolo.okf +40 -0
  120. package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/native-subscription-router.okf +86 -0
  121. package/docs/journal/2026/09/03/operations/deployment/local/session-socket-default-fallback.okf +41 -0
  122. package/docs/journal/2026/09/03/research/deployment/colab/tailscale/credential-storage.okf +41 -0
  123. package/docs/journal/2026/09/03/research/providers/meta/muse-code/subscription-billing-boundary.okf +64 -0
  124. package/docs/journal/2026/09/03/research/web-chat/voice/instruction-format.okf +135 -0
  125. package/docs/journal/2026/09/04/diagnostics/claude/context/early-auto-compaction.okf +75 -0
  126. package/docs/journal/2026/09/04/diagnostics/claude/context/fable-51-status-200k.okf +54 -0
  127. package/docs/journal/2026/09/04/diagnostics/runtime/codex/responses/replayed-item-id-validation.okf +74 -0
  128. package/docs/journal/2026/09/04/diagnostics/workspaces/onecieldmsui/codex/replay-stall.okf +100 -0
  129. package/docs/journal/2026/09/04/implementation/observability/tool-calls/websocket-stream.okf +49 -0
  130. package/docs/journal/2026/09/04/implementation/web-chat/input/raw-injection.okf +42 -0
  131. package/docs/journal/2026/09/05/diagnostics/tty/claude-raw-idle-render.okf +25 -0
  132. package/docs/journal/2026/09/05/implementation/events/search-response-urls.okf +23 -0
  133. package/docs/journal/2026/09/05/implementation/tools/native-injection.okf +19 -0
  134. package/docs/journal/2026/09/05/implementation/windows/conpty/prompt-delivery-lifecycle.okf +43 -0
  135. package/docs/journal/2026/09/05/release/nightly/conpty-native-tools.okf +13 -0
  136. package/docs/journal/2026/09/05/release/nightly/search-result-urls.okf +14 -0
  137. package/docs/journal/2026/09/06/implementation/codex/inherited-web-mcp.okf +22 -0
  138. package/docs/journal/2026/09/07/integration/main/nightly-merge.okf +13 -0
  139. package/docs/journal/2026/09/07/release/stable/0.2.38.okf +10 -0
  140. package/package.json +1 -1
@@ -17,6 +17,42 @@ from typing import Any, Callable
17
17
  class RuntimeInputGateway:
18
18
  append: Callable[[dict[str, Any]], dict[str, Any]]
19
19
  project_attachment: Callable[[dict[str, Any]], dict[str, Any]] | None = None
20
+ default_input_transport: Callable[[], str] = lambda: "session_socket"
21
+ status: Any = None
22
+
23
+ def _append(self, payload: dict[str, Any]) -> dict[str, Any]:
24
+ message = self.append(payload)
25
+ request_id = int(message.get("id") or 0)
26
+ if (
27
+ request_id > 0
28
+ and not message.get("_ciel_runtime_duplicate")
29
+ and self.status is not None
30
+ ):
31
+ self.status.transition(
32
+ request_id,
33
+ "queued",
34
+ data={
35
+ "channel": str(message.get("channel") or "default"),
36
+ "input_transport": str(
37
+ (message.get("meta") or {}).get("input_transport") or ""
38
+ ),
39
+ },
40
+ )
41
+ return message
42
+
43
+ def _transport(self, value: Any = None) -> str:
44
+ transport = str(value or self.default_input_transport() or "session_socket").strip().lower().replace("-", "_")
45
+ aliases = {
46
+ "socket": "session_socket",
47
+ "claude_socket": "session_socket",
48
+ "messaging_socket": "session_socket",
49
+ "llm": "router",
50
+ "context": "router",
51
+ "terminal": "tty",
52
+ "stdin": "tty",
53
+ }
54
+ transport = aliases.get(transport, transport)
55
+ return transport if transport in {"session_socket", "router", "tty"} else "tty"
20
56
 
21
57
  def submit_web_chat(
22
58
  self,
@@ -30,6 +66,10 @@ class RuntimeInputGateway:
30
66
  meta.setdefault("source", "ciel-runtime-web-chat")
31
67
  meta.setdefault("source_kind", "web_chat")
32
68
  meta.setdefault("injection_mode", "structured")
69
+ meta.setdefault(
70
+ "input_transport",
71
+ self._transport(inbound.get("input_transport") or meta.get("input_transport")),
72
+ )
33
73
  meta.setdefault("response_mode", "web_chat")
34
74
  if meta["response_mode"] == "web_chat":
35
75
  meta["reply_parent_id"] = public_message.get("id")
@@ -58,7 +98,7 @@ class RuntimeInputGateway:
58
98
  "delivery": ["llm"],
59
99
  "visibility": "private_runtime",
60
100
  }
61
- return self.append(payload)
101
+ return self._append(payload)
62
102
 
63
103
  def submit_tty(
64
104
  self,
@@ -94,6 +134,9 @@ class RuntimeInputGateway:
94
134
  meta["source"] = "ciel-runtime-api-tty"
95
135
  meta["source_kind"] = "tty_input"
96
136
  meta["injection_mode"] = "tty"
137
+ meta["input_transport"] = self._transport(
138
+ meta.get("input_transport") or body.get("input_transport")
139
+ )
97
140
  meta["response_mode"] = response_mode
98
141
 
99
142
  content: Any = params.get("content")
@@ -103,7 +146,7 @@ class RuntimeInputGateway:
103
146
  content = json.dumps(content, ensure_ascii=False, separators=(",", ":"))
104
147
  meta["content_type"] = "application/json"
105
148
 
106
- return self.append(
149
+ return self._append(
107
150
  {
108
151
  "channel": body.get("channel") or meta.get("channel") or "default",
109
152
  "sender_id": body.get("sender_id") or body.get("sender") or "api-user",
@@ -126,10 +169,14 @@ class RuntimeInputGateway:
126
169
  meta = dict(meta_value) if isinstance(meta_value, dict) else {}
127
170
  meta.setdefault("source", "ciel-runtime-notify")
128
171
  meta.setdefault("source_kind", "notification")
172
+ meta.setdefault(
173
+ "input_transport",
174
+ self._transport(body.get("input_transport") or meta.get("input_transport")),
175
+ )
129
176
  content = params.get("content")
130
177
  if content is None:
131
178
  content = body.get("content", body.get("message", body.get("text", "")))
132
- return self.append(
179
+ return self._append(
133
180
  {
134
181
  "channel": body.get("channel") or meta.get("channel") or "default",
135
182
  "sender_id": body.get("sender_id") or body.get("sender") or body.get("server") or meta.get("source") or "channel",
@@ -144,6 +191,36 @@ class RuntimeInputGateway:
144
191
  }
145
192
  )
146
193
 
194
+ def submit_stream_input(self, body: dict[str, Any]) -> dict[str, Any]:
195
+ """Admit an external Streamable HTTP/MCP input into the runtime queue."""
196
+
197
+ meta = dict(body.get("meta") or {}) if isinstance(body.get("meta"), dict) else {}
198
+ meta.setdefault("source", "ciel-runtime-mcp-streamable")
199
+ meta.setdefault("source_kind", "mcp_streamable_input")
200
+ meta.setdefault("injection_mode", "structured")
201
+ meta["input_transport"] = self._transport(
202
+ body.get("input_transport") or meta.get("input_transport")
203
+ )
204
+ meta.setdefault("response_mode", str(body.get("response_mode") or "tty"))
205
+ content = body.get("message", body.get("content", body.get("text", "")))
206
+ if isinstance(content, (dict, list)):
207
+ content = json.dumps(content, ensure_ascii=False, separators=(",", ":"))
208
+ meta["content_type"] = "application/json"
209
+ return self._append(
210
+ {
211
+ "channel": body.get("channel") or "mcp-streamable",
212
+ "sender_id": body.get("sender_id") or body.get("sender") or "mcp-client",
213
+ "recipients": body.get("recipients", "all"),
214
+ "thread_id": body.get("thread_id"),
215
+ "parent_id": body.get("parent_id"),
216
+ "kind": body.get("kind") or "mcp_streamable_input",
217
+ "message": content if content is not None else "",
218
+ "meta": meta,
219
+ "delivery": ["llm"],
220
+ "visibility": "private_runtime",
221
+ }
222
+ )
223
+
147
224
  def submit_external_event(
148
225
  self,
149
226
  raw_event: str,
@@ -154,10 +231,11 @@ class RuntimeInputGateway:
154
231
  event_type: str,
155
232
  event_source: str,
156
233
  transport_event_id: str = "",
234
+ input_transport: str = "",
157
235
  ) -> dict[str, Any]:
158
236
  """Admit exact decoded event text; never normalize or re-serialize it."""
159
237
 
160
- return self.append(
238
+ return self._append(
161
239
  {
162
240
  "channel": f"external:{receiver_id}",
163
241
  "sender_id": event_source or receiver_id,
@@ -174,6 +252,7 @@ class RuntimeInputGateway:
174
252
  "event_type": event_type,
175
253
  "event_source": event_source,
176
254
  "transport_event_id": transport_event_id,
255
+ "input_transport": self._transport(input_transport),
177
256
  "raw_preserved": True,
178
257
  },
179
258
  "delivery": ["llm"],
@@ -216,7 +295,7 @@ class RuntimeInputGateway:
216
295
  "to read only the needed range. This is input-only telemetry; no acknowledgement "
217
296
  f"or response is required. Stored ranges: {locations}"
218
297
  )
219
- return self.append(
298
+ return self._append(
220
299
  {
221
300
  "channel": "telemetry",
222
301
  "sender_id": "otlp",
@@ -231,6 +310,7 @@ class RuntimeInputGateway:
231
310
  "ack_required": False,
232
311
  "response_expected": False,
233
312
  "logs_embedded": False,
313
+ "input_transport": self._transport(),
234
314
  },
235
315
  "delivery": ["llm"],
236
316
  "visibility": "private_runtime",
@@ -0,0 +1,150 @@
1
+ """Durable lifecycle state for private runtime input requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timezone
7
+ import json
8
+ from pathlib import Path
9
+ import threading
10
+ import time
11
+ from typing import Any, Callable
12
+
13
+
14
+ RUNTIME_INPUT_STATES = frozenset({"queued", "submitted", "replied", "failed"})
15
+ _TERMINAL_STATES = frozenset({"replied", "failed"})
16
+
17
+
18
+ @dataclass(slots=True)
19
+ class RuntimeInputStatusRepository:
20
+ path: Path
21
+ publish_event: Callable[..., Any]
22
+ log: Callable[[str, str], None]
23
+ lock: threading.Lock | threading.RLock
24
+
25
+ def transition(
26
+ self,
27
+ request_id: int,
28
+ status: str,
29
+ *,
30
+ reason: str = "",
31
+ data: dict[str, Any] | None = None,
32
+ ) -> dict[str, Any]:
33
+ request_id = int(request_id)
34
+ status = str(status or "").strip().lower()
35
+ if request_id <= 0:
36
+ raise ValueError("request_id must be positive")
37
+ if status not in RUNTIME_INPUT_STATES:
38
+ raise ValueError(f"invalid runtime input status: {status}")
39
+ with self.lock:
40
+ current = self.get(request_id, _locked=True)
41
+ current_status = str((current or {}).get("status") or "")
42
+ if current_status in _TERMINAL_STATES or current_status == status:
43
+ return current or self._record(request_id, status, reason, data)
44
+ allowed = {
45
+ "": {"queued"},
46
+ "queued": {"submitted", "failed"},
47
+ "submitted": {"replied", "failed"},
48
+ }
49
+ if status not in allowed.get(current_status, set()):
50
+ self.log(
51
+ "WARN",
52
+ "runtime_input_status_transition_rejected "
53
+ f"request_id={request_id} from={current_status or '-'} to={status}",
54
+ )
55
+ return current or {}
56
+ record = self._record(request_id, status, reason, data)
57
+ self.publish_event(
58
+ level="error" if status == "failed" else "info",
59
+ category="runtime_input.status",
60
+ message=f"runtime input {status}",
61
+ source="runtime-input",
62
+ request_id=str(request_id),
63
+ data=record,
64
+ )
65
+ return record
66
+
67
+ def _record(
68
+ self,
69
+ request_id: int,
70
+ status: str,
71
+ reason: str,
72
+ data: dict[str, Any] | None,
73
+ ) -> dict[str, Any]:
74
+ now = time.time()
75
+ record: dict[str, Any] = {
76
+ "request_id": request_id,
77
+ "status": status,
78
+ "updated_at": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"),
79
+ "updated_at_epoch": now,
80
+ }
81
+ if reason:
82
+ record["reason"] = str(reason)
83
+ if data:
84
+ record["data"] = dict(data)
85
+ self.path.parent.mkdir(parents=True, exist_ok=True)
86
+ with self.path.open("a", encoding="utf-8") as stream:
87
+ stream.write(json.dumps(record, ensure_ascii=False, separators=(",", ":")) + "\n")
88
+ stream.flush()
89
+ return record
90
+
91
+ def get(self, request_id: int, *, _locked: bool = False) -> dict[str, Any] | None:
92
+ def read() -> dict[str, Any] | None:
93
+ latest = None
94
+ for item in self._items():
95
+ try:
96
+ if int(item.get("request_id") or 0) == int(request_id):
97
+ latest = item
98
+ except (TypeError, ValueError):
99
+ continue
100
+ return latest
101
+
102
+ if _locked:
103
+ return read()
104
+ with self.lock:
105
+ return read()
106
+
107
+ def list_latest(
108
+ self,
109
+ *,
110
+ after_request_id: int = 0,
111
+ status: str = "",
112
+ limit: int = 100,
113
+ ) -> list[dict[str, Any]]:
114
+ with self.lock:
115
+ latest: dict[int, dict[str, Any]] = {}
116
+ for item in self._items():
117
+ try:
118
+ request_id = int(item.get("request_id") or 0)
119
+ except (TypeError, ValueError):
120
+ continue
121
+ if request_id > after_request_id:
122
+ latest[request_id] = item
123
+ expected = str(status or "").strip().lower()
124
+ rows = [latest[key] for key in sorted(latest)]
125
+ if expected:
126
+ rows = [row for row in rows if row.get("status") == expected]
127
+ return rows[: max(1, min(500, int(limit)))]
128
+
129
+ def _items(self) -> list[dict[str, Any]]:
130
+ if not self.path.exists():
131
+ return []
132
+ items: list[dict[str, Any]] = []
133
+ try:
134
+ with self.path.open("r", encoding="utf-8") as stream:
135
+ for line in stream:
136
+ try:
137
+ item = json.loads(line)
138
+ except (json.JSONDecodeError, TypeError):
139
+ continue
140
+ if isinstance(item, dict):
141
+ items.append(item)
142
+ except OSError as exc:
143
+ self.log(
144
+ "WARN",
145
+ f"runtime_input_status_read_failed error={type(exc).__name__}: {exc}",
146
+ )
147
+ return items
148
+
149
+
150
+ __all__ = ["RUNTIME_INPUT_STATES", "RuntimeInputStatusRepository"]
@@ -8,6 +8,7 @@ from typing import Any, Callable
8
8
  from dataclasses import dataclass
9
9
 
10
10
  from ciel_runtime_support.claude_environment import CLAUDE_PROJECTED_ENV_KEYS
11
+ from ciel_runtime_support.managed_tool_injection import should_inject_tool, codex_native_web_tool_overrides
11
12
  from ciel_runtime_support.runtime_constants import (
12
13
  CLAUDE_SERVER_SIDE_WEB_TOOLS,
13
14
  CODEX_RUNTIME_API_KEY_ENV,
@@ -151,6 +152,8 @@ class ClaudeLaunchChannelDelivery:
151
152
  channel_wake_submit_delay_seconds: Callable[..., Any]
152
153
  channel_wake_submit_retries: Callable[..., Any]
153
154
  set_channel_transcript_scope: Callable[..., Any]
155
+ prepare_session_socket: Callable[..., str]
156
+ configure_session_socket: Callable[[str | None], None]
154
157
 
155
158
 
156
159
  @dataclass(frozen=True, slots=True)
@@ -245,6 +248,8 @@ def run_claude(
245
248
  channel_wake_submit_delay_seconds = services.channel_delivery.channel_wake_submit_delay_seconds
246
249
  channel_wake_submit_retries = services.channel_delivery.channel_wake_submit_retries
247
250
  set_channel_transcript_scope = services.channel_delivery.set_channel_transcript_scope
251
+ prepare_session_socket = services.channel_delivery.prepare_session_socket
252
+ configure_session_socket = services.channel_delivery.configure_session_socket
248
253
  start_router_if_needed = services.routing.start_router_if_needed
249
254
  subprocess_call_with_channel_wake_proxy = services.process.subprocess_call_with_channel_wake_proxy
250
255
  subprocess_call_with_child_pid_record = services.process.subprocess_call_with_child_pid_record
@@ -330,6 +335,11 @@ def run_claude(
330
335
  if use_native_anthropic and web_backend_start_requested(cfg):
331
336
  llm_channel_delivery = True
332
337
  stdin_channel_proxy = not has_noninteractive_claude_args(launch_passthrough)
338
+ session_socket_target = (
339
+ prepare_session_socket(cfg, launch_passthrough)
340
+ if stdin_channel_proxy
341
+ else ""
342
+ )
333
343
  manage_router_lifetime = False
334
344
  if use_router_mode or llm_channel_delivery:
335
345
  manage_router_lifetime = bool(start_router_if_needed())
@@ -454,7 +464,7 @@ def run_claude(
454
464
  zai_mcp_config = write_zai_mcp_config(provider, pcfg)
455
465
  if zai_mcp_config:
456
466
  mcp_config_paths.append(str(zai_mcp_config))
457
- if should_attach_web_search(provider, cfg, web_search_override):
467
+ if should_inject_tool(native=provider == "anthropic", mode="non_native") and should_attach_web_search(provider, cfg, web_search_override):
458
468
  mcp_config_paths.append(str(write_duckduckgo_mcp_config(cfg)))
459
469
  workspace_mcp_launch = (
460
470
  workspace_mcp.prepare(
@@ -463,6 +473,7 @@ def run_claude(
463
473
  "transport": "streamable_http", "url": f"{ROUTER_BASE}/ca/mcp",
464
474
  "runtimes": ["claude"],
465
475
  }} if llm_channel_delivery and web_backend_start_requested(cfg) else None,
476
+ native=provider == "anthropic",
466
477
  ) if workspace_mcp is not None else None
467
478
  )
468
479
  if workspace_mcp_launch is not None:
@@ -472,7 +483,15 @@ def run_claude(
472
483
  extra_args.extend(["--mcp-config", *mcp_config_paths])
473
484
  if should_append_compat_prompt(provider, pcfg, cfg) and not has_passthrough_option(launch_passthrough, "--system-prompt"):
474
485
  extra_args.extend(["--append-system-prompt", ROUTED_COMPAT_PROMPT])
475
- append_claude_code_runtime_settings_args(extra_args, launch_passthrough, provider, pcfg)
486
+ if session_socket_target and not has_passthrough_option(
487
+ launch_passthrough, "--messaging-socket-path"
488
+ ):
489
+ extra_args.extend(["--messaging-socket-path", session_socket_target])
490
+ runtime_settings_config = dict(pcfg)
491
+ runtime_settings_config["_ciel_session_socket_enabled"] = bool(session_socket_target)
492
+ append_claude_code_runtime_settings_args(
493
+ extra_args, launch_passthrough, provider, runtime_settings_config
494
+ )
476
495
  if fork_native_session:
477
496
  session_id = str(uuid.uuid4())
478
497
  extra_args.extend(["--session-id", session_id])
@@ -538,6 +557,7 @@ def run_claude(
538
557
  def run_claude_process() -> int:
539
558
  rc = 1
540
559
  try:
560
+ configure_session_socket(session_socket_target or None)
541
561
  set_channel_transcript_scope(
542
562
  "claude",
543
563
  cwd=Path.cwd(),
@@ -573,6 +593,7 @@ def run_claude(
573
593
  rc = subprocess.call(cmd, env=env)
574
594
  return rc
575
595
  finally:
596
+ configure_session_socket(None)
576
597
  if use_router_mode:
577
598
  print_routed_claude_exit_diagnostics(rc, provider, pcfg, log_offset=launch_log_offset)
578
599
 
@@ -902,10 +923,11 @@ def run_codex(
902
923
  ),
903
924
  )
904
925
  workspace_mcp_launch = (
905
- workspace_mcp.prepare("codex", cfg, env) if workspace_mcp is not None else None
926
+ workspace_mcp.prepare("codex", cfg, env, native=native_codex_enabled(provider)) if workspace_mcp is not None else None
906
927
  )
907
928
  if workspace_mcp_launch is not None:
908
929
  codex_mcp_compat_args.extend(workspace_mcp_launch.codex_args)
930
+ codex_mcp_compat_args.extend(codex_native_web_tool_overrides(native=native_codex_enabled(provider)))
909
931
  codex_yolo_args = codex_yolo_launch_args(codex_passthrough)
910
932
  if not use_native_codex and not use_codex_routed:
911
933
  env[CODEX_RUNTIME_API_KEY_ENV] = env.get(CODEX_RUNTIME_API_KEY_ENV) or "ciel-runtime-router-local-key"
@@ -1232,12 +1254,13 @@ def run_codex_app_server(
1232
1254
  ),
1233
1255
  )
1234
1256
  workspace_mcp_launch = (
1235
- workspace_mcp.prepare("codex-app-server", cfg, env)
1257
+ workspace_mcp.prepare("codex-app-server", cfg, env, native=native_codex_enabled(provider))
1236
1258
  if workspace_mcp is not None else None
1237
1259
  )
1238
1260
  try:
1239
1261
  if workspace_mcp_launch is not None:
1240
1262
  codex_mcp_compat_args.extend(workspace_mcp_launch.codex_args)
1263
+ codex_mcp_compat_args.extend(codex_native_web_tool_overrides(native=native_codex_enabled(provider)))
1241
1264
  config_args = [*config_args, *codex_mcp_compat_args]
1242
1265
  listen_url = codex_app_server_default_listen_url()
1243
1266
  app_server_args = codex_app_server_launch_args(
@@ -167,6 +167,7 @@ CONTEXT_USAGE_PATH = ROUTER_INSTANCE_DIR / "context-usage.json"
167
167
  OLLAMA_MODEL_CATALOG_PATH = CONFIG_DIR / "ollama-model-catalog.json"
168
168
  CHAT_MESSAGES_PATH = WORKSPACE_STATE_DIR / "chat-messages.jsonl"
169
169
  RUNTIME_INPUTS_PATH = WORKSPACE_STATE_DIR / "runtime-inputs.jsonl"
170
+ RUNTIME_INPUT_STATUS_PATH = WORKSPACE_STATE_DIR / "runtime-input-status.jsonl"
170
171
  CHAT_FILES_DIR = WORKSPACE_STATE_DIR / "chat-files"
171
172
  MENU_KEY_DEBUG_PATH = ROUTER_INSTANCE_DIR / "ca-key-debug.log"
172
173
  PLAN_ARTIFACTS_DIR = WORKSPACE_STATE_DIR / "plan-artifacts"
@@ -151,8 +151,9 @@ class SpeechHttpController:
151
151
  "chat_message_processing": {
152
152
  "input_mode": {"default": "structured", "allowed": ["structured", "tty"]},
153
153
  "input_transport": {
154
- "default": "tty",
155
- "allowed": ["tty", "router"],
154
+ "default": "session_socket; tty fallback when the active runtime has no usable session socket",
155
+ "allowed": ["session_socket", "tty", "router"],
156
+ "session_socket_requires": "an active Claude session launched with its Ciel-owned messaging socket",
156
157
  "router_requires": "an active model session routed through Ciel Router",
157
158
  },
158
159
  "response_mode": {"default": "web_chat", "allowed": ["web_chat", "tty", "mcp"]},
@@ -716,6 +716,7 @@ def rebatch_anthropic_sse_text(
716
716
  "anthropic_stream_tool_call",
717
717
  {
718
718
  "model": model,
719
+ "tool_id": tool_id,
719
720
  "raw_name": raw_name,
720
721
  "matched_name": matched_name,
721
722
  "raw_arguments": raw_args,
@@ -1656,6 +1657,7 @@ def ollama_stream_to_anthropic_sse(
1656
1657
  "ollama_stream_tool_call",
1657
1658
  {
1658
1659
  "model": model,
1660
+ "tool_id": tool_id,
1659
1661
  "raw_name": raw_name,
1660
1662
  "matched_name": matched_name,
1661
1663
  "raw_arguments": raw_args,
@@ -2434,6 +2436,7 @@ def forward_openai_chat_to_anthropic_sse(
2434
2436
  "openai_stream_tool_call",
2435
2437
  {
2436
2438
  "model": model,
2439
+ "tool_id": tool_id,
2437
2440
  "raw_name": raw_name,
2438
2441
  "matched_name": matched_name,
2439
2442
  "raw_arguments": fragment.get("arguments"),
@@ -0,0 +1,98 @@
1
+ """Normalize tool-call records emitted by supported CLI transcript formats."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+
9
+ _CODEX_CALL_TYPES = {"function_call", "custom_tool_call", "local_shell_call"}
10
+ _CODEX_BEGIN_TYPES = {"mcp_tool_call_begin", "tool_call_begin"}
11
+ _CLAUDE_CALL_TYPES = {"tool_use", "server_tool_use"}
12
+
13
+
14
+ def _object(value: Any) -> dict[str, Any]:
15
+ return value if isinstance(value, dict) else {}
16
+
17
+
18
+ def _call_id(value: dict[str, Any]) -> str:
19
+ for key in ("call_id", "id", "tool_call_id"):
20
+ text = str(value.get(key) or "").strip()
21
+ if text:
22
+ return text
23
+ return ""
24
+
25
+
26
+ def _arguments(value: Any) -> Any:
27
+ if not isinstance(value, str):
28
+ return value
29
+ text = value.strip()
30
+ if not text:
31
+ return ""
32
+ try:
33
+ return json.loads(text)
34
+ except (TypeError, ValueError):
35
+ return value
36
+
37
+
38
+ def _project(value: dict[str, Any], *, call_type: str) -> dict[str, Any]:
39
+ name = str(value.get("name") or value.get("tool_name") or "").strip()
40
+ arguments = value.get("arguments")
41
+ if arguments is None:
42
+ arguments = value.get("input")
43
+ if arguments is None:
44
+ arguments = value.get("action")
45
+ if arguments is None:
46
+ arguments = value.get("args")
47
+ return {
48
+ "call_id": _call_id(value),
49
+ "name": name or call_type,
50
+ "call_type": call_type,
51
+ "arguments": _arguments(arguments),
52
+ }
53
+
54
+
55
+ def project_transcript_tool_calls(
56
+ record: dict[str, Any], runtime: str
57
+ ) -> list[dict[str, Any]]:
58
+ """Project one Claude/Codex JSONL record into normalized tool-call starts."""
59
+
60
+ record_type = str(record.get("type") or "").strip()
61
+ payload = _object(record.get("payload"))
62
+ payload_type = str(payload.get("type") or "").strip()
63
+ calls: list[dict[str, Any]] = []
64
+ if record_type == "response_item" and payload_type in _CODEX_CALL_TYPES:
65
+ calls.append(_project(payload, call_type=payload_type))
66
+ elif record_type == "event_msg" and payload_type in _CODEX_BEGIN_TYPES:
67
+ invocation = _object(payload.get("invocation")) or _object(payload.get("tool_call"))
68
+ calls.append(_project({**payload, **invocation}, call_type=payload_type))
69
+
70
+ muse_payload_type = str(record.get("payload_type") or "").strip()
71
+ if str(record.get("record_type") or "").strip() == "event" and muse_payload_type == "runtime.session":
72
+ muse_event = _object(payload.get("event"))
73
+ muse_calls = muse_event.get("tool_calls")
74
+ if isinstance(muse_calls, list):
75
+ for call in muse_calls:
76
+ if isinstance(call, dict):
77
+ calls.append(_project(call, call_type="muse_tool_call"))
78
+
79
+ message = _object(record.get("message"))
80
+ message_role = str(message.get("role") or "").strip()
81
+ if record_type == "assistant" or message_role == "assistant":
82
+ content = message.get("content")
83
+ blocks = [content] if isinstance(content, dict) else content if isinstance(content, list) else []
84
+ for block in blocks:
85
+ block_obj = _object(block)
86
+ block_type = str(block_obj.get("type") or "").strip()
87
+ if block_type in _CLAUDE_CALL_TYPES:
88
+ calls.append(_project(block_obj, call_type=block_type))
89
+
90
+ model = str(record.get("model") or message.get("model") or "").strip()
91
+ for call in calls:
92
+ call["runtime"] = str(runtime or "runtime")
93
+ if model:
94
+ call["model"] = model
95
+ return calls
96
+
97
+
98
+ __all__ = ["project_transcript_tool_calls"]