@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
@@ -3,8 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import io
6
+ import base64
7
+ import hashlib
6
8
  import json
7
9
  import sys
10
+ import tempfile
8
11
  import traceback
9
12
  import urllib.error
10
13
  import urllib.parse
@@ -30,6 +33,11 @@ from ciel_runtime_support.responses_input_compatibility import (
30
33
  repair_replayed_response_items,
31
34
  )
32
35
  from ciel_runtime_support.channel_llm_context import ChannelLlmInjectionDeferred
36
+ from ciel_runtime_support.codex_completion_gate import (
37
+ ResponsesCompletionObservation,
38
+ completion_check_body,
39
+ request_requires_completion_check,
40
+ )
33
41
  from ciel_runtime_support.request_body_policy import (
34
42
  RequestBodyCapacityExceeded,
35
43
  RequestBodyTooLarge,
@@ -63,6 +71,17 @@ class UpstreamContextExceeded(Exception):
63
71
  self.payload = payload
64
72
 
65
73
 
74
+ @dataclass(slots=True)
75
+ class _BufferedCodexResponse:
76
+ status: int
77
+ headers: Any
78
+ stream: Any
79
+ observation: ResponsesCompletionObservation
80
+
81
+ def close(self) -> None:
82
+ self.stream.close()
83
+
84
+
66
85
  @dataclass(frozen=True, slots=True)
67
86
  class CodexRoutedHeaderPolicy:
68
87
  decorate: Callable[[dict[str, str]], dict[str, str]]
@@ -154,6 +173,13 @@ class RouterHttpErrors:
154
173
  try_write_json: Callable[..., Any]
155
174
 
156
175
 
176
+ @dataclass(frozen=True, slots=True)
177
+ class RouterHttpFileEndpoints:
178
+ get: Callable[[Any, str, dict[str, Any]], bool]
179
+ post: Callable[[Any, str, int, str, dict[str, Any]], bool]
180
+ delete: Callable[[Any, str, dict[str, Any]], bool]
181
+
182
+
157
183
  @dataclass(frozen=True, slots=True)
158
184
  class RouterHttpServices:
159
185
  core: RouterHttpCore
@@ -161,6 +187,7 @@ class RouterHttpServices:
161
187
  post: RouterHttpPostEndpoints
162
188
  presentation: RouterHttpPresentation
163
189
  errors: RouterHttpErrors
190
+ files: RouterHttpFileEndpoints | None = None
164
191
 
165
192
 
166
193
  @dataclass(frozen=True, slots=True)
@@ -216,7 +243,11 @@ class CodexBackendHttpAdapter:
216
243
  def copy_response_headers(handler: BaseHTTPRequestHandler, headers: Any) -> None:
217
244
  allowed = {
218
245
  "cache-control",
246
+ "content-disposition",
247
+ "content-length",
219
248
  "content-type",
249
+ "etag",
250
+ "last-modified",
220
251
  "openai-request-id",
221
252
  "request-id",
222
253
  "retry-after",
@@ -524,9 +555,121 @@ class CodexBackendHttpAdapter:
524
555
  )
525
556
  self._retry.sleep(wait)
526
557
  continue
527
- self._write_response(handler, response, preamble)
558
+ if mutate_responses and upstream_body.get("tools"):
559
+ buffered = self._buffer_codex_response(response, preamble)
560
+ chosen = buffered
561
+ try:
562
+ if request_requires_completion_check(
563
+ upstream_body, buffered.observation
564
+ ):
565
+ validated = self._validate_codex_completion(
566
+ provider,
567
+ config,
568
+ url,
569
+ headers,
570
+ upstream_body,
571
+ buffered.observation,
572
+ )
573
+ if validated is not None:
574
+ if validated.observation.completion_confirmed:
575
+ self._retry.log(
576
+ "INFO",
577
+ "codex_completion_gate_confirmed "
578
+ f"model={str(upstream_body.get('model') or '-')}",
579
+ )
580
+ elif (
581
+ validated.observation.status == "completed"
582
+ and validated.observation.has_action
583
+ ):
584
+ chosen = validated
585
+ self._retry.log(
586
+ "WARN",
587
+ "codex_completion_gate_continued "
588
+ f"model={str(upstream_body.get('model') or '-')}",
589
+ )
590
+ self._write_buffered_response(handler, chosen)
591
+ if validated is not None and validated is not chosen:
592
+ validated.close()
593
+ else:
594
+ self._write_buffered_response(handler, chosen)
595
+ finally:
596
+ if chosen is not buffered:
597
+ chosen.close()
598
+ buffered.close()
599
+ else:
600
+ self._write_response(handler, response, preamble)
528
601
  break
529
602
 
603
+ def _buffer_codex_response(
604
+ self, response: Any, preamble: Any
605
+ ) -> _BufferedCodexResponse:
606
+ stream = tempfile.SpooledTemporaryFile(max_size=1024 * 1024)
607
+ observation = ResponsesCompletionObservation()
608
+ initial = bytes(getattr(preamble, "payload", b"") or b"")
609
+ if initial:
610
+ stream.write(initial)
611
+ observation.feed(initial)
612
+ while chunk := response.read(65_536):
613
+ stream.write(chunk)
614
+ observation.feed(chunk)
615
+ observation.finish()
616
+ stream.seek(0)
617
+ return _BufferedCodexResponse(
618
+ status=int(getattr(response, "status", 200)),
619
+ headers=response.headers,
620
+ stream=stream,
621
+ observation=observation,
622
+ )
623
+
624
+ def _validate_codex_completion(
625
+ self,
626
+ provider: str,
627
+ config: dict[str, Any],
628
+ url: str,
629
+ headers: dict[str, str],
630
+ upstream_body: dict[str, Any],
631
+ observation: ResponsesCompletionObservation,
632
+ ) -> _BufferedCodexResponse | None:
633
+ validation_body = completion_check_body(upstream_body, observation)
634
+ validation_data = json.dumps(validation_body).encode("utf-8")
635
+ dump_upstream_request(url, validation_data, self._retry.log)
636
+ request = urllib.request.Request(
637
+ url, data=validation_data, headers=headers, method="POST"
638
+ )
639
+ try:
640
+ with self._open_with_transport_retry(
641
+ request,
642
+ provider,
643
+ config,
644
+ str(upstream_body.get("model") or ""),
645
+ "completion_check",
646
+ ) as response:
647
+ preamble = self._retry.read_preamble(response)
648
+ if preamble is not None and (
649
+ preamble.capacity_error_code
650
+ or getattr(preamble, "context_error_code", None)
651
+ ):
652
+ return None
653
+ return self._buffer_codex_response(response, preamble)
654
+ except Exception as exc: # noqa: BLE001 - preserve the original response
655
+ self._retry.log(
656
+ "WARN",
657
+ "codex_completion_gate_failed "
658
+ f"error={type(exc).__name__}: {exc}",
659
+ )
660
+ return None
661
+
662
+ def _write_buffered_response(
663
+ self, handler: BaseHTTPRequestHandler, response: _BufferedCodexResponse
664
+ ) -> None:
665
+ handler.send_response(response.status)
666
+ self.copy_response_headers(handler, response.headers)
667
+ handler.end_headers()
668
+ response.stream.seek(0)
669
+ while chunk := response.stream.read(65_536):
670
+ handler.wfile.write(chunk)
671
+ handler.wfile.flush()
672
+
530
673
  def _open_with_transport_retry(
531
674
  self,
532
675
  request: urllib.request.Request,
@@ -613,6 +756,8 @@ class EventHttpPorts:
613
756
 
614
757
 
615
758
  class EventHttpAdapter:
759
+ _WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
760
+
616
761
  def __init__(self, ports: EventHttpPorts) -> None:
617
762
  self._ports = ports
618
763
 
@@ -650,6 +795,8 @@ class EventHttpAdapter:
650
795
  },
651
796
  )
652
797
  return True
798
+ if path == "/ca/events/ws":
799
+ return self._handle_websocket(handler, query)
653
800
  if path != "/ca/events/stream":
654
801
  return False
655
802
  last_id = self.query_int(query, "after", 0)
@@ -659,11 +806,30 @@ class EventHttpAdapter:
659
806
  handler.send_header("connection", "close")
660
807
  handler.end_headers()
661
808
  try:
662
- last_id = self._write_events(handler, self._ports.recent(limit=200, min_id=last_id), last_id)
809
+ level = (query.get("level") or [None])[0]
810
+ category = (query.get("category") or [None])[0]
811
+ last_id = self._write_events(
812
+ handler,
813
+ self._ports.recent(
814
+ limit=200,
815
+ min_id=last_id,
816
+ level=level,
817
+ category=category,
818
+ ),
819
+ last_id,
820
+ )
663
821
  while True:
664
- events = self._ports.wait_after(last_id, timeout=15.0)
822
+ observed = self._ports.wait_after(last_id, timeout=15.0)
823
+ observed_last_id = self._max_event_id(observed, last_id)
824
+ events = self._filtered(
825
+ observed,
826
+ level=level,
827
+ category=category,
828
+ )
665
829
  if events:
666
830
  last_id = self._write_events(handler, events, last_id)
831
+ if observed:
832
+ last_id = max(last_id, observed_last_id)
667
833
  else:
668
834
  handler.wfile.write(b": keepalive\n\n")
669
835
  handler.wfile.flush()
@@ -673,6 +839,125 @@ class EventHttpAdapter:
673
839
  self._ports.log("DEBUG", f"events stream closed: {type(exc).__name__}: {exc}")
674
840
  return True
675
841
 
842
+ def _handle_websocket(
843
+ self,
844
+ handler: BaseHTTPRequestHandler,
845
+ query: dict[str, list[str]],
846
+ ) -> bool:
847
+ upgrade = str(handler.headers.get("upgrade") or "").strip().lower()
848
+ key = str(handler.headers.get("sec-websocket-key") or "").strip()
849
+ version = str(handler.headers.get("sec-websocket-version") or "13").strip()
850
+ if upgrade != "websocket" or not key or version != "13":
851
+ self._ports.write_json(
852
+ handler,
853
+ {
854
+ "ok": False,
855
+ "error": "websocket_upgrade_required",
856
+ "required_version": "13",
857
+ },
858
+ 400,
859
+ )
860
+ return True
861
+ accept = base64.b64encode(
862
+ hashlib.sha1((key + self._WEBSOCKET_GUID).encode("ascii")).digest()
863
+ ).decode("ascii")
864
+ handler.protocol_version = "HTTP/1.1"
865
+ handler.send_response(101)
866
+ handler.send_header("upgrade", "websocket")
867
+ handler.send_header("connection", "Upgrade")
868
+ handler.send_header("sec-websocket-accept", accept)
869
+ handler.end_headers()
870
+ handler.close_connection = True
871
+ last_id = self.query_int(query, "after", 0)
872
+ level = (query.get("level") or [None])[0]
873
+ category = (query.get("category") or [None])[0]
874
+ try:
875
+ last_id = self._write_ws_events(
876
+ handler,
877
+ self._ports.recent(
878
+ limit=200,
879
+ min_id=last_id,
880
+ level=level,
881
+ category=category,
882
+ ),
883
+ last_id,
884
+ )
885
+ while True:
886
+ observed = self._ports.wait_after(last_id, timeout=15.0)
887
+ observed_last_id = self._max_event_id(observed, last_id)
888
+ events = self._filtered(
889
+ observed,
890
+ level=level,
891
+ category=category,
892
+ )
893
+ if events:
894
+ last_id = self._write_ws_events(handler, events, last_id)
895
+ if observed:
896
+ last_id = max(last_id, observed_last_id)
897
+ else:
898
+ handler.wfile.write(self._websocket_frame(b"", opcode=0x9))
899
+ handler.wfile.flush()
900
+ except (BrokenPipeError, ConnectionResetError):
901
+ return True
902
+ except Exception as exc:
903
+ self._ports.log("DEBUG", f"events websocket closed: {type(exc).__name__}: {exc}")
904
+ return True
905
+
906
+ @staticmethod
907
+ def _max_event_id(events: list[dict[str, Any]], default: int) -> int:
908
+ return max(
909
+ (int(event.get("id") or 0) for event in events),
910
+ default=default,
911
+ )
912
+
913
+ @staticmethod
914
+ def _filtered(
915
+ events: list[dict[str, Any]],
916
+ *,
917
+ level: str | None,
918
+ category: str | None,
919
+ ) -> list[dict[str, Any]]:
920
+ if level:
921
+ ranks = {"trace": 10, "debug": 20, "info": 30, "warn": 40, "error": 50, "fatal": 60}
922
+ threshold = ranks.get(str(level).lower(), 30)
923
+ events = [
924
+ event
925
+ for event in events
926
+ if ranks.get(str(event.get("level") or "info").lower(), 30) >= threshold
927
+ ]
928
+ if category:
929
+ events = [
930
+ event
931
+ for event in events
932
+ if str(event.get("category") or "").startswith(category)
933
+ ]
934
+ return events
935
+
936
+ @staticmethod
937
+ def _websocket_frame(payload: bytes, *, opcode: int = 0x1) -> bytes:
938
+ size = len(payload)
939
+ head = bytes([0x80 | (opcode & 0x0F)])
940
+ if size < 126:
941
+ return head + bytes([size]) + payload
942
+ if size <= 0xFFFF:
943
+ return head + bytes([126]) + size.to_bytes(2, "big") + payload
944
+ return head + bytes([127]) + size.to_bytes(8, "big") + payload
945
+
946
+ def _write_ws_events(
947
+ self,
948
+ handler: BaseHTTPRequestHandler,
949
+ events: list[dict[str, Any]],
950
+ last_id: int,
951
+ ) -> int:
952
+ for event in events:
953
+ last_id = max(last_id, int(event.get("id") or 0))
954
+ payload = json.dumps(event, ensure_ascii=False, separators=(",", ":")).encode(
955
+ "utf-8"
956
+ )
957
+ handler.wfile.write(self._websocket_frame(payload))
958
+ handler.wfile.flush()
959
+ return last_id
960
+
676
961
  @staticmethod
677
962
  def _write_events(
678
963
  handler: BaseHTTPRequestHandler,
@@ -747,6 +1032,8 @@ class RouterHttpHandler(BaseHTTPRequestHandler):
747
1032
  return
748
1033
  if services.core.reject_external(self, cfg):
749
1034
  return
1035
+ if services.files is not None and services.files.get(self, path, cfg):
1036
+ return
750
1037
  endpoints = services.get
751
1038
  if endpoints.tui(self, path, query):
752
1039
  return
@@ -1068,11 +1355,19 @@ class RouterHttpHandler(BaseHTTPRequestHandler):
1068
1355
  length = self._validated_content_length(path, services)
1069
1356
  if length is None:
1070
1357
  return
1358
+ content_type = str(
1359
+ self.headers.get("content-type") or "application/json"
1360
+ )
1361
+ if (
1362
+ services.files is not None
1363
+ and services.files.post(self, path, length, content_type, cfg)
1364
+ ):
1365
+ return
1071
1366
  try:
1072
1367
  admission = services.core.request_body_policy.admit(
1073
1368
  path,
1074
1369
  length,
1075
- str(self.headers.get("content-type") or "application/json"),
1370
+ content_type,
1076
1371
  )
1077
1372
  with admission:
1078
1373
  raw = self._read_request_body(path, services, length)
@@ -1292,6 +1587,8 @@ class RouterHttpHandler(BaseHTTPRequestHandler):
1292
1587
  cfg = services.core.load_config()
1293
1588
  if services.core.reject_external(self, cfg):
1294
1589
  return
1590
+ if services.files is not None and services.files.delete(self, path, cfg):
1591
+ return
1295
1592
  services.presentation.write_json(
1296
1593
  self,
1297
1594
  {"type": "error", "error": {"type": "not_found_error", "message": path}},
@@ -71,6 +71,7 @@ class SseObservabilityPorts:
71
71
  current_log_level: Callable[[], int]
72
72
  truncate: Callable[[str, int], str]
73
73
  log: Callable[[str, str], None]
74
+ event_publish: Callable[..., Any]
74
75
 
75
76
 
76
77
  @dataclass(frozen=True, slots=True)
@@ -185,6 +186,31 @@ class RouterObservabilityContext:
185
186
 
186
187
  def append_tool_call(self, *args: Any, **kwargs: Any) -> None:
187
188
  self.sse_repository().append_tool_call(*args, **kwargs)
189
+ event = str(args[0] if args else kwargs.get("event") or "tool_call")
190
+ payload = args[1] if len(args) > 1 else kwargs.get("payload")
191
+ payload = payload if isinstance(payload, dict) else {}
192
+ provider = event.removesuffix("_stream_tool_call").replace("_", "-")
193
+ name = str(payload.get("matched_name") or payload.get("raw_name") or "tool").strip()
194
+ config = self.preview.load_config()
195
+ tool_event_config = config.get("tool_call_events")
196
+ tool_event_config = tool_event_config if isinstance(tool_event_config, dict) else {}
197
+ data = {
198
+ "call_id": str(payload.get("tool_id") or ""),
199
+ "name": name,
200
+ "call_type": event,
201
+ "sse_index": payload.get("sse_index"),
202
+ }
203
+ if bool(tool_event_config.get("include_arguments", True)):
204
+ data["arguments"] = payload.get("emitted_input")
205
+ self.sse.event_publish(
206
+ level="info",
207
+ category="tool.call",
208
+ message=f"{provider} tool call: {name}",
209
+ source="router-stream",
210
+ provider=provider,
211
+ model=str(payload.get("model") or ""),
212
+ data=data,
213
+ )
188
214
 
189
215
 
190
216
  @dataclass(frozen=True, slots=True)
@@ -65,6 +65,7 @@ class RouterServerContext:
65
65
  "chat": "/ca/chat/health",
66
66
  "plan": "/ca/plan/artifacts",
67
67
  "events": "/ca/events",
68
+ "events_websocket": "/ca/events/ws",
68
69
  "external_event_receivers": "/ca/events/receivers",
69
70
  "otlp_logs": "/v1/logs",
70
71
  "telemetry_logs_tool": "POST /ca/mcp tools/call telemetry_logs",
@@ -116,12 +116,42 @@ class ZcodeRuntimeAdapter(CliRuntimeAdapter):
116
116
  return RuntimeCommand(argv=tuple(argv), env=dict(self.environment), cwd=spec.cwd)
117
117
 
118
118
 
119
+ class MuseRuntimeAdapter(CliRuntimeAdapter):
120
+ """Official Muse Code CLI with optional WSL command prefix."""
121
+
122
+ def build_command(self, spec: LaunchSpec) -> RuntimeCommand:
123
+ options = spec.runtime.options
124
+ argv = [str(spec.runtime.executable or self.executable)]
125
+ argv.extend(str(value) for value in options.get("prefix_args", ()))
126
+ passthrough = list(spec.passthrough)
127
+ subcommand = passthrough[0] if passthrough else ""
128
+ utility_commands = {
129
+ "config", "export", "trace", "skills", "sandbox", "schema", "serve",
130
+ "session-message", "auth", "login", "logout", "init", "help", "version",
131
+ }
132
+ if subcommand in utility_commands or subcommand in {"--help", "-h", "--version", "-V", "-v"}:
133
+ argv.extend(passthrough)
134
+ return RuntimeCommand(argv=tuple(argv), env=dict(self.environment), cwd=spec.cwd)
135
+ if subcommand in {"exec", "resume"}:
136
+ argv.append(passthrough.pop(0))
137
+ argv.extend(str(value) for value in options.get("yolo_args", ()))
138
+ model = str(options.get("model") or "").strip()
139
+ if model:
140
+ argv.extend(("--model", model))
141
+ effort = str(options.get("reasoning_effort") or "").strip()
142
+ if effort:
143
+ argv.extend(("--reasoning-effort", effort))
144
+ argv.extend(passthrough)
145
+ return RuntimeCommand(argv=tuple(argv), env=dict(self.environment), cwd=spec.cwd)
146
+
147
+
119
148
  RUNTIME_ADAPTERS: AdapterRegistry[RuntimeAdapter] = AdapterRegistry()
120
149
  RUNTIME_ADAPTERS.register("claude", lambda **kwargs: ClaudeRuntimeAdapter(name="claude", **kwargs))
121
150
  RUNTIME_ADAPTERS.register("codex", lambda **kwargs: CodexRuntimeAdapter(name="codex", **kwargs))
122
151
  RUNTIME_ADAPTERS.register("agy", lambda **kwargs: AgyRuntimeAdapter(name="agy", **kwargs))
123
152
  RUNTIME_ADAPTERS.register("grok", lambda **kwargs: GrokRuntimeAdapter(name="grok", **kwargs))
124
153
  RUNTIME_ADAPTERS.register("zcode", lambda **kwargs: ZcodeRuntimeAdapter(name="zcode", **kwargs))
154
+ RUNTIME_ADAPTERS.register("muse", lambda **kwargs: MuseRuntimeAdapter(name="muse", **kwargs))
125
155
 
126
156
 
127
157
  __all__ = [
@@ -131,5 +161,6 @@ __all__ = [
131
161
  "CliRuntimeAdapter",
132
162
  "CodexRuntimeAdapter",
133
163
  "GrokRuntimeAdapter",
164
+ "MuseRuntimeAdapter",
134
165
  "ZcodeRuntimeAdapter",
135
166
  ]
@@ -12,7 +12,17 @@ ANTHROPIC_MODEL_DOCS_URLS = (
12
12
  ANTHROPIC_MODEL_DOCS_URL,
13
13
  "https://platform.claude.com/docs/en/about-claude/models/overview",
14
14
  )
15
+ ANTHROPIC_ONE_MILLION_MODEL_IDS: tuple[str, ...] = (
16
+ "claude-fable-5-1[1m]",
17
+ "claude-fable-5[1m]",
18
+ "claude-opus-5[1m]",
19
+ "claude-opus-4-8[1m]",
20
+ "claude-sonnet-5[1m]",
21
+ "claude-sonnet-4-6[1m]",
22
+ )
15
23
  ANTHROPIC_PUBLIC_MODEL_FALLBACK_IDS: tuple[str, ...] = (
24
+ *ANTHROPIC_ONE_MILLION_MODEL_IDS,
25
+ "claude-fable-5-1",
16
26
  "claude-fable-5",
17
27
  "claude-opus-5",
18
28
  "claude-opus-4-8",
@@ -87,7 +97,7 @@ OPENCODE_ENDPOINT_ALIASES = {
87
97
  }
88
98
 
89
99
  APP_NAME = "Ciel Runtime"
90
- VERSION = "0.2.37"
100
+ VERSION = "0.2.38"
91
101
  CREDITS = "Credits: One Ciel LLC"
92
102
  PRELAUNCH_CANCEL = 10
93
103
  PRELAUNCH_LAUNCH_CODEX = 11
@@ -175,12 +185,24 @@ ROUTED_CODEX_COMPAT_PROMPT = (
175
185
  "You are running inside Codex CLI through the ciel-runtime router. Do not stop after announcing what you plan to do. "
176
186
  "When the user asks you to inspect, create, edit, or run code, call the tools available in this turn and report the concrete result. "
177
187
  "Never end a turn with only a statement of intent such as saying you will now start; perform the work in the same turn. "
188
+ "If you identify a concrete next action in a progress report, call the next tool before returning a final answer. "
189
+ "For a persistent monitoring goal, unchanged external state is a verified wait, not a blocker: do not mark the goal blocked merely because the next market, job, or service event has not occurred. "
190
+ "Poll a confirmed-live handle or use the available recurring monitor or wait mechanism, and leave the goal active while safe in-scope monitoring work remains. "
178
191
  "If the task has several reasonable in-scope parts, do all of them; do not ask which part to start unless the user explicitly asked for a choice. "
179
192
  "If you decide not to use tools, give the complete answer in the same turn. "
180
193
  "Use exactly the tool schema provided and do not invent extra fields, and never write pseudo tool calls or partial JSON where a real tool call is required."
181
194
  )
182
195
  LANGUAGES = {"en": "English", "ko": "한국어", "ja": "日本語", "zh": "中文"}
183
196
  MODEL_PRESETS: dict[str, dict[str, Any]] = {
197
+ # Muse Spark reasoning tokens consume the output budget. A live
198
+ # contributor probe exhausted 128 tokens before it could emit tool_use;
199
+ # 4096 completed the same request and is also the documented minimum used
200
+ # by Meta's long-media examples.
201
+ "muse-spark-1.3": {"compat_max_tokens": 4096, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1048576},
202
+ "muse-spark-1.3-contributor": {"compat_max_tokens": 4096, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1048576},
203
+ "muse-spark-1.2": {"compat_max_tokens": 4096, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1048576},
204
+ "muse-spark-1.2-contributor": {"compat_max_tokens": 4096, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1048576},
205
+ "muse-spark-1.1": {"compat_max_tokens": 4096, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1048576},
184
206
  "glm-5.3": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
185
207
  "glm-5.3:cloud": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},
186
208
  "deepseek-v4-flash:0731": {"compat_max_tokens": 64, "thinking": True, "num_ctx_min": 32768, "num_ctx_max": 1000000},