@oneciel-ai/ciel-runtime 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/README.md +132 -0
  2. package/ciel-runtime-menu.py +56 -6
  3. package/ciel_runtime.py +9379 -35909
  4. package/ciel_runtime_support/advisor_client.py +193 -0
  5. package/ciel_runtime_support/advisor_policy.py +320 -0
  6. package/ciel_runtime_support/advisor_refinement.py +160 -0
  7. package/ciel_runtime_support/advisor_request_builder.py +261 -0
  8. package/ciel_runtime_support/agy_installer.py +169 -0
  9. package/ciel_runtime_support/agy_mcp_restore.py +182 -0
  10. package/ciel_runtime_support/anthropic_model_policy.py +186 -0
  11. package/ciel_runtime_support/anthropic_response_writer.py +255 -0
  12. package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
  13. package/ciel_runtime_support/api_key_cooldown.py +159 -0
  14. package/ciel_runtime_support/architecture.py +488 -1
  15. package/ciel_runtime_support/architecture_budget.py +42 -0
  16. package/ciel_runtime_support/channel_backlog.py +90 -0
  17. package/ciel_runtime_support/channel_cli.py +119 -0
  18. package/ciel_runtime_support/channel_compact_injection.py +82 -0
  19. package/ciel_runtime_support/channel_compact_poll.py +67 -0
  20. package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
  21. package/ciel_runtime_support/channel_config_service.py +281 -0
  22. package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
  23. package/ciel_runtime_support/channel_connection_registry.py +128 -0
  24. package/ciel_runtime_support/channel_connection_worker.py +284 -0
  25. package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
  26. package/ciel_runtime_support/channel_cursor_repository.py +89 -0
  27. package/ciel_runtime_support/channel_cursor_service.py +178 -0
  28. package/ciel_runtime_support/channel_event_identity.py +212 -0
  29. package/ciel_runtime_support/channel_event_projection.py +315 -0
  30. package/ciel_runtime_support/channel_inflight.py +127 -0
  31. package/ciel_runtime_support/channel_injection.py +115 -0
  32. package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
  33. package/ciel_runtime_support/channel_launch_policy.py +180 -0
  34. package/ciel_runtime_support/channel_llm_context.py +156 -0
  35. package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
  36. package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
  37. package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
  38. package/ciel_runtime_support/channel_mcp_tools.py +240 -0
  39. package/ciel_runtime_support/channel_mcp_transport.py +394 -0
  40. package/ciel_runtime_support/channel_message_dedupe.py +65 -0
  41. package/ciel_runtime_support/channel_message_policy.py +256 -0
  42. package/ciel_runtime_support/channel_message_prompt.py +305 -0
  43. package/ciel_runtime_support/channel_message_repository.py +234 -0
  44. package/ciel_runtime_support/channel_notification_projection.py +217 -0
  45. package/ciel_runtime_support/channel_panel.py +162 -0
  46. package/ciel_runtime_support/channel_pending_injection.py +209 -0
  47. package/ciel_runtime_support/channel_pending_poll.py +109 -0
  48. package/ciel_runtime_support/channel_probe_cache.py +433 -0
  49. package/ciel_runtime_support/channel_probe_report.py +101 -0
  50. package/ciel_runtime_support/channel_runtime_environment.py +181 -0
  51. package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
  52. package/ciel_runtime_support/channel_session_repository.py +90 -0
  53. package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
  54. package/ciel_runtime_support/channel_terminal_input.py +277 -0
  55. package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
  56. package/ciel_runtime_support/channel_tool_context.py +166 -0
  57. package/ciel_runtime_support/channel_transcript.py +414 -0
  58. package/ciel_runtime_support/channel_transcript_repository.py +96 -0
  59. package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
  60. package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
  61. package/ciel_runtime_support/chat_files.py +138 -0
  62. package/ciel_runtime_support/chat_http_controller.py +235 -0
  63. package/ciel_runtime_support/claude_environment.py +375 -0
  64. package/ciel_runtime_support/claude_router.py +247 -193
  65. package/ciel_runtime_support/cli_dispatch.py +792 -0
  66. package/ciel_runtime_support/cli_parser.py +165 -0
  67. package/ciel_runtime_support/cli_usage.py +100 -0
  68. package/ciel_runtime_support/codex_app_server.py +20 -5
  69. package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
  70. package/ciel_runtime_support/codex_cli.py +42 -6
  71. package/ciel_runtime_support/codex_config.py +323 -0
  72. package/ciel_runtime_support/codex_launch_configuration.py +240 -0
  73. package/ciel_runtime_support/codex_launch_policy.py +66 -0
  74. package/ciel_runtime_support/codex_mcp_integration.py +195 -0
  75. package/ciel_runtime_support/codex_mcp_restore.py +304 -0
  76. package/ciel_runtime_support/codex_model_catalog.py +133 -0
  77. package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
  78. package/ciel_runtime_support/codex_router.py +147 -1
  79. package/ciel_runtime_support/codex_session_repository.py +115 -0
  80. package/ciel_runtime_support/codex_session_selection.py +114 -0
  81. package/ciel_runtime_support/command_asset_installer.py +103 -0
  82. package/ciel_runtime_support/compatibility_probe.py +295 -0
  83. package/ciel_runtime_support/compatibility_protocol.py +251 -0
  84. package/ciel_runtime_support/compatibility_runtime.py +166 -0
  85. package/ciel_runtime_support/compatibility_test.py +370 -0
  86. package/ciel_runtime_support/config_migrations.py +307 -0
  87. package/ciel_runtime_support/config_repository.py +175 -0
  88. package/ciel_runtime_support/config_value_codec.py +64 -0
  89. package/ciel_runtime_support/configuration_cli.py +374 -0
  90. package/ciel_runtime_support/context_compaction.py +280 -0
  91. package/ciel_runtime_support/context_setup.py +208 -0
  92. package/ciel_runtime_support/context_summary_policy.py +392 -0
  93. package/ciel_runtime_support/credential_cli.py +104 -0
  94. package/ciel_runtime_support/credential_management.py +261 -0
  95. package/ciel_runtime_support/credentials.py +269 -0
  96. package/ciel_runtime_support/executable_discovery.py +141 -0
  97. package/ciel_runtime_support/github_copilot_oauth.py +335 -0
  98. package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
  99. package/ciel_runtime_support/header_forwarding.py +73 -0
  100. package/ciel_runtime_support/headless_config.py +221 -0
  101. package/ciel_runtime_support/http_response.py +129 -0
  102. package/ciel_runtime_support/install_diagnostics.py +149 -0
  103. package/ciel_runtime_support/kimi_identity.py +123 -0
  104. package/ciel_runtime_support/launch_diagnostics.py +204 -0
  105. package/ciel_runtime_support/launch_state.py +127 -0
  106. package/ciel_runtime_support/live_api_key_controller.py +58 -0
  107. package/ciel_runtime_support/llm_config_http.py +148 -0
  108. package/ciel_runtime_support/llm_option_config.py +259 -0
  109. package/ciel_runtime_support/llm_presentation_data.py +447 -0
  110. package/ciel_runtime_support/llm_presets.py +773 -0
  111. package/ciel_runtime_support/lm_studio_runtime.py +401 -0
  112. package/ciel_runtime_support/managed_mcp_config.py +144 -0
  113. package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
  114. package/ciel_runtime_support/managed_service_cleanup.py +89 -0
  115. package/ciel_runtime_support/mcp_config_reader.py +230 -0
  116. package/ciel_runtime_support/mcp_http_proxy.py +607 -0
  117. package/ciel_runtime_support/mcp_inventory.py +59 -0
  118. package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
  119. package/ciel_runtime_support/mcp_probe_codec.py +136 -0
  120. package/ciel_runtime_support/mcp_probe_transport.py +328 -0
  121. package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
  122. package/ciel_runtime_support/mcp_proxy_config.py +107 -0
  123. package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
  124. package/ciel_runtime_support/mcp_proxy_process.py +560 -0
  125. package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
  126. package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
  127. package/ciel_runtime_support/mcp_transport.py +146 -0
  128. package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
  129. package/ciel_runtime_support/model_catalog_projection.py +61 -0
  130. package/ciel_runtime_support/model_context_hints.py +109 -0
  131. package/ciel_runtime_support/model_panel.py +147 -0
  132. package/ciel_runtime_support/model_registry_repository.py +231 -0
  133. package/ciel_runtime_support/npm_runtime.py +191 -0
  134. package/ciel_runtime_support/ollama_catalog.py +462 -0
  135. package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
  136. package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
  137. package/ciel_runtime_support/ollama_context_sync.py +87 -0
  138. package/ciel_runtime_support/ollama_forwarding.py +449 -0
  139. package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
  140. package/ciel_runtime_support/openai_chat_router.py +64 -0
  141. package/ciel_runtime_support/openai_forwarding.py +194 -0
  142. package/ciel_runtime_support/openai_responses_router.py +291 -0
  143. package/ciel_runtime_support/openai_responses_stream.py +135 -0
  144. package/ciel_runtime_support/output_budget.py +89 -0
  145. package/ciel_runtime_support/package_lifecycle.py +217 -0
  146. package/ciel_runtime_support/plan_artifact_controller.py +104 -0
  147. package/ciel_runtime_support/prelaunch.py +959 -0
  148. package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
  149. package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
  150. package/ciel_runtime_support/prelaunch_terminal.py +764 -0
  151. package/ciel_runtime_support/process_control.py +708 -0
  152. package/ciel_runtime_support/prompt_compaction.py +322 -0
  153. package/ciel_runtime_support/prompt_injection.py +176 -0
  154. package/ciel_runtime_support/protocols/__init__.py +24 -0
  155. package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
  156. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
  157. package/ciel_runtime_support/protocols/chat_projection.py +315 -0
  158. package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
  159. package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
  160. package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
  161. package/ciel_runtime_support/protocols/ollama_response.py +231 -0
  162. package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
  163. package/ciel_runtime_support/protocols/openai_responses.py +271 -0
  164. package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
  165. package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
  166. package/ciel_runtime_support/provider_adapters.py +160 -0
  167. package/ciel_runtime_support/provider_catalog_sources.py +316 -0
  168. package/ciel_runtime_support/provider_choice.py +201 -0
  169. package/ciel_runtime_support/provider_compatibility.py +165 -0
  170. package/ciel_runtime_support/provider_config_mutations.py +361 -0
  171. package/ciel_runtime_support/provider_configuration_service.py +162 -0
  172. package/ciel_runtime_support/provider_context.py +308 -0
  173. package/ciel_runtime_support/provider_contract_projection.py +73 -0
  174. package/ciel_runtime_support/provider_descriptor.py +82 -0
  175. package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
  176. package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
  177. package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
  178. package/ciel_runtime_support/provider_limits.py +457 -0
  179. package/ciel_runtime_support/provider_model_identity.py +139 -0
  180. package/ciel_runtime_support/provider_model_selection.py +431 -0
  181. package/ciel_runtime_support/provider_model_specs.py +142 -0
  182. package/ciel_runtime_support/provider_models.py +263 -0
  183. package/ciel_runtime_support/provider_network.py +176 -0
  184. package/ciel_runtime_support/provider_option_cli.py +238 -0
  185. package/ciel_runtime_support/provider_option_panel.py +275 -0
  186. package/ciel_runtime_support/provider_option_status.py +192 -0
  187. package/ciel_runtime_support/provider_policy.py +101 -0
  188. package/ciel_runtime_support/provider_query_policy.py +67 -0
  189. package/ciel_runtime_support/provider_readiness.py +112 -0
  190. package/ciel_runtime_support/provider_request_access.py +131 -0
  191. package/ciel_runtime_support/provider_request_builder.py +250 -0
  192. package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
  193. package/ciel_runtime_support/provider_runtime_info.py +113 -0
  194. package/ciel_runtime_support/provider_runtime_modes.py +150 -0
  195. package/ciel_runtime_support/provider_sampling_policy.py +46 -0
  196. package/ciel_runtime_support/provider_status.py +145 -0
  197. package/ciel_runtime_support/provider_timeout_policy.py +184 -0
  198. package/ciel_runtime_support/provider_tool_policy.py +145 -0
  199. package/ciel_runtime_support/providers/__init__.py +65 -0
  200. package/ciel_runtime_support/providers/anthropic.py +160 -0
  201. package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
  202. package/ciel_runtime_support/providers/base.py +232 -0
  203. package/ciel_runtime_support/providers/catalog.py +326 -0
  204. package/ciel_runtime_support/providers/cloud.py +194 -0
  205. package/ciel_runtime_support/providers/constants.py +54 -0
  206. package/ciel_runtime_support/providers/deepseek.py +115 -0
  207. package/ciel_runtime_support/providers/fireworks.py +158 -0
  208. package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
  209. package/ciel_runtime_support/providers/kimi.py +297 -0
  210. package/ciel_runtime_support/providers/lm_studio.py +76 -0
  211. package/ciel_runtime_support/providers/meta.py +257 -0
  212. package/ciel_runtime_support/providers/native.py +194 -0
  213. package/ciel_runtime_support/providers/nim.py +69 -0
  214. package/ciel_runtime_support/providers/nvidia.py +158 -0
  215. package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
  216. package/ciel_runtime_support/providers/ollama.py +181 -0
  217. package/ciel_runtime_support/providers/ollama_context.py +195 -0
  218. package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
  219. package/ciel_runtime_support/providers/opencode.py +220 -0
  220. package/ciel_runtime_support/providers/opencode_go.py +37 -0
  221. package/ciel_runtime_support/providers/openrouter.py +57 -0
  222. package/ciel_runtime_support/providers/vllm.py +65 -0
  223. package/ciel_runtime_support/providers/zai.py +119 -0
  224. package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
  225. package/ciel_runtime_support/rate_limit_policy.py +117 -0
  226. package/ciel_runtime_support/rate_limit_repository.py +154 -0
  227. package/ciel_runtime_support/registry.py +46 -0
  228. package/ciel_runtime_support/request_shortcuts.py +253 -0
  229. package/ciel_runtime_support/request_trace.py +323 -0
  230. package/ciel_runtime_support/response_collection.py +209 -0
  231. package/ciel_runtime_support/router_access.py +238 -0
  232. package/ciel_runtime_support/router_client_lifecycle.py +366 -0
  233. package/ciel_runtime_support/router_health_policy.py +101 -0
  234. package/ciel_runtime_support/router_http.py +513 -0
  235. package/ciel_runtime_support/router_process_lifecycle.py +401 -0
  236. package/ciel_runtime_support/router_rate_limit_service.py +285 -0
  237. package/ciel_runtime_support/router_server_runtime.py +103 -0
  238. package/ciel_runtime_support/router_shortcuts.py +201 -0
  239. package/ciel_runtime_support/routing_fallback.py +73 -0
  240. package/ciel_runtime_support/runtime_activity_repository.py +143 -0
  241. package/ciel_runtime_support/runtime_adapters.py +104 -0
  242. package/ciel_runtime_support/runtime_command_factory.py +73 -0
  243. package/ciel_runtime_support/runtime_compatibility.py +50 -0
  244. package/ciel_runtime_support/runtime_constants.py +178 -0
  245. package/ciel_runtime_support/runtime_launch.py +1602 -0
  246. package/ciel_runtime_support/runtime_llm_options.py +312 -0
  247. package/ciel_runtime_support/runtime_logging.py +161 -0
  248. package/ciel_runtime_support/runtime_paths.py +157 -0
  249. package/ciel_runtime_support/runtime_restart.py +84 -0
  250. package/ciel_runtime_support/runtime_upgrade.py +149 -0
  251. package/ciel_runtime_support/secure_json_repository.py +55 -0
  252. package/ciel_runtime_support/session_import.py +356 -0
  253. package/ciel_runtime_support/settings_repository.py +8 -0
  254. package/ciel_runtime_support/slash_command_assets.py +211 -0
  255. package/ciel_runtime_support/sse_stream.py +57 -0
  256. package/ciel_runtime_support/sse_trace.py +225 -0
  257. package/ciel_runtime_support/statusline_script.py +593 -0
  258. package/ciel_runtime_support/statusline_settings.py +53 -0
  259. package/ciel_runtime_support/stream_chunk_policy.py +18 -0
  260. package/ciel_runtime_support/streaming_anthropic.py +1955 -0
  261. package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
  262. package/ciel_runtime_support/terminal_platform_io.py +127 -0
  263. package/ciel_runtime_support/timeout_profile.py +196 -0
  264. package/ciel_runtime_support/tool_dialects.py +85 -0
  265. package/ciel_runtime_support/tool_exposure_policy.py +63 -0
  266. package/ciel_runtime_support/tool_guard_hooks.py +218 -0
  267. package/ciel_runtime_support/tool_request_projection.py +96 -0
  268. package/ciel_runtime_support/tool_schema.py +483 -0
  269. package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
  270. package/ciel_runtime_support/ui_text.py +266 -0
  271. package/ciel_runtime_support/upstream_error_policy.py +104 -0
  272. package/ciel_runtime_support/upstream_retry.py +419 -0
  273. package/ciel_runtime_support/upstream_stream_io.py +106 -0
  274. package/ciel_runtime_support/usage_events.py +96 -0
  275. package/ciel_runtime_support/visible_stream_filters.py +130 -0
  276. package/ciel_runtime_support/web_endpoints.py +447 -0
  277. package/ciel_runtime_support/web_ui.py +915 -0
  278. package/ciel_runtime_support/web_ui_controller.py +189 -0
  279. package/ciel_runtime_support/windows_console_input.py +137 -0
  280. package/ciel_runtime_support/windows_console_mode.py +112 -0
  281. package/docs/Architecture.md +54 -0
  282. package/docs/Configuration.md +17 -0
  283. package/docs/Module-Map.md +1093 -26
  284. package/docs/Providers.md +46 -1
  285. package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
  286. package/npm-bin/run-ciel-runtime.js +22 -2
  287. package/package.json +9 -2
@@ -0,0 +1,42 @@
1
+ """Enforce the bounded-context migration's monotonically decreasing budget."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ FINAL_FILE_LINE_BUDGET = 4_999
9
+ MAIN_FILE_LINE_BUDGET = 12_989
10
+
11
+
12
+ def production_python_files(root: Path) -> tuple[Path, ...]:
13
+ support = tuple(sorted((root / "ciel_runtime_support").rglob("*.py")))
14
+ entrypoints = (
15
+ root / "ciel_runtime.py",
16
+ root / "ciel-runtime-menu.py",
17
+ root / "ciel-runtime-tool-guard.py",
18
+ )
19
+ return tuple(path for path in (*entrypoints, *support) if path.is_file())
20
+
21
+
22
+ def physical_line_count(path: Path) -> int:
23
+ return len(path.read_text(encoding="utf-8").splitlines())
24
+
25
+
26
+ def architecture_budget_violations(root: Path) -> tuple[str, ...]:
27
+ violations: list[str] = []
28
+ main = root / "ciel_runtime.py"
29
+ main_lines = physical_line_count(main)
30
+ if main_lines > MAIN_FILE_LINE_BUDGET:
31
+ violations.append(
32
+ f"ciel_runtime.py has {main_lines} lines; ratchet budget is {MAIN_FILE_LINE_BUDGET}"
33
+ )
34
+ for path in production_python_files(root):
35
+ if path == main:
36
+ continue
37
+ lines = physical_line_count(path)
38
+ if lines > FINAL_FILE_LINE_BUDGET:
39
+ violations.append(
40
+ f"{path.relative_to(root)} has {lines} lines; file budget is {FINAL_FILE_LINE_BUDGET}"
41
+ )
42
+ return tuple(violations)
@@ -0,0 +1,90 @@
1
+ """Application service for inspecting and discarding transient channel backlog."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Any, Callable
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class ChannelBacklogCursors:
10
+ scan_tail: Callable[[], int]
11
+ llm_lock: Any
12
+ read_llm: Callable[[], int]
13
+ write_llm: Callable[[int], None]
14
+ cache_llm: Callable[[int], None]
15
+ write_clear_floor: Callable[[int], None]
16
+ mcp_lock: Any
17
+ read_mcp: Callable[[], int]
18
+ write_mcp: Callable[[int], None]
19
+ cache_mcp: Callable[[int], None]
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class ChannelBacklogRuntime:
24
+ recovery_cache: dict[Any, Any]
25
+ session_lock: Any
26
+ sessions: dict[Any, dict[str, Any]]
27
+ condition: Any
28
+ log: Callable[[str, str], None]
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class ChannelBacklogService:
33
+ cursors: ChannelBacklogCursors
34
+ runtime: ChannelBacklogRuntime
35
+
36
+ def clear(self) -> dict[str, Any]:
37
+ tail = max(0, self.cursors.scan_tail())
38
+ with self.cursors.llm_lock:
39
+ old_llm = self.cursors.read_llm()
40
+ self.cursors.cache_llm(tail)
41
+ self._write_cursor("channel_llm_cursor", self.cursors.write_llm, tail)
42
+ self._write_cursor("channel_llm_clear_floor", self.cursors.write_clear_floor, tail)
43
+ self.runtime.recovery_cache.clear()
44
+ with self.cursors.mcp_lock:
45
+ old_mcp = self.cursors.read_mcp()
46
+ self.cursors.cache_mcp(tail)
47
+ self._write_cursor("channel_mcp_cursor", self.cursors.write_mcp, tail)
48
+ with self.runtime.session_lock:
49
+ for state in self.runtime.sessions.values():
50
+ try:
51
+ state["last_id"] = max(int(state.get("last_id") or 0), tail)
52
+ except (TypeError, ValueError, OverflowError):
53
+ state["last_id"] = tail
54
+ session_count = len(self.runtime.sessions)
55
+ with self.runtime.condition:
56
+ self.runtime.condition.notify_all()
57
+ stats = {
58
+ "chat_tail": tail,
59
+ "discarded_llm": max(0, tail - int(old_llm or 0)),
60
+ "discarded_mcp": max(0, tail - int(old_mcp or 0)),
61
+ "mcp_sessions_updated": session_count,
62
+ }
63
+ self.runtime.log(
64
+ "INFO",
65
+ f"channel_backlog_cleared chat_tail={tail} discarded_llm={stats['discarded_llm']} "
66
+ f"discarded_mcp={stats['discarded_mcp']} mcp_sessions_updated={session_count}",
67
+ )
68
+ return stats
69
+
70
+ def status(self) -> dict[str, Any]:
71
+ tail = max(0, self.cursors.scan_tail())
72
+ with self.cursors.llm_lock:
73
+ llm_cursor = self.cursors.read_llm()
74
+ with self.cursors.mcp_lock:
75
+ mcp_cursor = self.cursors.read_mcp()
76
+ return {
77
+ "chat_tail": tail,
78
+ "pending_llm": max(0, tail - int(llm_cursor or 0)),
79
+ "pending_mcp": max(0, tail - int(mcp_cursor or 0)),
80
+ "mcp_sessions": len(self.runtime.sessions),
81
+ }
82
+
83
+ def _write_cursor(self, name: str, writer: Callable[[int], None], tail: int) -> None:
84
+ try:
85
+ writer(tail)
86
+ except Exception as exc:
87
+ self.runtime.log("WARN", f"{name}_write_failed error={type(exc).__name__}: {exc}")
88
+
89
+
90
+ __all__ = ["ChannelBacklogCursors", "ChannelBacklogRuntime", "ChannelBacklogService"]
@@ -0,0 +1,119 @@
1
+ """CLI controller for Channel configuration and capability probing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable, Mapping
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class ChannelCliView:
11
+ load_config: Callable[[], dict[str, Any]]
12
+ status_text: Callable[[dict[str, Any]], str]
13
+ delivery_mode: Callable[[dict[str, Any] | None], str]
14
+ configured_specs: Callable[[dict[str, Any]], list[str]]
15
+ official_plugins: Mapping[str, str]
16
+ output: Callable[[str], None]
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class ChannelCliCommands:
21
+ add: Callable[[str], list[str]]
22
+ development: Callable[[bool], list[str]]
23
+ remove: Callable[[str], list[str]]
24
+ clear: Callable[[], list[str]]
25
+ refresh: Callable[[], dict[str, Any]]
26
+ report: Callable[[dict[str, Any]], list[str]]
27
+ set_delivery: Callable[[Any], list[str]]
28
+
29
+
30
+ class ChannelCliController:
31
+ def __init__(self, view: ChannelCliView, commands: ChannelCliCommands) -> None:
32
+ self.view = view
33
+ self.commands = commands
34
+
35
+ def run(self, args: Any) -> None:
36
+ config = self.view.load_config()
37
+ values = list(getattr(args, "values", []) or [])
38
+ if not values:
39
+ self._status(config)
40
+ return
41
+ head = values[0].strip().lower()
42
+ if head in ("on", "enable", "add"):
43
+ self._require_value(values, "Usage: ciel-runtime channels add CHANNEL_SPEC")
44
+ self._lines(self.commands.add(values[1]))
45
+ return
46
+ if head in ("dev", "development"):
47
+ if len(values) >= 2 and values[1].lower() in (
48
+ "on",
49
+ "off",
50
+ "true",
51
+ "false",
52
+ "1",
53
+ "0",
54
+ ):
55
+ self._lines(self.commands.development(True))
56
+ return
57
+ self._require_value(values, "Usage: ciel-runtime channels add CHANNEL_SPEC")
58
+ self._lines(self.commands.add(values[1]))
59
+ return
60
+ if head in ("off", "disable", "remove", "rm"):
61
+ self._require_value(values, "Usage: ciel-runtime channels remove CHANNEL_SPEC")
62
+ self._lines(self.commands.remove(values[1]))
63
+ return
64
+ if head in ("clear", "reset"):
65
+ self._lines(self.commands.clear())
66
+ return
67
+ if head in ("detect", "probe", "refresh"):
68
+ try:
69
+ result = self.commands.refresh()
70
+ except Exception as exc:
71
+ raise SystemExit(
72
+ f"Channel probe failed: {type(exc).__name__}: {exc}"
73
+ ) from exc
74
+ self._lines(self.commands.report(result))
75
+ return
76
+ if head in ("delivery", "mode"):
77
+ if len(values) < 2:
78
+ self.view.output(f"channel_delivery: {self.view.delivery_mode(config)}")
79
+ else:
80
+ self._lines(self.commands.set_delivery(values[1]))
81
+ return
82
+ if head in self.view.official_plugins:
83
+ spec = self.view.official_plugins[head]
84
+ command = (
85
+ self.commands.remove
86
+ if spec in self.view.configured_specs(config)
87
+ else self.commands.add
88
+ )
89
+ self._lines(command(spec))
90
+ return
91
+ self._lines(self.commands.add(values[0]))
92
+
93
+ def delivery(self, args: Any) -> None:
94
+ value = getattr(args, "value", None)
95
+ if value:
96
+ self._lines(self.commands.set_delivery(value))
97
+ else:
98
+ self.view.output(f"channel_delivery: {self.view.delivery_mode(None)}")
99
+
100
+ def _status(self, config: dict[str, Any]) -> None:
101
+ self.view.output(f"channels: {self.view.status_text(config)}")
102
+ self.view.output(f"delivery: {self.view.delivery_mode(config)}")
103
+ specs = self.view.configured_specs(config)
104
+ for name, spec in self.view.official_plugins.items():
105
+ mark = "*" if spec in specs else " "
106
+ self.view.output(f" {mark} {name:<10} {spec}")
107
+ official = set(self.view.official_plugins.values())
108
+ for spec in specs:
109
+ if spec not in official:
110
+ self.view.output(f" * custom {spec}")
111
+
112
+ @staticmethod
113
+ def _require_value(values: list[str], message: str) -> None:
114
+ if len(values) < 2:
115
+ raise SystemExit(message)
116
+
117
+ def _lines(self, lines: list[str]) -> None:
118
+ for line in lines:
119
+ self.view.output(line)
@@ -0,0 +1,82 @@
1
+ """Application service for injecting queued channel compaction commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class ChannelCompactRequestPorts:
12
+ read: Callable[[], dict[str, Any] | None]
13
+ clear: Callable[[str | None], None]
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class ChannelCompactRuntimePorts:
18
+ active_tool_call: Callable[[], bool]
19
+ active_turn: Callable[[], bool]
20
+ enter_bytes: Callable[[bytes | None], bytes]
21
+ write_prompt: Callable[..., None]
22
+ enter_label: Callable[[bytes], str]
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class ChannelCompactInjectionService:
27
+ request: ChannelCompactRequestPorts
28
+ runtime: ChannelCompactRuntimePorts
29
+ log: Callable[[str, str], None]
30
+
31
+ def inject(
32
+ self,
33
+ writer: Any,
34
+ enter_bytes: bytes | None = None,
35
+ *,
36
+ log_defer: bool = True,
37
+ submit_retry_count: int = 1,
38
+ confirm_submit: bool = False,
39
+ bracketed_paste: bool = False,
40
+ submit_delay_seconds: float | None = None,
41
+ ) -> str:
42
+ request = self.request.read()
43
+ if not request:
44
+ return "none"
45
+ request_id = str(request.get("id") or "")
46
+ if self.runtime.active_tool_call():
47
+ self._log_deferred(request_id, "active_tool_call", log_defer)
48
+ return "deferred"
49
+ if self.runtime.active_turn():
50
+ self._log_deferred(request_id, "active_turn", log_defer)
51
+ return "deferred"
52
+
53
+ command = str(request.get("command") or "/compact").strip() or "/compact"
54
+ if command != "/compact":
55
+ command = "/compact"
56
+ submit_bytes = self.runtime.enter_bytes(enter_bytes)
57
+ self.runtime.write_prompt(
58
+ writer,
59
+ command,
60
+ submit_bytes,
61
+ submit_retry_count=submit_retry_count,
62
+ confirm_submit=confirm_submit,
63
+ bracketed_paste=bracketed_paste,
64
+ submit_delay_seconds=submit_delay_seconds,
65
+ )
66
+ self.request.clear(request_id or None)
67
+ self.log(
68
+ "INFO",
69
+ f"channel_compact_request_injected id={request_id or '-'} "
70
+ f"enter={self.runtime.enter_label(submit_bytes)}",
71
+ )
72
+ return "injected"
73
+
74
+ def _log_deferred(
75
+ self, request_id: str, reason: str, enabled: bool
76
+ ) -> None:
77
+ if enabled:
78
+ self.log(
79
+ "INFO",
80
+ f"channel_compact_request_deferred id={request_id or '-'} "
81
+ f"reason={reason}",
82
+ )
@@ -0,0 +1,67 @@
1
+ """Polling policy for injecting queued session compaction requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class ChannelCompactPollState:
12
+ last_poll_at: float = 0.0
13
+ defer_logged_at: float = 0.0
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class ChannelCompactInjectionOptions:
18
+ submit_retry_count: int
19
+ confirm_submit: bool
20
+ bracketed_paste: bool
21
+ submit_delay_seconds: float | None
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class ChannelCompactPollPolicy:
26
+ poll_interval_seconds: float = 0.5
27
+ defer_log_interval_seconds: float = 30.0
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class ChannelCompactPollServices:
32
+ inject_pending: Callable[..., str]
33
+
34
+
35
+ def poll_pending_compaction(
36
+ now: float,
37
+ writer: Any,
38
+ enter_bytes: bytes,
39
+ inflight_message_id: int | None,
40
+ state: ChannelCompactPollState,
41
+ options: ChannelCompactInjectionOptions,
42
+ services: ChannelCompactPollServices,
43
+ policy: ChannelCompactPollPolicy = ChannelCompactPollPolicy(),
44
+ *,
45
+ input_ready: bool = True,
46
+ ) -> ChannelCompactPollState:
47
+ if not input_ready or now - state.last_poll_at < policy.poll_interval_seconds:
48
+ return state
49
+ if inflight_message_id is not None:
50
+ return ChannelCompactPollState(now, state.defer_logged_at)
51
+
52
+ log_defer = now - state.defer_logged_at >= policy.defer_log_interval_seconds
53
+ status = services.inject_pending(
54
+ writer,
55
+ enter_bytes,
56
+ log_defer=log_defer,
57
+ submit_retry_count=options.submit_retry_count,
58
+ confirm_submit=options.confirm_submit,
59
+ bracketed_paste=options.bracketed_paste,
60
+ submit_delay_seconds=options.submit_delay_seconds,
61
+ )
62
+ defer_logged_at = state.defer_logged_at
63
+ if status == "deferred" and log_defer:
64
+ defer_logged_at = now
65
+ elif status == "injected":
66
+ defer_logged_at = 0.0
67
+ return ChannelCompactPollState(now, defer_logged_at)
@@ -0,0 +1,113 @@
1
+ """Persistent single-slot repository for channel-triggered compact requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import time
8
+ import uuid
9
+ from collections.abc import Callable
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+
14
+ class ChannelCompactRequestRepository:
15
+ def __init__(
16
+ self,
17
+ path: Path,
18
+ lock: Any,
19
+ save: Callable[[dict[str, Any], str], None],
20
+ truncate: Callable[[str, int], str],
21
+ log: Callable[[str, str], None],
22
+ ttl: Callable[[], float],
23
+ ) -> None:
24
+ self.path = path
25
+ self.lock = lock
26
+ self.save = save
27
+ self.truncate = truncate
28
+ self.log = log
29
+ self.ttl = ttl
30
+
31
+ def payload(self, source: str, reason: str) -> dict[str, Any]:
32
+ now = time.time()
33
+ return {
34
+ "id": uuid.uuid4().hex,
35
+ "command": "/compact",
36
+ "source": str(source or "mcp"),
37
+ "reason": self.truncate(str(reason or ""), 1000),
38
+ "requested_at": now,
39
+ "expires_at": now + self.ttl(),
40
+ "pid": os.getpid(),
41
+ }
42
+
43
+ def queue(self, source: str = "mcp", reason: str = "") -> dict[str, Any]:
44
+ request = self.payload(source, reason)
45
+ with self.lock:
46
+ self.save(request, "channel_compact_request")
47
+ self.log(
48
+ "INFO",
49
+ f"channel_compact_request_queued id={request['id']} source={request['source']}",
50
+ )
51
+ return request
52
+
53
+ def read(self) -> dict[str, Any] | None:
54
+ with self.lock:
55
+ try:
56
+ data = json.loads(self.path.read_text(encoding="utf-8"))
57
+ except FileNotFoundError:
58
+ return None
59
+ except Exception as exc:
60
+ self.log(
61
+ "WARN",
62
+ f"channel_compact_request_read_failed error={type(exc).__name__}: {exc}",
63
+ )
64
+ return None
65
+ if not isinstance(data, dict):
66
+ return None
67
+ try:
68
+ expires_at = float(data.get("expires_at") or 0)
69
+ except Exception:
70
+ expires_at = 0.0
71
+ if expires_at and time.time() > expires_at:
72
+ self._unlink(str(data.get("id") or "-"))
73
+ self.log(
74
+ "INFO", f"channel_compact_request_expired id={data.get('id') or '-'}"
75
+ )
76
+ return None
77
+ return data
78
+
79
+ def clear(self, request_id: str | None = None) -> bool:
80
+ with self.lock:
81
+ if request_id:
82
+ try:
83
+ data = json.loads(self.path.read_text(encoding="utf-8"))
84
+ except FileNotFoundError:
85
+ return False
86
+ except Exception:
87
+ data = {}
88
+ if isinstance(data, dict) and str(data.get("id") or "") != str(request_id):
89
+ return False
90
+ return self._unlink(request_id or "-", missing=False)
91
+
92
+ def _unlink(self, request_id: str, *, missing: bool = True) -> bool:
93
+ try:
94
+ self.path.unlink()
95
+ return True
96
+ except FileNotFoundError:
97
+ return missing
98
+ except Exception as exc:
99
+ self.log(
100
+ "WARN",
101
+ f"channel_compact_request_clear_failed id={request_id} "
102
+ f"error={type(exc).__name__}: {exc}",
103
+ )
104
+ return False
105
+
106
+
107
+ def compact_request_ttl(value: str | None) -> float:
108
+ if value is None:
109
+ return 600.0
110
+ try:
111
+ return max(5.0, min(3600.0, float(str(value).strip())))
112
+ except (TypeError, ValueError):
113
+ return 600.0