@oneciel-ai/ciel-runtime 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ciel_runtime.py +2555 -9635
- package/ciel_runtime_support/advisor_request_builder.py +8 -21
- package/ciel_runtime_support/anthropic_tool_turns.py +13 -8
- package/ciel_runtime_support/architecture.py +68 -0
- package/ciel_runtime_support/architecture_budget.py +1 -1
- package/ciel_runtime_support/channel_connection_context.py +233 -0
- package/ciel_runtime_support/channel_delivery_context.py +332 -0
- package/ciel_runtime_support/channel_mcp_context.py +313 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +47 -0
- package/ciel_runtime_support/channel_mcp_transport.py +5 -1
- package/ciel_runtime_support/channel_message_context.py +212 -0
- package/ciel_runtime_support/channel_message_repository.py +14 -3
- package/ciel_runtime_support/channel_pending_injection.py +9 -0
- package/ciel_runtime_support/channel_probe_launch_context.py +213 -0
- package/ciel_runtime_support/channel_replay_policy.py +38 -0
- package/ciel_runtime_support/channel_runtime_environment.py +8 -0
- package/ciel_runtime_support/channel_session_context.py +236 -0
- package/ciel_runtime_support/channel_terminal_context.py +350 -0
- package/ciel_runtime_support/channel_wake_context.py +532 -0
- package/ciel_runtime_support/claude_environment.py +60 -0
- package/ciel_runtime_support/claude_launch_assembly.py +249 -0
- package/ciel_runtime_support/claude_router.py +62 -12
- package/ciel_runtime_support/cli_application_context.py +132 -0
- package/ciel_runtime_support/cli_assembly.py +50 -0
- package/ciel_runtime_support/codex_backend_context.py +363 -0
- package/ciel_runtime_support/codex_config.py +13 -1
- package/ciel_runtime_support/codex_launch_assembly.py +213 -0
- package/ciel_runtime_support/codex_launch_configuration.py +30 -1
- package/ciel_runtime_support/codex_mcp_integration.py +90 -8
- package/ciel_runtime_support/codex_model_catalog.py +4 -1
- package/ciel_runtime_support/codex_reasoning_rejects.py +225 -0
- package/ciel_runtime_support/codex_router.py +38 -8
- package/ciel_runtime_support/codex_turn_recovery.py +154 -0
- package/ciel_runtime_support/config_migrations.py +103 -0
- package/ciel_runtime_support/config_repository.py +30 -0
- package/ciel_runtime_support/configuration_cli.py +38 -0
- package/ciel_runtime_support/context_compaction.py +9 -4
- package/ciel_runtime_support/credential_management.py +12 -0
- package/ciel_runtime_support/credentials.py +12 -0
- package/ciel_runtime_support/github_copilot_oauth.py +2 -2
- package/ciel_runtime_support/hosted_formula_tools.py +216 -0
- package/ciel_runtime_support/kimi_runtime_context.py +208 -0
- package/ciel_runtime_support/llm_preset_context.py +338 -0
- package/ciel_runtime_support/managed_mcp_config.py +8 -4
- package/ciel_runtime_support/mcp_configuration_context.py +291 -0
- package/ciel_runtime_support/mcp_http_proxy.py +14 -8
- package/ciel_runtime_support/mcp_probe_transport.py +47 -15
- package/ciel_runtime_support/mcp_transport.py +14 -1
- package/ciel_runtime_support/native_context_recovery.py +72 -0
- package/ciel_runtime_support/ollama_catalog_context.py +213 -0
- package/ciel_runtime_support/ollama_stream_collection.py +103 -0
- package/ciel_runtime_support/ollama_thinking.py +6 -1
- package/ciel_runtime_support/ollama_wire_projection.py +157 -0
- package/ciel_runtime_support/openai_forwarding.py +32 -10
- package/ciel_runtime_support/openai_responses_router.py +12 -0
- package/ciel_runtime_support/package_lifecycle.py +39 -0
- package/ciel_runtime_support/prelaunch_assembly.py +37 -0
- package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
- package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
- package/ciel_runtime_support/prompt_compaction.py +144 -0
- package/ciel_runtime_support/prompt_injection.py +45 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
- package/ciel_runtime_support/protocols/chat_projection.py +85 -5
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
- package/ciel_runtime_support/protocols/ollama_response.py +57 -5
- package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
- package/ciel_runtime_support/protocols/openai_responses.py +61 -15
- package/ciel_runtime_support/provider_adapters.py +26 -0
- package/ciel_runtime_support/provider_administration_context.py +207 -0
- package/ciel_runtime_support/provider_config_mutations.py +3 -0
- package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
- package/ciel_runtime_support/provider_model_context.py +107 -0
- package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
- package/ciel_runtime_support/provider_model_selection.py +10 -3
- package/ciel_runtime_support/provider_models.py +45 -2
- package/ciel_runtime_support/provider_option_cli.py +19 -0
- package/ciel_runtime_support/provider_policy.py +1 -1
- package/ciel_runtime_support/provider_readiness_context.py +189 -0
- package/ciel_runtime_support/provider_request_builder.py +64 -28
- package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
- package/ciel_runtime_support/provider_timeout_policy.py +54 -0
- package/ciel_runtime_support/provider_tool_policy.py +9 -1
- package/ciel_runtime_support/providers/__init__.py +6 -0
- package/ciel_runtime_support/providers/alibaba.py +634 -0
- package/ciel_runtime_support/providers/catalog.py +24 -16
- package/ciel_runtime_support/providers/deepseek.py +73 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
- package/ciel_runtime_support/providers/kimi.py +69 -9
- package/ciel_runtime_support/providers/ollama.py +8 -0
- package/ciel_runtime_support/providers/ollama_context.py +21 -2
- package/ciel_runtime_support/providers/vllm.py +7 -1
- package/ciel_runtime_support/response_collection.py +68 -18
- package/ciel_runtime_support/response_collection_context.py +391 -0
- package/ciel_runtime_support/response_stream_context.py +555 -0
- package/ciel_runtime_support/responses_input_compatibility.py +121 -0
- package/ciel_runtime_support/responses_usage_observer.py +83 -0
- package/ciel_runtime_support/router_client_lifecycle.py +1 -0
- package/ciel_runtime_support/router_http.py +245 -3
- package/ciel_runtime_support/router_observability_context.py +251 -0
- package/ciel_runtime_support/router_process_context.py +200 -0
- package/ciel_runtime_support/router_process_lifecycle.py +2 -0
- package/ciel_runtime_support/router_request_assembly.py +399 -0
- package/ciel_runtime_support/router_request_context.py +215 -0
- package/ciel_runtime_support/router_server_context.py +84 -0
- package/ciel_runtime_support/runaway_output_guard.py +488 -0
- package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
- package/ciel_runtime_support/runtime_asset_context.py +297 -0
- package/ciel_runtime_support/runtime_constants.py +16 -1
- package/ciel_runtime_support/runtime_launch.py +9 -5
- package/ciel_runtime_support/runtime_launch_context.py +130 -0
- package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
- package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
- package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
- package/ciel_runtime_support/runtime_paths.py +60 -40
- package/ciel_runtime_support/runtime_primitives.py +78 -0
- package/ciel_runtime_support/speech_http_controller.py +335 -0
- package/ciel_runtime_support/sse_stream_collection.py +236 -0
- package/ciel_runtime_support/statusline_script.py +57 -8
- package/ciel_runtime_support/streaming_anthropic.py +361 -24
- package/ciel_runtime_support/tool_schema.py +40 -2
- package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
- package/ciel_runtime_support/upstream_dump.py +68 -0
- package/ciel_runtime_support/upstream_retry_context.py +259 -0
- package/ciel_runtime_support/web_ui.py +248 -1
- package/ciel_runtime_support/workspace_router_selection.py +86 -0
- package/docs/COLAB_SPEECH.md +32 -0
- package/docs/Configuration.md +50 -0
- package/docs/Test-Suite.md +1 -0
- package/package.json +4 -1
- package/scripts/colab/__pycache__/bootstrap_moss_tts.cpython-311.pyc +0 -0
- package/scripts/colab/__pycache__/bootstrap_qwen_asr.cpython-311.pyc +0 -0
- package/scripts/colab/bootstrap_moss_tts.py +106 -0
- package/scripts/colab/bootstrap_qwen_asr.py +106 -0
- package/scripts/configure_speech_workers.py +37 -0
- package/scripts/deploy_colab_speech.ps1 +47 -0
|
@@ -7,6 +7,8 @@ import urllib.error
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from typing import Any, Callable
|
|
9
9
|
|
|
10
|
+
from .mcp_transport import negotiated_protocol_version
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
@dataclass(frozen=True, slots=True)
|
|
12
14
|
class ChannelMcpTransportConfig:
|
|
@@ -262,9 +264,10 @@ class ChannelMcpTransport:
|
|
|
262
264
|
},
|
|
263
265
|
},
|
|
264
266
|
}
|
|
265
|
-
|
|
267
|
+
result, session_id = self.http.streamable_post(
|
|
266
268
|
endpoint, headers, initialize, timeout, protocol_version
|
|
267
269
|
)
|
|
270
|
+
protocol_version = negotiated_protocol_version(result, protocol_version)
|
|
268
271
|
if requires_session and not session_id:
|
|
269
272
|
self.effects.set_state(
|
|
270
273
|
name,
|
|
@@ -291,6 +294,7 @@ class ChannelMcpTransport:
|
|
|
291
294
|
mcp_endpoint=endpoint,
|
|
292
295
|
mcp_initialized=True,
|
|
293
296
|
mcp_session_id=session_id,
|
|
297
|
+
mcp_protocol_version=protocol_version,
|
|
294
298
|
mcp_last_error=None,
|
|
295
299
|
mcp_rpc_results={},
|
|
296
300
|
)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Durable channel message queue and launch-dedupe bounded context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from .channel_launch_guard_repository import ChannelLaunchGuardRepository
|
|
10
|
+
from .channel_message_dedupe import (
|
|
11
|
+
ChannelMessageDedupePorts,
|
|
12
|
+
ChannelMessageDedupeService,
|
|
13
|
+
)
|
|
14
|
+
from .channel_message_repository import (
|
|
15
|
+
ChannelMessageAppendPorts,
|
|
16
|
+
ChannelMessageRepository,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class ChannelMessageStoragePorts:
|
|
22
|
+
path: Callable[[], Path]
|
|
23
|
+
max_bytes: int
|
|
24
|
+
condition: Any
|
|
25
|
+
file_lock: Callable[[Path], Any]
|
|
26
|
+
log: Callable[[str, str], None]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class ChannelMessageIdentityPorts:
|
|
31
|
+
stable_key: Callable[[dict[str, Any]], Any]
|
|
32
|
+
fallback_key: Callable[[dict[str, Any]], Any]
|
|
33
|
+
timestamp_seconds: Callable[[Any], float]
|
|
34
|
+
normalize_recipients: Callable[[Any], list[str]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class ChannelMessageLaunchPorts:
|
|
39
|
+
guard_path: Callable[[], Path]
|
|
40
|
+
now: Callable[[], float]
|
|
41
|
+
recent_scan_limit: int
|
|
42
|
+
fallback_ttl_seconds: float
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class ChannelMessageCachePorts:
|
|
47
|
+
read_next_id: Callable[[], int | None]
|
|
48
|
+
write_next_id: Callable[[int], None]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class ChannelMessageContext:
|
|
53
|
+
storage: ChannelMessageStoragePorts
|
|
54
|
+
identity: ChannelMessageIdentityPorts
|
|
55
|
+
launch: ChannelMessageLaunchPorts
|
|
56
|
+
cache: ChannelMessageCachePorts
|
|
57
|
+
|
|
58
|
+
def repository(self) -> ChannelMessageRepository:
|
|
59
|
+
return ChannelMessageRepository(
|
|
60
|
+
path=self.storage.path(),
|
|
61
|
+
log=self.storage.log,
|
|
62
|
+
max_bytes=self.storage.max_bytes,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def max_id(self) -> int:
|
|
66
|
+
return self.repository().max_id()
|
|
67
|
+
|
|
68
|
+
def max_id_before_epoch(self, cutoff_epoch: float) -> int:
|
|
69
|
+
return self.repository().max_id_before_epoch(cutoff_epoch)
|
|
70
|
+
|
|
71
|
+
def file_lock(self) -> Any:
|
|
72
|
+
return self.storage.file_lock(self.storage.path())
|
|
73
|
+
|
|
74
|
+
def read(
|
|
75
|
+
self,
|
|
76
|
+
after_id: int = 0,
|
|
77
|
+
channel: str | None = None,
|
|
78
|
+
recipient: str | None = None,
|
|
79
|
+
limit: int = 100,
|
|
80
|
+
) -> list[dict[str, Any]]:
|
|
81
|
+
return self.repository().read(after_id, channel, recipient, limit)
|
|
82
|
+
|
|
83
|
+
def read_before(
|
|
84
|
+
self,
|
|
85
|
+
before_id: int = 0,
|
|
86
|
+
channel: str | None = None,
|
|
87
|
+
recipient: str | None = None,
|
|
88
|
+
limit: int = 100,
|
|
89
|
+
) -> list[dict[str, Any]]:
|
|
90
|
+
return self.repository().read_before(before_id, channel, recipient, limit)
|
|
91
|
+
|
|
92
|
+
def recent_rows(self, limit: int | None = None) -> list[dict[str, Any]]:
|
|
93
|
+
return self.repository().recent_rows(
|
|
94
|
+
self.launch.recent_scan_limit if limit is None else limit
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def launch_guard_repository(self) -> ChannelLaunchGuardRepository:
|
|
98
|
+
return ChannelLaunchGuardRepository(
|
|
99
|
+
path=self.launch.guard_path(),
|
|
100
|
+
now=self.launch.now,
|
|
101
|
+
log=self.storage.log,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def launch_guard(self) -> dict[str, Any] | None:
|
|
105
|
+
return self.launch_guard_repository().read()
|
|
106
|
+
|
|
107
|
+
def write_launch_guard(
|
|
108
|
+
self, max_existing_id: int, ttl_seconds: float = 180.0
|
|
109
|
+
) -> None:
|
|
110
|
+
self.launch_guard_repository().write(max_existing_id, ttl_seconds)
|
|
111
|
+
|
|
112
|
+
def duplicate(self, message: dict[str, Any]) -> dict[str, Any] | None:
|
|
113
|
+
return ChannelMessageDedupeService(
|
|
114
|
+
ports=ChannelMessageDedupePorts(
|
|
115
|
+
stable_key=self.identity.stable_key,
|
|
116
|
+
fallback_key=self.identity.fallback_key,
|
|
117
|
+
recent_rows=self.recent_rows,
|
|
118
|
+
launch_guard=self.launch_guard,
|
|
119
|
+
timestamp_seconds=self.identity.timestamp_seconds,
|
|
120
|
+
now=self.launch.now,
|
|
121
|
+
),
|
|
122
|
+
fallback_ttl_seconds=self.launch.fallback_ttl_seconds,
|
|
123
|
+
).duplicate(message)
|
|
124
|
+
|
|
125
|
+
def initialize_next_id(self) -> int:
|
|
126
|
+
cached = self.cache.read_next_id()
|
|
127
|
+
if cached is not None:
|
|
128
|
+
return cached
|
|
129
|
+
next_id = self.max_id() + 1
|
|
130
|
+
self.cache.write_next_id(next_id)
|
|
131
|
+
return next_id
|
|
132
|
+
|
|
133
|
+
def append(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
134
|
+
message = self.repository().append(
|
|
135
|
+
payload,
|
|
136
|
+
ChannelMessageAppendPorts(
|
|
137
|
+
self.storage.condition,
|
|
138
|
+
self.file_lock,
|
|
139
|
+
self.duplicate,
|
|
140
|
+
self.identity.normalize_recipients,
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
self.cache.write_next_id(int(message.get("id") or 0) + 1)
|
|
144
|
+
return message
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True, slots=True)
|
|
148
|
+
class ChannelMessageCompatibilityApi:
|
|
149
|
+
context: Callable[[], ChannelMessageContext]
|
|
150
|
+
|
|
151
|
+
def repository(self) -> ChannelMessageRepository:
|
|
152
|
+
return self.context().repository()
|
|
153
|
+
|
|
154
|
+
def max_id(self) -> int:
|
|
155
|
+
return self.context().max_id()
|
|
156
|
+
|
|
157
|
+
def max_id_before_epoch(self, cutoff_epoch: float) -> int:
|
|
158
|
+
return self.context().max_id_before_epoch(cutoff_epoch)
|
|
159
|
+
|
|
160
|
+
def file_lock(self) -> Any:
|
|
161
|
+
return self.context().file_lock()
|
|
162
|
+
|
|
163
|
+
def read(
|
|
164
|
+
self,
|
|
165
|
+
after_id: int = 0,
|
|
166
|
+
channel: str | None = None,
|
|
167
|
+
recipient: str | None = None,
|
|
168
|
+
limit: int = 100,
|
|
169
|
+
) -> list[dict[str, Any]]:
|
|
170
|
+
return self.context().read(after_id, channel, recipient, limit)
|
|
171
|
+
|
|
172
|
+
def read_before(
|
|
173
|
+
self,
|
|
174
|
+
before_id: int = 0,
|
|
175
|
+
channel: str | None = None,
|
|
176
|
+
recipient: str | None = None,
|
|
177
|
+
limit: int = 100,
|
|
178
|
+
) -> list[dict[str, Any]]:
|
|
179
|
+
return self.context().read_before(before_id, channel, recipient, limit)
|
|
180
|
+
|
|
181
|
+
def recent_rows(self, limit: int | None = None) -> list[dict[str, Any]]:
|
|
182
|
+
return self.context().recent_rows(limit)
|
|
183
|
+
|
|
184
|
+
def launch_guard_repository(self) -> ChannelLaunchGuardRepository:
|
|
185
|
+
return self.context().launch_guard_repository()
|
|
186
|
+
|
|
187
|
+
def launch_guard(self) -> dict[str, Any] | None:
|
|
188
|
+
return self.context().launch_guard()
|
|
189
|
+
|
|
190
|
+
def write_launch_guard(
|
|
191
|
+
self, max_existing_id: int, ttl_seconds: float = 180.0
|
|
192
|
+
) -> None:
|
|
193
|
+
self.context().write_launch_guard(max_existing_id, ttl_seconds)
|
|
194
|
+
|
|
195
|
+
def duplicate(self, message: dict[str, Any]) -> dict[str, Any] | None:
|
|
196
|
+
return self.context().duplicate(message)
|
|
197
|
+
|
|
198
|
+
def initialize_next_id(self) -> int:
|
|
199
|
+
return self.context().initialize_next_id()
|
|
200
|
+
|
|
201
|
+
def append(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
202
|
+
return self.context().append(payload)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
__all__ = [
|
|
206
|
+
"ChannelMessageCachePorts",
|
|
207
|
+
"ChannelMessageCompatibilityApi",
|
|
208
|
+
"ChannelMessageContext",
|
|
209
|
+
"ChannelMessageIdentityPorts",
|
|
210
|
+
"ChannelMessageLaunchPorts",
|
|
211
|
+
"ChannelMessageStoragePorts",
|
|
212
|
+
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from contextlib import contextmanager
|
|
4
|
-
from datetime import datetime
|
|
4
|
+
from datetime import datetime
|
|
5
5
|
import json
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
import os
|
|
@@ -65,9 +65,11 @@ class ChannelMessageRepository:
|
|
|
65
65
|
if self.path.exists() and self.path.stat().st_size > self.max_bytes:
|
|
66
66
|
self.path.replace(self.path.with_suffix(".jsonl.1"))
|
|
67
67
|
next_id = previous_max_id + 1
|
|
68
|
+
created_at = time.time()
|
|
68
69
|
message = {
|
|
69
70
|
"id": next_id,
|
|
70
|
-
"time": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
71
|
+
"time": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(created_at)),
|
|
72
|
+
"created_at_epoch": created_at,
|
|
71
73
|
"channel": str(payload.get("channel") or "default"),
|
|
72
74
|
"sender_id": str(payload.get("sender_id") or payload.get("sender") or "anonymous"),
|
|
73
75
|
"recipients": ports.normalize_recipients(
|
|
@@ -100,6 +102,12 @@ class ChannelMessageRepository:
|
|
|
100
102
|
|
|
101
103
|
@staticmethod
|
|
102
104
|
def timestamp_seconds(item: dict[str, Any]) -> float | None:
|
|
105
|
+
raw_epoch = item.get("created_at_epoch")
|
|
106
|
+
if raw_epoch is not None:
|
|
107
|
+
try:
|
|
108
|
+
return float(raw_epoch)
|
|
109
|
+
except (TypeError, ValueError):
|
|
110
|
+
pass
|
|
103
111
|
raw = item.get("time") or item.get("created_at") or item.get("updated_at")
|
|
104
112
|
if not raw:
|
|
105
113
|
return None
|
|
@@ -113,7 +121,10 @@ class ChannelMessageRepository:
|
|
|
113
121
|
except (TypeError, ValueError):
|
|
114
122
|
return None
|
|
115
123
|
if parsed.tzinfo is None:
|
|
116
|
-
|
|
124
|
+
# Historical messages were written with time.strftime(), i.e. in
|
|
125
|
+
# the machine's local timezone. Treating those values as UTC can
|
|
126
|
+
# move the replay boundary by several hours.
|
|
127
|
+
return parsed.timestamp()
|
|
117
128
|
return parsed.timestamp()
|
|
118
129
|
|
|
119
130
|
def _jsonl_items(self, operation: str) -> list[dict[str, Any]]:
|
|
@@ -52,6 +52,7 @@ class ChannelInjectionIO:
|
|
|
52
52
|
@dataclass(frozen=True, slots=True)
|
|
53
53
|
class ChannelInjectionPolicy:
|
|
54
54
|
wake_batch_limit: Callable[[], int]
|
|
55
|
+
replay_skip_reason: Callable[[dict[str, Any]], str]
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
@dataclass(frozen=True, slots=True)
|
|
@@ -115,6 +116,14 @@ def inject_pending_channel_messages(
|
|
|
115
116
|
if web_chat_only and not state.message_is_web_chat(message):
|
|
116
117
|
io.log("INFO", f"channel_stdin_proxy_skipped_noise message_id={message_id} channel={channel} reason=not_web_chat")
|
|
117
118
|
continue
|
|
119
|
+
replay_skip_reason = services.policy.replay_skip_reason(message)
|
|
120
|
+
if replay_skip_reason:
|
|
121
|
+
io.log(
|
|
122
|
+
"WARN",
|
|
123
|
+
f"channel_stdin_proxy_skipped_noise message_id={message_id} "
|
|
124
|
+
f"channel={channel} reason={replay_skip_reason}",
|
|
125
|
+
)
|
|
126
|
+
continue
|
|
118
127
|
skip_reason = state.message_skip_reason(message)
|
|
119
128
|
if skip_reason:
|
|
120
129
|
io.log("INFO", f"channel_stdin_proxy_skipped_noise message_id={message_id} channel={channel} reason={skip_reason}")
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Channel probe-cache decisions and runtime launch orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Callable, Iterable
|
|
8
|
+
|
|
9
|
+
from .codex_channel_sse_launch import (
|
|
10
|
+
CodexChannelSseEffects,
|
|
11
|
+
CodexChannelSseLaunchService,
|
|
12
|
+
CodexChannelSseQueryPorts,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class ChannelProbeLaunchDiscoveryPorts:
|
|
18
|
+
discover_servers: Callable[..., dict[str, dict[str, Any]]]
|
|
19
|
+
cached_external_capable: Callable[[], list[str]]
|
|
20
|
+
channel_specs: Callable[[dict[str, Any], list[str]], list[str]]
|
|
21
|
+
server_names: Callable[[Iterable[str]], list[str]]
|
|
22
|
+
codex_capable_names: Callable[..., list[str]]
|
|
23
|
+
external_names: Callable[..., list[str]]
|
|
24
|
+
dedupe: Callable[[Iterable[str]], list[str]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class ChannelProbeLaunchCachePorts:
|
|
29
|
+
service: Callable[[], Any]
|
|
30
|
+
read: Callable[[], dict[str, Any]]
|
|
31
|
+
records: Callable[[], list[dict[str, Any]]]
|
|
32
|
+
refresh: Callable[..., dict[str, Any]]
|
|
33
|
+
bucket: Callable[[dict[str, Any]], str]
|
|
34
|
+
panel_rows: Callable[[dict[str, Any]], tuple[list[str], list[str]]]
|
|
35
|
+
log: Callable[[str, str], None]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class ChannelProbeLaunchEffects:
|
|
40
|
+
delivery_mode: Callable[[dict[str, Any]], str]
|
|
41
|
+
auto_start: Callable[..., list[dict[str, Any]]]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class ChannelProbeLaunchContext:
|
|
46
|
+
discovery: ChannelProbeLaunchDiscoveryPorts
|
|
47
|
+
cache: ChannelProbeLaunchCachePorts
|
|
48
|
+
effects: ChannelProbeLaunchEffects
|
|
49
|
+
native_channel_names: frozenset[str]
|
|
50
|
+
|
|
51
|
+
def native_auto_capable_names(
|
|
52
|
+
self, passthrough: list[str] | None = None
|
|
53
|
+
) -> list[str]:
|
|
54
|
+
discovered = set(
|
|
55
|
+
self.discovery.discover_servers(passthrough or []).keys()
|
|
56
|
+
)
|
|
57
|
+
if not discovered:
|
|
58
|
+
return []
|
|
59
|
+
return [
|
|
60
|
+
name
|
|
61
|
+
for name in self.discovery.cached_external_capable()
|
|
62
|
+
if name in discovered
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
def start_codex_sse(
|
|
66
|
+
self,
|
|
67
|
+
cfg: dict[str, Any],
|
|
68
|
+
codex_mcp_config: Path | None,
|
|
69
|
+
allowed_server_names: Iterable[str] | None = None,
|
|
70
|
+
) -> list[dict[str, Any]]:
|
|
71
|
+
service = CodexChannelSseLaunchService(
|
|
72
|
+
query=CodexChannelSseQueryPorts(
|
|
73
|
+
delivery_mode=self.effects.delivery_mode,
|
|
74
|
+
channel_specs=self.discovery.channel_specs,
|
|
75
|
+
server_names=self.discovery.server_names,
|
|
76
|
+
capable_names=self.discovery.codex_capable_names,
|
|
77
|
+
dedupe=self.discovery.dedupe,
|
|
78
|
+
),
|
|
79
|
+
effects=CodexChannelSseEffects(
|
|
80
|
+
auto_start=self.effects.auto_start,
|
|
81
|
+
log=self.cache.log,
|
|
82
|
+
),
|
|
83
|
+
native_channel_names=frozenset(
|
|
84
|
+
name.casefold() for name in self.native_channel_names
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
return service.start(cfg, codex_mcp_config, allowed_server_names)
|
|
88
|
+
|
|
89
|
+
def summary(self, prefix: str, cache: dict[str, Any]) -> str:
|
|
90
|
+
records = [
|
|
91
|
+
record
|
|
92
|
+
for record in cache.get("servers") or []
|
|
93
|
+
if isinstance(record, dict)
|
|
94
|
+
]
|
|
95
|
+
grouped = {
|
|
96
|
+
name: [
|
|
97
|
+
record for record in records if self.cache.bucket(record) == name
|
|
98
|
+
]
|
|
99
|
+
for name in ("capable", "inconclusive", "non_capable")
|
|
100
|
+
}
|
|
101
|
+
return (
|
|
102
|
+
f"{prefix}: {len(grouped['capable'])} channel-capable, "
|
|
103
|
+
f"{len(grouped['inconclusive'])} inconclusive, "
|
|
104
|
+
f"{len(grouped['non_capable'])} non-capable server(s)."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def panel_rows(
|
|
108
|
+
self, cfg: dict[str, Any], passthrough: list[str]
|
|
109
|
+
) -> tuple[list[str], list[str], list[str]]:
|
|
110
|
+
messages: list[str] = []
|
|
111
|
+
if self.needs_refresh(cfg, passthrough):
|
|
112
|
+
try:
|
|
113
|
+
self.cache.log(
|
|
114
|
+
"INFO",
|
|
115
|
+
"channel_probe_menu_refresh "
|
|
116
|
+
"reason=missing_cache_or_selected_server",
|
|
117
|
+
)
|
|
118
|
+
result = self.cache.refresh(passthrough)
|
|
119
|
+
messages = [self.summary("Probe complete", result)]
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
self.cache.log(
|
|
122
|
+
"WARN",
|
|
123
|
+
"channel_probe_menu_refresh_failed "
|
|
124
|
+
f"error={type(exc).__name__}: {exc}",
|
|
125
|
+
)
|
|
126
|
+
messages = [
|
|
127
|
+
f"Channel probe failed: {type(exc).__name__}: {exc}"
|
|
128
|
+
]
|
|
129
|
+
rows, values = self.cache.panel_rows(cfg)
|
|
130
|
+
return rows, values, messages
|
|
131
|
+
|
|
132
|
+
def candidate_names(
|
|
133
|
+
self,
|
|
134
|
+
cfg: dict[str, Any],
|
|
135
|
+
passthrough: list[str],
|
|
136
|
+
extra_config_paths: list[Path | str] | None = None,
|
|
137
|
+
) -> list[str]:
|
|
138
|
+
return self.cache.service().candidate_names(
|
|
139
|
+
self.discovery.channel_specs(cfg, passthrough),
|
|
140
|
+
lambda: self.discovery.external_names(
|
|
141
|
+
passthrough,
|
|
142
|
+
extra_config_paths=extra_config_paths,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def needs_refresh(
|
|
147
|
+
self,
|
|
148
|
+
cfg: dict[str, Any],
|
|
149
|
+
passthrough: list[str],
|
|
150
|
+
extra_config_paths: list[Path | str] | None = None,
|
|
151
|
+
) -> bool:
|
|
152
|
+
candidates = self.candidate_names(
|
|
153
|
+
cfg, passthrough, extra_config_paths=extra_config_paths
|
|
154
|
+
)
|
|
155
|
+
return self.cache.service().needs_refresh(
|
|
156
|
+
self.cache.read(), self.cache.records(), candidates
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def ensure_cache(
|
|
160
|
+
self,
|
|
161
|
+
cfg: dict[str, Any],
|
|
162
|
+
passthrough: list[str],
|
|
163
|
+
extra_config_paths: list[Path | str] | None = None,
|
|
164
|
+
) -> bool:
|
|
165
|
+
needed = self.needs_refresh(
|
|
166
|
+
cfg, passthrough, extra_config_paths=extra_config_paths
|
|
167
|
+
)
|
|
168
|
+
return self.cache.service().ensure_refresh(
|
|
169
|
+
needed,
|
|
170
|
+
lambda: self.cache.refresh(
|
|
171
|
+
passthrough,
|
|
172
|
+
**(
|
|
173
|
+
{"extra_config_paths": extra_config_paths}
|
|
174
|
+
if extra_config_paths is not None
|
|
175
|
+
else {}
|
|
176
|
+
),
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass(frozen=True, slots=True)
|
|
182
|
+
class ChannelProbeLaunchCompatibilityApi:
|
|
183
|
+
context: Callable[[], ChannelProbeLaunchContext]
|
|
184
|
+
|
|
185
|
+
def native_auto_capable_names(self, *args: Any, **kwargs: Any) -> list[str]:
|
|
186
|
+
return self.context().native_auto_capable_names(*args, **kwargs)
|
|
187
|
+
|
|
188
|
+
def start_codex_sse(self, *args: Any, **kwargs: Any) -> list[dict[str, Any]]:
|
|
189
|
+
return self.context().start_codex_sse(*args, **kwargs)
|
|
190
|
+
|
|
191
|
+
def summary(self, *args: Any, **kwargs: Any) -> str:
|
|
192
|
+
return self.context().summary(*args, **kwargs)
|
|
193
|
+
|
|
194
|
+
def panel_rows(self, *args: Any, **kwargs: Any) -> tuple[list[str], list[str], list[str]]:
|
|
195
|
+
return self.context().panel_rows(*args, **kwargs)
|
|
196
|
+
|
|
197
|
+
def candidate_names(self, *args: Any, **kwargs: Any) -> list[str]:
|
|
198
|
+
return self.context().candidate_names(*args, **kwargs)
|
|
199
|
+
|
|
200
|
+
def needs_refresh(self, *args: Any, **kwargs: Any) -> bool:
|
|
201
|
+
return self.context().needs_refresh(*args, **kwargs)
|
|
202
|
+
|
|
203
|
+
def ensure_cache(self, *args: Any, **kwargs: Any) -> bool:
|
|
204
|
+
return self.context().ensure_cache(*args, **kwargs)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
__all__ = [
|
|
208
|
+
"ChannelProbeLaunchCachePorts",
|
|
209
|
+
"ChannelProbeLaunchCompatibilityApi",
|
|
210
|
+
"ChannelProbeLaunchContext",
|
|
211
|
+
"ChannelProbeLaunchDiscoveryPorts",
|
|
212
|
+
"ChannelProbeLaunchEffects",
|
|
213
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Safety policy for replaying durable channel messages into an agent terminal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class ChannelReplaySafetyPolicy:
|
|
11
|
+
now: Callable[[], float]
|
|
12
|
+
replay_ttl_seconds: Callable[[], float]
|
|
13
|
+
timestamp_seconds: Callable[[dict[str, Any]], float | None]
|
|
14
|
+
is_web_chat: Callable[[dict[str, Any]], bool]
|
|
15
|
+
|
|
16
|
+
def skip_reason(self, message: dict[str, Any]) -> str:
|
|
17
|
+
"""Expire old browser commands independently of startup cursor state.
|
|
18
|
+
|
|
19
|
+
Browser messages are durable history, but they are also executable input.
|
|
20
|
+
A cursor or launch-hook failure must never turn old history back into a
|
|
21
|
+
command after a process or machine restart.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
if not self.is_web_chat(message):
|
|
25
|
+
return ""
|
|
26
|
+
created_at = self.timestamp_seconds(message)
|
|
27
|
+
if created_at is None:
|
|
28
|
+
return ""
|
|
29
|
+
ttl = max(1.0, float(self.replay_ttl_seconds()))
|
|
30
|
+
age = self.now() - created_at
|
|
31
|
+
if age > ttl:
|
|
32
|
+
return "stale_web_chat_replay"
|
|
33
|
+
if age < -300.0:
|
|
34
|
+
return "future_web_chat_timestamp"
|
|
35
|
+
return ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["ChannelReplaySafetyPolicy"]
|
|
@@ -55,6 +55,14 @@ class ChannelRuntimeEnvironmentPolicy:
|
|
|
55
55
|
maximum=50,
|
|
56
56
|
)
|
|
57
57
|
|
|
58
|
+
def web_chat_replay_ttl_seconds(self) -> float:
|
|
59
|
+
return self._bounded_float(
|
|
60
|
+
self.environment.get("CIEL_RUNTIME_WEB_CHAT_REPLAY_TTL_SECONDS"),
|
|
61
|
+
default=600.0,
|
|
62
|
+
minimum=30.0,
|
|
63
|
+
maximum=3600.0,
|
|
64
|
+
)
|
|
65
|
+
|
|
58
66
|
def wake_claim_ttl_seconds(self) -> float:
|
|
59
67
|
return self._bounded_float(
|
|
60
68
|
self.environment.get(
|