@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,212 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import re
6
+ import time
7
+ from typing import Any
8
+
9
+
10
+ _EVENT_IDENTITY_META_KEYS = (
11
+ "stream_id",
12
+ "message_id",
13
+ "source_message_id",
14
+ "event_id",
15
+ "sse_id",
16
+ "cursor",
17
+ "sequence",
18
+ "seq",
19
+ )
20
+
21
+ _STABLE_META_KEYS = (
22
+ "cursor",
23
+ "stream_id",
24
+ "sse_id",
25
+ "event_id",
26
+ "message_id",
27
+ "source_message_id",
28
+ "sequence",
29
+ "seq",
30
+ "rpc_id",
31
+ )
32
+
33
+
34
+ def payload_hash(value: Any) -> str:
35
+ text = re.sub(r"\s+", " ", str(value or "")).strip()
36
+ return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()
37
+
38
+
39
+ def message_time_seconds(value: Any) -> float:
40
+ text = str(value or "").strip()
41
+ if not text:
42
+ return 0.0
43
+ try:
44
+ return time.mktime(time.strptime(text[:19], "%Y-%m-%dT%H:%M:%S"))
45
+ except (OverflowError, ValueError):
46
+ return 0.0
47
+
48
+
49
+ def _room_key(message: dict[str, Any], sources: list[dict[str, Any]]) -> str:
50
+ room_ids: list[str] = []
51
+ for source in sources:
52
+ rooms = source.get("rooms")
53
+ if not isinstance(rooms, str) or not rooms.strip():
54
+ continue
55
+ try:
56
+ parsed_rooms = json.loads(rooms)
57
+ except (json.JSONDecodeError, TypeError):
58
+ continue
59
+ if not isinstance(parsed_rooms, list):
60
+ continue
61
+ for room in parsed_rooms:
62
+ if not isinstance(room, dict):
63
+ continue
64
+ room_id = str(room.get("room_id") or room.get("room") or "").strip()
65
+ if room_id and room_id not in room_ids:
66
+ room_ids.append(room_id)
67
+ if room_ids:
68
+ return ",".join(sorted(room_ids))
69
+ for source in sources:
70
+ room = str(source.get("room_id") or source.get("room") or source.get("channel") or "").strip()
71
+ if room:
72
+ return room
73
+ return str(message.get("channel") or "").strip()
74
+
75
+
76
+ def message_event_identity_key(message: dict[str, Any]) -> tuple[str, ...] | None:
77
+ meta = message.get("meta") if isinstance(message.get("meta"), dict) else {}
78
+ envelopes = [meta[key] for key in ("sse_json", "mcp_json") if isinstance(meta.get(key), dict)]
79
+ raw_message = message.get("message")
80
+ if isinstance(raw_message, str) and raw_message.strip().startswith("{"):
81
+ try:
82
+ parsed = json.loads(raw_message.strip())
83
+ except (json.JSONDecodeError, TypeError):
84
+ parsed = None
85
+ if isinstance(parsed, dict):
86
+ envelopes.append(parsed)
87
+
88
+ sources: list[dict[str, Any]] = [meta]
89
+ method = str(meta.get("mcp_method") or meta.get("sse_event") or "").strip()
90
+ content: Any = message.get("message")
91
+ for envelope in envelopes:
92
+ envelope_method = str(envelope.get("method") or "").strip()
93
+ if envelope_method:
94
+ method = envelope_method
95
+ params = envelope.get("params") if isinstance(envelope.get("params"), dict) else {}
96
+ if not params:
97
+ continue
98
+ sources.append(params)
99
+ params_meta = params.get("meta")
100
+ if isinstance(params_meta, dict):
101
+ sources.append(params_meta)
102
+ if params.get("content") is not None:
103
+ content = params.get("content")
104
+ if method and not method.startswith("notifications/"):
105
+ return None
106
+
107
+ kind = ""
108
+ for source in sources:
109
+ if not kind:
110
+ kind = str(
111
+ source.get("kind")
112
+ or source.get("type")
113
+ or source.get("event_type")
114
+ or source.get("eventType")
115
+ or ""
116
+ ).strip()
117
+ room = _room_key(message, sources)
118
+ for key in _EVENT_IDENTITY_META_KEYS:
119
+ for source in sources:
120
+ value = source.get(key)
121
+ stable_value = str(value).strip() if value is not None else ""
122
+ if stable_value:
123
+ return ("event", method, room, kind, key, stable_value, payload_hash(content))
124
+ return None
125
+
126
+
127
+ def stable_dedupe_key(message: dict[str, Any]) -> tuple[str, ...] | None:
128
+ event_identity = message_event_identity_key(message)
129
+ if event_identity:
130
+ return ("stable",) + event_identity
131
+ meta = message.get("meta") if isinstance(message.get("meta"), dict) else {}
132
+ source = str(
133
+ meta.get("mcp_server")
134
+ or meta.get("sse_source")
135
+ or meta.get("source")
136
+ or message.get("sender_id")
137
+ or ""
138
+ ).strip()
139
+ method = str(meta.get("mcp_method") or "").strip()
140
+ channel = str(
141
+ message.get("channel")
142
+ or meta.get("room_id")
143
+ or meta.get("room")
144
+ or meta.get("channel")
145
+ or ""
146
+ ).strip()
147
+ kind = str(
148
+ message.get("kind")
149
+ or meta.get("kind")
150
+ or meta.get("type")
151
+ or meta.get("event_type")
152
+ or meta.get("eventType")
153
+ or ""
154
+ ).strip()
155
+ body_hash = payload_hash(message.get("message"))
156
+ for key in _STABLE_META_KEYS:
157
+ value = meta.get(key)
158
+ stable_value = str(value).strip() if value is not None else ""
159
+ if stable_value:
160
+ return ("stable", source, method, channel, kind, key, stable_value, body_hash)
161
+ mcp_json = meta.get("mcp_json")
162
+ if method.startswith("notifications/") and isinstance(mcp_json, dict):
163
+ try:
164
+ normalized = json.dumps(
165
+ mcp_json,
166
+ ensure_ascii=False,
167
+ sort_keys=True,
168
+ separators=(",", ":"),
169
+ default=str,
170
+ )
171
+ except (TypeError, ValueError, OverflowError):
172
+ normalized = ""
173
+ if normalized:
174
+ digest = hashlib.sha256(normalized.encode("utf-8", errors="replace")).hexdigest()
175
+ return ("stable", source, method, channel, kind, "mcp_json", digest, body_hash)
176
+ return None
177
+
178
+
179
+ def fallback_dedupe_key(message: dict[str, Any]) -> tuple[str, ...] | None:
180
+ meta = message.get("meta") if isinstance(message.get("meta"), dict) else {}
181
+ method = str(meta.get("mcp_method") or "").strip()
182
+ if not method.startswith("notifications/"):
183
+ return None
184
+ body = re.sub(r"\s+", " ", str(message.get("message") or "")).strip()
185
+ if not body:
186
+ return None
187
+ source = str(
188
+ meta.get("mcp_server")
189
+ or meta.get("sse_source")
190
+ or meta.get("source")
191
+ or message.get("sender_id")
192
+ or ""
193
+ ).strip()
194
+ channel = str(
195
+ message.get("channel")
196
+ or meta.get("room_id")
197
+ or meta.get("room")
198
+ or meta.get("channel")
199
+ or ""
200
+ ).strip()
201
+ sender = str(
202
+ message.get("sender_id") or meta.get("sender_id") or meta.get("agent_id") or ""
203
+ ).strip()
204
+ kind = str(
205
+ message.get("kind")
206
+ or meta.get("kind")
207
+ or meta.get("type")
208
+ or meta.get("event_type")
209
+ or meta.get("eventType")
210
+ or ""
211
+ ).strip()
212
+ return ("fallback", source, method, channel, sender, kind, payload_hash(body))
@@ -0,0 +1,315 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from typing import Any
6
+
7
+ CHANNEL_CONTROL_KINDS = {
8
+ "agent_presence",
9
+ "check-in",
10
+ "checkin",
11
+ "checkins",
12
+ "checked_in",
13
+ "colleague_presence",
14
+ "connection",
15
+ "connected",
16
+ "disconnect",
17
+ "disconnected",
18
+ "endpoint",
19
+ "heartbeat",
20
+ "initialized",
21
+ "init",
22
+ "keepalive",
23
+ "ping",
24
+ "pong",
25
+ "presence",
26
+ "ready",
27
+ "status",
28
+ "system",
29
+ "user_presence",
30
+ }
31
+
32
+
33
+ def first_present_dict_value(*sources: Any, keys: tuple[str, ...]) -> Any:
34
+ for source in sources:
35
+ if not isinstance(source, dict):
36
+ continue
37
+ for key in keys:
38
+ value = source.get(key)
39
+ if value is not None:
40
+ return value
41
+ return None
42
+
43
+
44
+ def event_payload_text(value: Any, depth: int = 0) -> str | None:
45
+ if value is None or depth > 5:
46
+ return None
47
+ if isinstance(value, str):
48
+ return value.strip() or None
49
+ if not isinstance(value, dict):
50
+ return str(value)
51
+ direct = first_present_dict_value(value, keys=("content", "message", "text", "body", "summary"))
52
+ if isinstance(direct, str) and direct.strip():
53
+ return direct.strip()
54
+ if isinstance(direct, dict):
55
+ nested = event_payload_text(direct, depth + 1)
56
+ if nested:
57
+ return nested
58
+ for key in ("data", "event", "payload", "message", "notification", "item"):
59
+ nested = event_payload_text(value.get(key), depth + 1)
60
+ if nested:
61
+ return nested
62
+ event_type = value.get("type") or value.get("event_type") or value.get("kind")
63
+ if event_type:
64
+ payload = value.get("payload") if isinstance(value.get("payload"), dict) else value.get("data")
65
+ if payload is not None:
66
+ return f"{event_type}: {json.dumps(payload, ensure_ascii=False, separators=(',', ':'))}"
67
+ return str(event_type)
68
+ return None
69
+
70
+
71
+ def pretty_json_text_or_raw(text: str) -> str:
72
+ try:
73
+ parsed = json.loads(text)
74
+ except Exception:
75
+ return text
76
+ return json.dumps(parsed, ensure_ascii=False, indent=2, default=str)
77
+
78
+
79
+ def pretty_json_value(value: Any) -> str:
80
+ return json.dumps(value, ensure_ascii=False, indent=2, default=str)
81
+
82
+
83
+ def notification_semantic_text_from_envelope(envelope: Any) -> str | None:
84
+ if not isinstance(envelope, dict):
85
+ return None
86
+ params = envelope.get("params") if isinstance(envelope.get("params"), dict) else {}
87
+ payload = envelope.get("payload") if isinstance(envelope.get("payload"), dict) else {}
88
+ data = params.get("data") if isinstance(params.get("data"), dict) else {}
89
+ event = params.get("event") if isinstance(params.get("event"), dict) else {}
90
+ nested_payload = (
91
+ data.get("payload")
92
+ if isinstance(data.get("payload"), dict)
93
+ else event.get("payload")
94
+ if isinstance(event.get("payload"), dict)
95
+ else {}
96
+ )
97
+ content = event_payload_text(nested_payload) or event_payload_text(payload)
98
+ for source in (data, event):
99
+ direct = first_present_dict_value(source, keys=("content", "message", "text", "body", "summary"))
100
+ content = content or event_payload_text(direct)
101
+ return content or event_payload_text(params) or event_payload_text(envelope)
102
+
103
+
104
+ def event_meta_from_sources(*sources: Any) -> dict[str, Any]:
105
+ meta: dict[str, Any] = {}
106
+ keys = (
107
+ "room_id",
108
+ "room",
109
+ "channel",
110
+ "thread_id",
111
+ "parent_id",
112
+ "message_id",
113
+ "source_message_id",
114
+ "event_id",
115
+ "stream_id",
116
+ "sse_id",
117
+ "cursor",
118
+ "assignment_id",
119
+ "poll_id",
120
+ "task_id",
121
+ "sequence",
122
+ "seq",
123
+ "round_id",
124
+ "conversation_id",
125
+ "session_id",
126
+ "agent_id",
127
+ "agent_name",
128
+ "sender_id",
129
+ "sender",
130
+ "recipient_id",
131
+ "recipient",
132
+ "recipients",
133
+ "target_id",
134
+ "target",
135
+ "type",
136
+ "event_type",
137
+ "eventType",
138
+ "kind",
139
+ "timestamp",
140
+ "created_at",
141
+ "updated_at",
142
+ "status",
143
+ "priority",
144
+ "title",
145
+ "name",
146
+ "model",
147
+ "runtime",
148
+ )
149
+ for source in sources:
150
+ if not isinstance(source, dict):
151
+ continue
152
+ source_meta = source.get("meta")
153
+ if isinstance(source_meta, dict):
154
+ meta.update(source_meta)
155
+ for key in keys:
156
+ value = source.get(key)
157
+ if value is not None and key not in meta:
158
+ meta[key] = value
159
+ return meta
160
+
161
+
162
+ def metadata_key_is_sensitive(key: str) -> bool:
163
+ return bool(re.search(r"(authorization|api[_-]?key|token|secret|password|credential|cookie)", key, re.I))
164
+
165
+
166
+ def json_safe_metadata(value: Any, depth: int = 0) -> Any:
167
+ if depth > 8:
168
+ return "<max-depth>"
169
+ if value is None or isinstance(value, (bool, int, float)):
170
+ return value
171
+ if isinstance(value, str):
172
+ return value if len(value) <= 8000 else value[:8000] + "...<truncated>"
173
+ if isinstance(value, list):
174
+ out = [json_safe_metadata(item, depth + 1) for item in value[:200]]
175
+ if len(value) > 200:
176
+ out.append(f"...<{len(value) - 200} more>")
177
+ return out
178
+ if isinstance(value, dict):
179
+ out: dict[str, Any] = {}
180
+ items = list(value.items())
181
+ for key, item in items[:200]:
182
+ safe_key = str(key)
183
+ out[safe_key] = "[redacted]" if metadata_key_is_sensitive(safe_key) else json_safe_metadata(item, depth + 1)
184
+ if len(items) > 200:
185
+ out["..."] = f"<{len(items) - 200} more>"
186
+ return out
187
+ return str(value)
188
+
189
+
190
+ def compact_json_for_prompt(value: Any, max_chars: int = 2400) -> str:
191
+ try:
192
+ text = json.dumps(value, ensure_ascii=False, separators=(",", ":"), default=str)
193
+ except Exception:
194
+ text = str(value)
195
+ if len(text) <= max_chars:
196
+ return text
197
+ return text[: max_chars - 16] + "...<truncated>"
198
+
199
+
200
+ def channel_event_is_user_visible(
201
+ kind: str,
202
+ method: str,
203
+ event_name: str,
204
+ content: str,
205
+ meta: dict[str, Any],
206
+ ) -> bool:
207
+ normalized_kind = str(kind or "").strip().lower().replace("_", ".").replace("/", ".")
208
+ normalized_method = str(method or "").strip().lower()
209
+ normalized_event = str(event_name or "").strip().lower()
210
+ meta_type = str(
211
+ meta.get("type")
212
+ or meta.get("event_type")
213
+ or meta.get("eventType")
214
+ or meta.get("kind")
215
+ or meta.get("status")
216
+ or ""
217
+ ).strip().lower()
218
+ if normalized_kind in CHANNEL_CONTROL_KINDS or normalized_event in CHANNEL_CONTROL_KINDS:
219
+ return False
220
+ if meta_type in CHANNEL_CONTROL_KINDS:
221
+ return False
222
+ if meta.get("jsonrpc") is not None:
223
+ if normalized_method.startswith("notifications/claude/"):
224
+ return True
225
+ if normalized_method in {"notifications/message", "notifications/chat", "notifications/event"}:
226
+ return True
227
+ return False
228
+ return bool(content.strip())
229
+
230
+
231
+ def sse_payload_to_chat_payload(
232
+ data_text: str,
233
+ event_name: str,
234
+ defaults: dict[str, Any],
235
+ event_id: str | None = None,
236
+ ) -> dict[str, Any] | None:
237
+ raw_text = data_text or ""
238
+ stripped_text = raw_text.strip()
239
+ if not stripped_text or stripped_text == "[DONE]":
240
+ return None
241
+ if (event_name or "").strip().lower() == "endpoint":
242
+ return None
243
+ try:
244
+ parsed: Any = json.loads(stripped_text)
245
+ except Exception:
246
+ parsed = None
247
+ meta: dict[str, Any] = {
248
+ "sse_event": event_name or "message",
249
+ "sse_source": defaults.get("name") or "",
250
+ }
251
+ if event_id:
252
+ meta["sse_id"] = event_id
253
+ content = raw_text
254
+ kind = "sse"
255
+ method = str(event_name or "message")
256
+ event_filter = defaults.get("event_filter")
257
+ allowed_events = (
258
+ {str(item).strip() for item in event_filter if str(item).strip()}
259
+ if isinstance(event_filter, list)
260
+ else set()
261
+ )
262
+ if isinstance(parsed, dict):
263
+ method = str(parsed.get("method") or event_name or "message")
264
+ meta["sse_json"] = json_safe_metadata(parsed)
265
+ if parsed.get("jsonrpc") is not None:
266
+ meta["jsonrpc"] = parsed.get("jsonrpc")
267
+ if parsed.get("id") is not None:
268
+ meta["rpc_id"] = parsed.get("id")
269
+ if parsed.get("method") is not None:
270
+ meta["mcp_method"] = parsed.get("method")
271
+ params = parsed.get("params") if isinstance(parsed.get("params"), dict) else {}
272
+ payload = parsed.get("payload") if isinstance(parsed.get("payload"), dict) else {}
273
+ data = params.get("data") if isinstance(params.get("data"), dict) else {}
274
+ event = params.get("event") if isinstance(params.get("event"), dict) else {}
275
+ nested_payload = (
276
+ data.get("payload")
277
+ if isinstance(data.get("payload"), dict)
278
+ else event.get("payload")
279
+ if isinstance(event.get("payload"), dict)
280
+ else {}
281
+ )
282
+ meta.update(event_meta_from_sources(parsed, params, payload, data, event, nested_payload))
283
+ nested_content = event_payload_text(nested_payload) or event_payload_text(payload)
284
+ for source in (data, event):
285
+ direct = first_present_dict_value(
286
+ source,
287
+ keys=("content", "message", "text", "body", "summary"),
288
+ )
289
+ nested_content = nested_content or event_payload_text(direct)
290
+ content = nested_content or event_payload_text(params) or event_payload_text(parsed) or content
291
+ kind = method.replace("notifications/claude/", "").replace("/", ".") if method else "sse"
292
+ if not parsed.get("method") and meta.get("kind"):
293
+ kind = str(meta.get("kind"))
294
+ if allowed_events and method not in allowed_events and (event_name or "message") not in allowed_events:
295
+ return None
296
+ if not channel_event_is_user_visible(kind, method, event_name, content, meta):
297
+ return None
298
+ channel = meta.get("channel") or meta.get("room_id") or meta.get("room") or defaults.get("channel") or "default"
299
+ return {
300
+ "channel": channel,
301
+ "sender_id": meta.get("sender_id") or meta.get("sender") or meta.get("agent_id") or defaults.get("sender_id") or "sse",
302
+ "recipients": meta.get("recipients")
303
+ or meta.get("recipient_id")
304
+ or meta.get("recipient")
305
+ or defaults.get("recipient")
306
+ or defaults.get("recipients")
307
+ or "all",
308
+ "thread_id": meta.get("thread_id"),
309
+ "parent_id": meta.get("parent_id"),
310
+ "kind": kind,
311
+ "message": pretty_json_text_or_raw(raw_text),
312
+ "meta": meta,
313
+ "visibility": "user",
314
+ "delivery": ["llm"],
315
+ }
@@ -0,0 +1,127 @@
1
+ """State machine for channel wake prompts awaiting turn completion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Callable, Literal
7
+
8
+
9
+ InflightAction = Literal["none", "completed", "unseen_retry", "stale", "waiting"]
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class ChannelInflightSnapshot:
14
+ message_id: int
15
+ cursor: int | None
16
+ wake_state: str
17
+ started_at: float
18
+ logged_at: float
19
+ now: float
20
+ last_id: int
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class ChannelInflightPolicy:
25
+ unseen_retry_seconds: float
26
+ waiting_log_interval: float
27
+ is_stale: Callable[[str, float, float], bool]
28
+ commit_cursor_on_stale: bool
29
+ log_namespace: str
30
+ stale_event: str
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class ChannelInflightEffects:
35
+ commit_cursor: Callable[[int], Any]
36
+ complete_wake: Callable[[int], Any]
37
+ release_wake: Callable[[int], Any]
38
+ ensure_cursor: Callable[[], int]
39
+ log: Callable[[str, str], Any]
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class ChannelInflightUpdate:
44
+ action: InflightAction
45
+ message_id: int | None
46
+ cursor: int | None
47
+ started_at: float
48
+ logged_at: float
49
+ pending_recheck: bool
50
+ last_id: int
51
+
52
+
53
+ def advance_channel_inflight(
54
+ snapshot: ChannelInflightSnapshot,
55
+ policy: ChannelInflightPolicy,
56
+ effects: ChannelInflightEffects,
57
+ ) -> ChannelInflightUpdate:
58
+ message_id = snapshot.message_id
59
+ state = snapshot.wake_state
60
+ age = snapshot.now - snapshot.started_at
61
+ if state == "completed":
62
+ if snapshot.cursor is not None:
63
+ effects.commit_cursor(snapshot.cursor)
64
+ effects.complete_wake(message_id)
65
+ effects.log(
66
+ "INFO",
67
+ f"{policy.log_namespace}_confirmed message_id={message_id} cursor={snapshot.cursor or '-'}",
68
+ )
69
+ return _reset("completed", snapshot)
70
+ if state == "missing" and snapshot.started_at > 0 and age >= policy.unseen_retry_seconds:
71
+ effects.release_wake(message_id)
72
+ effects.log(
73
+ "WARN",
74
+ f"{policy.log_namespace}_unseen_retry message_id={message_id} age={age:.1f}s",
75
+ )
76
+ return _reset("unseen_retry", snapshot, last_id=effects.ensure_cursor(), logged_at=snapshot.now)
77
+ if policy.is_stale(state, snapshot.started_at, snapshot.now):
78
+ if policy.commit_cursor_on_stale and snapshot.cursor is not None:
79
+ effects.commit_cursor(snapshot.cursor)
80
+ effects.release_wake(message_id)
81
+ effects.log(
82
+ "WARN",
83
+ f"{policy.log_namespace}_{policy.stale_event} message_id={message_id} state={state} "
84
+ f"age={age:.1f}s cursor={snapshot.cursor or '-'}",
85
+ )
86
+ return _reset("stale", snapshot, last_id=effects.ensure_cursor(), logged_at=snapshot.now)
87
+ if snapshot.now - snapshot.logged_at >= policy.waiting_log_interval:
88
+ effects.log(
89
+ "INFO",
90
+ f"{policy.log_namespace}_waiting_for_turn_completion message_id={message_id} state={state}",
91
+ )
92
+ return ChannelInflightUpdate(
93
+ action="waiting",
94
+ message_id=message_id,
95
+ cursor=snapshot.cursor,
96
+ started_at=snapshot.started_at,
97
+ logged_at=snapshot.now,
98
+ pending_recheck=False,
99
+ last_id=snapshot.last_id,
100
+ )
101
+ return ChannelInflightUpdate(
102
+ action="none",
103
+ message_id=message_id,
104
+ cursor=snapshot.cursor,
105
+ started_at=snapshot.started_at,
106
+ logged_at=snapshot.logged_at,
107
+ pending_recheck=False,
108
+ last_id=snapshot.last_id,
109
+ )
110
+
111
+
112
+ def _reset(
113
+ action: InflightAction,
114
+ snapshot: ChannelInflightSnapshot,
115
+ *,
116
+ last_id: int | None = None,
117
+ logged_at: float | None = None,
118
+ ) -> ChannelInflightUpdate:
119
+ return ChannelInflightUpdate(
120
+ action=action,
121
+ message_id=None,
122
+ cursor=None,
123
+ started_at=0.0,
124
+ logged_at=snapshot.logged_at if logged_at is None else logged_at,
125
+ pending_recheck=True,
126
+ last_id=snapshot.last_id if last_id is None else last_id,
127
+ )