@oneciel-ai/ciel-runtime 0.2.2 → 0.2.3

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 (126) hide show
  1. package/ciel_runtime.py +2553 -9635
  2. package/ciel_runtime_support/advisor_request_builder.py +8 -21
  3. package/ciel_runtime_support/anthropic_tool_turns.py +13 -8
  4. package/ciel_runtime_support/architecture.py +68 -0
  5. package/ciel_runtime_support/architecture_budget.py +1 -1
  6. package/ciel_runtime_support/channel_connection_context.py +233 -0
  7. package/ciel_runtime_support/channel_delivery_context.py +332 -0
  8. package/ciel_runtime_support/channel_mcp_context.py +313 -0
  9. package/ciel_runtime_support/channel_mcp_discovery.py +47 -0
  10. package/ciel_runtime_support/channel_mcp_transport.py +5 -1
  11. package/ciel_runtime_support/channel_message_context.py +212 -0
  12. package/ciel_runtime_support/channel_message_repository.py +14 -3
  13. package/ciel_runtime_support/channel_pending_injection.py +9 -0
  14. package/ciel_runtime_support/channel_probe_launch_context.py +213 -0
  15. package/ciel_runtime_support/channel_replay_policy.py +38 -0
  16. package/ciel_runtime_support/channel_runtime_environment.py +8 -0
  17. package/ciel_runtime_support/channel_session_context.py +236 -0
  18. package/ciel_runtime_support/channel_terminal_context.py +350 -0
  19. package/ciel_runtime_support/channel_wake_context.py +532 -0
  20. package/ciel_runtime_support/claude_environment.py +60 -0
  21. package/ciel_runtime_support/claude_launch_assembly.py +249 -0
  22. package/ciel_runtime_support/claude_router.py +62 -12
  23. package/ciel_runtime_support/cli_application_context.py +132 -0
  24. package/ciel_runtime_support/cli_assembly.py +50 -0
  25. package/ciel_runtime_support/codex_backend_context.py +363 -0
  26. package/ciel_runtime_support/codex_config.py +13 -1
  27. package/ciel_runtime_support/codex_launch_assembly.py +213 -0
  28. package/ciel_runtime_support/codex_launch_configuration.py +30 -1
  29. package/ciel_runtime_support/codex_mcp_integration.py +90 -8
  30. package/ciel_runtime_support/codex_model_catalog.py +4 -1
  31. package/ciel_runtime_support/codex_reasoning_rejects.py +225 -0
  32. package/ciel_runtime_support/codex_router.py +38 -8
  33. package/ciel_runtime_support/codex_turn_recovery.py +154 -0
  34. package/ciel_runtime_support/config_migrations.py +103 -0
  35. package/ciel_runtime_support/configuration_cli.py +38 -0
  36. package/ciel_runtime_support/context_compaction.py +9 -4
  37. package/ciel_runtime_support/credential_management.py +12 -0
  38. package/ciel_runtime_support/credentials.py +12 -0
  39. package/ciel_runtime_support/github_copilot_oauth.py +2 -2
  40. package/ciel_runtime_support/hosted_formula_tools.py +216 -0
  41. package/ciel_runtime_support/kimi_runtime_context.py +208 -0
  42. package/ciel_runtime_support/llm_preset_context.py +338 -0
  43. package/ciel_runtime_support/managed_mcp_config.py +8 -4
  44. package/ciel_runtime_support/mcp_configuration_context.py +291 -0
  45. package/ciel_runtime_support/mcp_http_proxy.py +14 -8
  46. package/ciel_runtime_support/mcp_probe_transport.py +47 -15
  47. package/ciel_runtime_support/mcp_transport.py +14 -1
  48. package/ciel_runtime_support/native_context_recovery.py +72 -0
  49. package/ciel_runtime_support/ollama_catalog_context.py +213 -0
  50. package/ciel_runtime_support/ollama_stream_collection.py +103 -0
  51. package/ciel_runtime_support/ollama_thinking.py +6 -1
  52. package/ciel_runtime_support/ollama_wire_projection.py +157 -0
  53. package/ciel_runtime_support/openai_forwarding.py +32 -10
  54. package/ciel_runtime_support/openai_responses_router.py +12 -0
  55. package/ciel_runtime_support/package_lifecycle.py +39 -0
  56. package/ciel_runtime_support/prelaunch_assembly.py +37 -0
  57. package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
  58. package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
  59. package/ciel_runtime_support/prompt_compaction.py +144 -0
  60. package/ciel_runtime_support/prompt_injection.py +45 -0
  61. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
  62. package/ciel_runtime_support/protocols/chat_projection.py +85 -5
  63. package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
  64. package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
  65. package/ciel_runtime_support/protocols/ollama_response.py +57 -5
  66. package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
  67. package/ciel_runtime_support/protocols/openai_responses.py +61 -15
  68. package/ciel_runtime_support/provider_adapters.py +26 -0
  69. package/ciel_runtime_support/provider_administration_context.py +207 -0
  70. package/ciel_runtime_support/provider_config_mutations.py +3 -0
  71. package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
  72. package/ciel_runtime_support/provider_model_context.py +107 -0
  73. package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
  74. package/ciel_runtime_support/provider_model_selection.py +10 -3
  75. package/ciel_runtime_support/provider_models.py +45 -2
  76. package/ciel_runtime_support/provider_option_cli.py +19 -0
  77. package/ciel_runtime_support/provider_policy.py +1 -1
  78. package/ciel_runtime_support/provider_readiness_context.py +189 -0
  79. package/ciel_runtime_support/provider_request_builder.py +64 -28
  80. package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
  81. package/ciel_runtime_support/provider_timeout_policy.py +54 -0
  82. package/ciel_runtime_support/provider_tool_policy.py +9 -1
  83. package/ciel_runtime_support/providers/__init__.py +6 -0
  84. package/ciel_runtime_support/providers/alibaba.py +634 -0
  85. package/ciel_runtime_support/providers/catalog.py +24 -16
  86. package/ciel_runtime_support/providers/deepseek.py +73 -0
  87. package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
  88. package/ciel_runtime_support/providers/kimi.py +69 -9
  89. package/ciel_runtime_support/providers/ollama.py +8 -0
  90. package/ciel_runtime_support/providers/ollama_context.py +21 -2
  91. package/ciel_runtime_support/providers/vllm.py +7 -1
  92. package/ciel_runtime_support/response_collection.py +68 -18
  93. package/ciel_runtime_support/response_collection_context.py +391 -0
  94. package/ciel_runtime_support/response_stream_context.py +555 -0
  95. package/ciel_runtime_support/responses_input_compatibility.py +121 -0
  96. package/ciel_runtime_support/responses_usage_observer.py +83 -0
  97. package/ciel_runtime_support/router_client_lifecycle.py +1 -0
  98. package/ciel_runtime_support/router_http.py +239 -3
  99. package/ciel_runtime_support/router_observability_context.py +251 -0
  100. package/ciel_runtime_support/router_process_context.py +200 -0
  101. package/ciel_runtime_support/router_process_lifecycle.py +2 -0
  102. package/ciel_runtime_support/router_request_assembly.py +399 -0
  103. package/ciel_runtime_support/router_request_context.py +215 -0
  104. package/ciel_runtime_support/router_server_context.py +82 -0
  105. package/ciel_runtime_support/runaway_output_guard.py +488 -0
  106. package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
  107. package/ciel_runtime_support/runtime_asset_context.py +297 -0
  108. package/ciel_runtime_support/runtime_constants.py +16 -1
  109. package/ciel_runtime_support/runtime_launch.py +9 -5
  110. package/ciel_runtime_support/runtime_launch_context.py +130 -0
  111. package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
  112. package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
  113. package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
  114. package/ciel_runtime_support/runtime_paths.py +60 -40
  115. package/ciel_runtime_support/runtime_primitives.py +78 -0
  116. package/ciel_runtime_support/sse_stream_collection.py +236 -0
  117. package/ciel_runtime_support/statusline_script.py +57 -8
  118. package/ciel_runtime_support/streaming_anthropic.py +361 -24
  119. package/ciel_runtime_support/tool_schema.py +40 -2
  120. package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
  121. package/ciel_runtime_support/upstream_dump.py +68 -0
  122. package/ciel_runtime_support/upstream_retry_context.py +259 -0
  123. package/ciel_runtime_support/workspace_router_selection.py +86 -0
  124. package/docs/Configuration.md +50 -0
  125. package/docs/Test-Suite.md +1 -0
  126. package/package.json +1 -1
@@ -0,0 +1,532 @@
1
+ """Interactive channel wake, transcript, and cursor-recovery 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 . import channel_injection, channel_llm_context
10
+ from .channel_cursor_recovery import (
11
+ ChannelCursorRecoveryPolicy,
12
+ ChannelCursorRecoveryPorts,
13
+ ChannelCursorRecoveryService,
14
+ )
15
+ from .channel_compact_injection import (
16
+ ChannelCompactInjectionService,
17
+ ChannelCompactRequestPorts,
18
+ ChannelCompactRuntimePorts,
19
+ )
20
+ from .channel_pending_injection import (
21
+ ChannelInjectionIO,
22
+ ChannelInjectionPolicy,
23
+ ChannelInjectionPrompts,
24
+ ChannelInjectionServices,
25
+ ChannelInjectionState,
26
+ ChannelInjectionWakeStore,
27
+ inject_pending_channel_messages,
28
+ )
29
+ from .channel_replay_policy import ChannelReplaySafetyPolicy
30
+ from .channel_transcript import (
31
+ ChannelWakeStateReader,
32
+ ChannelWakeStateReaderPorts,
33
+ ChannelWakeTranscriptServices,
34
+ )
35
+ from .channel_transcript_repository import ChannelTranscriptRepository
36
+ from .channel_wake_claim_repository import ChannelWakeClaimRepository
37
+ from .channel_wake_delivery_repository import ChannelWakeDeliveryRepository
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class ChannelWakeClaimPorts:
42
+ path: Path
43
+ file_lock: Callable[[Path], Any]
44
+ now: Callable[[], float]
45
+ ttl_seconds: Callable[[], float]
46
+ log: Callable[[str, str], None]
47
+ delivery: ChannelWakeDeliveryRepository
48
+ prompt_message_ids: Callable[[str], set[int]]
49
+ prompt_reference: Callable[[str, int, list[str]], bool]
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class ChannelWakeMessagePorts:
54
+ content_to_text: Callable[[Any], str]
55
+ read_messages: Callable[[int, int], list[dict[str, Any]]]
56
+ superseded_ids: Callable[[list[dict[str, Any]]], set[int]]
57
+ wake_request: Callable[[dict[str, Any]], bool]
58
+ plan_mode_active: Callable[[dict[str, Any]], bool]
59
+ delivery_mode: Callable[[], str]
60
+ scan_limit: Callable[[], int]
61
+ skip_reason: Callable[[dict[str, Any]], str]
62
+ remove_wake_prompt: Callable[[dict[str, Any]], dict[str, Any]]
63
+ format_batch_prompt: Callable[[list[dict[str, Any]]], str]
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class ChannelWakeCursorPorts:
68
+ lock: Callable[[], Any]
69
+ read: Callable[[], int]
70
+ write: Callable[[int], None]
71
+ cache: Callable[[int], None]
72
+ clamp_to_clear_floor: Callable[[int], int]
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class ChannelWakeInputPorts:
77
+ environment: dict[str, str]
78
+ platform_enter_bytes: Callable[[], bytes]
79
+ resolve_enter_bytes: Callable[[str | bytes | None, bytes], bytes]
80
+ build_input_bytes: Callable[[str, bytes], bytes]
81
+ find_executable: Callable[[str], str | None]
82
+ run_process: Callable[..., Any]
83
+ sleep: Callable[[float], None]
84
+ retry_delay_seconds: Callable[[], float]
85
+ submit_delay_seconds: Callable[[], float]
86
+ log: Callable[[str, str], None]
87
+
88
+
89
+ @dataclass(frozen=True, slots=True)
90
+ class ChannelTranscriptPorts:
91
+ home: Path
92
+ cache: dict[str, Any]
93
+ scope: dict[str, Any]
94
+ recovery_cache: dict[str, Any]
95
+ now: Callable[[], float]
96
+ read_tail: Callable[[Path], str]
97
+ active_tool_call_from_text: Callable[[str], bool]
98
+ active_turn_from_text: Callable[[str], bool]
99
+ queued_age_from_text: Callable[..., float | None]
100
+ wake_state_from_text: Callable[..., str]
101
+
102
+
103
+ @dataclass(frozen=True, slots=True)
104
+ class ChannelTranscriptPolicyPorts:
105
+ queued_ids_from_text: Callable[[str, ChannelWakeTranscriptServices], set[int]]
106
+ inflight_stale_seconds: Callable[[], float]
107
+ latest_transcript: Callable[..., Path | None]
108
+ claim_prompt: Callable[[int], str]
109
+ prompt_references_message: Callable[..., bool]
110
+ prompt_message_ids: Callable[[str], set[int]]
111
+ log: Callable[[str, str], None]
112
+
113
+
114
+ @dataclass(frozen=True, slots=True)
115
+ class ChannelPendingStatePorts:
116
+ active_tool_call: Callable[[], bool]
117
+ active_turn: Callable[[], bool]
118
+ recover_cursor: Callable[[int], int]
119
+ pending_scan_limit: Callable[[], int]
120
+ superseded_ids: Callable[[list[dict[str, Any]]], set[int]]
121
+ message_is_web_chat: Callable[[dict[str, Any]], bool]
122
+ message_skip_reason: Callable[[dict[str, Any]], str]
123
+ event_identity_key: Callable[[dict[str, Any]], str]
124
+ wake_state_for_message: Callable[..., str]
125
+ queued_wake_is_stale: Callable[..., bool]
126
+
127
+
128
+ @dataclass(frozen=True, slots=True)
129
+ class ChannelPendingDeliveryPorts:
130
+ format_llm_delivery: Callable[[list[dict[str, Any]]], str]
131
+ format_web_chat: Callable[[list[dict[str, Any]]], str]
132
+ format_standard: Callable[[list[dict[str, Any]]], str]
133
+ enter_label: Callable[[bytes], str]
134
+ release_stale: Callable[[int, bool], None]
135
+ mark_delivered: Callable[[int], None]
136
+ record_prompts: Callable[[list[dict[str, Any]], str], None]
137
+ rollback: Callable[[list[dict[str, Any]], list[int]], None]
138
+ commit_cursor: Callable[[int | None], None]
139
+
140
+
141
+ @dataclass(frozen=True, slots=True)
142
+ class ChannelPendingIoPorts:
143
+ inject_lock: Any
144
+ read_messages: Callable[..., list[dict[str, Any]]]
145
+ write_prompt: Callable[..., None]
146
+ compact_read: Callable[[], dict[str, Any] | None]
147
+ compact_clear: Callable[[], None]
148
+ messages_path: Path
149
+ log: Callable[[str, str], None]
150
+
151
+
152
+ @dataclass(frozen=True, slots=True)
153
+ class ChannelPendingPolicyPorts:
154
+ wake_batch_limit: Callable[[], int]
155
+ now: Callable[[], float]
156
+ replay_ttl_seconds: Callable[[], float]
157
+ timestamp_seconds: Callable[[dict[str, Any]], float | None]
158
+ is_web_chat: Callable[[dict[str, Any]], bool]
159
+
160
+
161
+ @dataclass(frozen=True, slots=True)
162
+ class ChannelWakeContext:
163
+ claims: ChannelWakeClaimPorts
164
+ messages: ChannelWakeMessagePorts
165
+ cursor: ChannelWakeCursorPorts
166
+ input: ChannelWakeInputPorts
167
+ transcript: ChannelTranscriptPorts
168
+ transcript_policy: ChannelTranscriptPolicyPorts
169
+ pending_state: ChannelPendingStatePorts
170
+ pending_delivery: ChannelPendingDeliveryPorts
171
+ pending_io: ChannelPendingIoPorts
172
+ pending_policy: ChannelPendingPolicyPorts
173
+
174
+ def claim_repository(self) -> ChannelWakeClaimRepository:
175
+ return ChannelWakeClaimRepository(
176
+ path=self.claims.path,
177
+ file_lock=self.claims.file_lock,
178
+ now=self.claims.now,
179
+ ttl_seconds=self.claims.ttl_seconds,
180
+ log=self.claims.log,
181
+ )
182
+
183
+ def claim_prompt(self, message_id: int) -> str:
184
+ if message_id <= 0:
185
+ return ""
186
+ return self.claims.delivery.prompt(message_id) or self.claim_repository().prompt(
187
+ message_id
188
+ )
189
+
190
+ def claim_wake_prompt(self, message_id: int, prompt: str) -> bool:
191
+ return self.claim_repository().claim(message_id, prompt)
192
+
193
+ def clear_wake_claim(self, message_id: int) -> None:
194
+ self.claim_repository().clear(message_id)
195
+
196
+ def prompt_references_message_id(
197
+ self,
198
+ text: str,
199
+ message_id: int,
200
+ prompt_texts: list[str] | tuple[str, ...] | None = None,
201
+ ) -> bool:
202
+ prompts = [str(item) for item in (prompt_texts or ()) if str(item or "").strip()]
203
+ if prompt_texts is None:
204
+ claimed_prompt = self.claim_prompt(message_id)
205
+ if claimed_prompt:
206
+ prompts.append(claimed_prompt)
207
+ return self.claims.prompt_reference(text, message_id, prompts)
208
+
209
+ def message_ids_already_in_request(self, body: dict[str, Any]) -> set[int]:
210
+ ids: set[int] = set()
211
+ for message in body.get("messages") or []:
212
+ if not isinstance(message, dict):
213
+ continue
214
+ text = self.messages.content_to_text(message.get("content"))
215
+ if (
216
+ "ciel-runtime external channel message" not in text
217
+ and "[external channel input]" not in text
218
+ ):
219
+ continue
220
+ ids.update(self.claims.prompt_message_ids(text))
221
+ return ids
222
+
223
+ def commit_cursor(self, last_id: int) -> None:
224
+ self.cursor.cache(last_id)
225
+ try:
226
+ self.cursor.write(last_id)
227
+ except Exception as exc:
228
+ self.claims.log(
229
+ "WARN",
230
+ f"channel_llm_cursor_write_failed error={type(exc).__name__}: {exc}",
231
+ )
232
+
233
+ def stdin_skip_reason(self, message_id: int) -> str:
234
+ if self.claims.delivery.is_delivered(message_id):
235
+ return "stdin_wake_delivered"
236
+ return (
237
+ "stdin_wake_claimed"
238
+ if self.transcript_policy.claim_prompt(message_id)
239
+ else ""
240
+ )
241
+
242
+ def body_with_pending_messages(self, body: dict[str, Any]) -> dict[str, Any]:
243
+ return channel_llm_context.inject_pending_channel_context(
244
+ body,
245
+ channel_llm_context.ChannelLlmContextServices(
246
+ policy=channel_llm_context.ChannelLlmContextPolicy(
247
+ wake_request=self.messages.wake_request,
248
+ plan_mode_active=self.messages.plan_mode_active,
249
+ delivery_mode=self.messages.delivery_mode,
250
+ ids_in_request=self.message_ids_already_in_request,
251
+ scan_limit=self.messages.scan_limit,
252
+ skip_reason=self.messages.skip_reason,
253
+ stdin_skip_reason=self.stdin_skip_reason,
254
+ ),
255
+ repository=channel_llm_context.ChannelLlmContextRepository(
256
+ lock=self.cursor.lock,
257
+ read_cursor=self.cursor.read,
258
+ commit_cursor=self.commit_cursor,
259
+ read_messages=self.messages.read_messages,
260
+ superseded_ids=self.messages.superseded_ids,
261
+ ),
262
+ projection=channel_llm_context.ChannelLlmContextProjection(
263
+ remove_wake_prompt=self.messages.remove_wake_prompt,
264
+ format_prompt=self.messages.format_batch_prompt,
265
+ ),
266
+ log=self.claims.log,
267
+ ),
268
+ )
269
+
270
+ @staticmethod
271
+ def write_all(fd: Any, data: bytes) -> None:
272
+ writer = getattr(fd, "write", None)
273
+ if callable(writer):
274
+ writer(data)
275
+ return
276
+ import os
277
+
278
+ view = memoryview(data)
279
+ while view:
280
+ view = view[os.write(fd, view) :]
281
+
282
+ def enter_bytes(self, value: str | bytes | None = None) -> bytes:
283
+ configured = (
284
+ self.input.environment.get("CIEL_RUNTIME_CHANNEL_WAKE_ENTER")
285
+ if value is None
286
+ else value
287
+ )
288
+ return self.input.resolve_enter_bytes(
289
+ configured, self.input.platform_enter_bytes()
290
+ )
291
+
292
+ def input_bytes(self, prompt: str, enter_bytes: bytes | None = None) -> bytes:
293
+ return self.input.build_input_bytes(prompt, self.enter_bytes(enter_bytes))
294
+
295
+ def current_tmux_pane_text(self) -> str | None:
296
+ from .channel_terminal_input import TmuxPaneSnapshot
297
+
298
+ return TmuxPaneSnapshot(
299
+ self.input.environment,
300
+ self.input.find_executable,
301
+ self.input.run_process,
302
+ self.input.log,
303
+ ).capture()
304
+
305
+ def write_prompt(
306
+ self,
307
+ master_fd: int,
308
+ prompt: str,
309
+ enter_bytes: bytes | None = None,
310
+ *,
311
+ submit_retry_count: int = 1,
312
+ confirm_submit: bool = False,
313
+ bracketed_paste: bool = False,
314
+ submit_delay_seconds: float | None = None,
315
+ write_all: Callable[[Any, bytes], None] | None = None,
316
+ snapshot: Callable[[], str | None] | None = None,
317
+ ) -> None:
318
+ delay = (
319
+ self.input.submit_delay_seconds()
320
+ if submit_delay_seconds is None
321
+ else max(0.0, float(submit_delay_seconds))
322
+ )
323
+ injector = channel_injection.ChannelPromptInjector(
324
+ sleep=self.input.sleep,
325
+ retry_delay_seconds=self.input.retry_delay_seconds,
326
+ snapshot=snapshot or self.current_tmux_pane_text,
327
+ log=self.input.log,
328
+ )
329
+ injector.inject(
330
+ channel_injection.CallableInputTransport(
331
+ master_fd, write_all or self.write_all
332
+ ),
333
+ channel_injection.PromptInjection(
334
+ prompt=prompt,
335
+ policy=channel_injection.RuntimeInjectionPolicy(
336
+ runtime="interactive-cli",
337
+ clear_input=b"\x15",
338
+ submit_input=self.enter_bytes(enter_bytes),
339
+ submit_delay_seconds=delay,
340
+ submit_attempts=max(1, min(8, int(submit_retry_count or 1))),
341
+ confirm_submission=confirm_submit,
342
+ bracketed_paste=bracketed_paste,
343
+ ),
344
+ ),
345
+ )
346
+
347
+ def transcript_repository(self) -> ChannelTranscriptRepository:
348
+ return ChannelTranscriptRepository(
349
+ self.transcript.home,
350
+ self.transcript.cache,
351
+ self.transcript.scope,
352
+ self.transcript.now,
353
+ )
354
+
355
+ def set_transcript_scope(
356
+ self,
357
+ runtime: str,
358
+ *,
359
+ started_at: float | None = None,
360
+ codex_home: Path | None = None,
361
+ ) -> None:
362
+ self.transcript_repository().set_scope(
363
+ runtime, started_at=started_at, codex_home=codex_home
364
+ )
365
+
366
+ def transcript_roots(self) -> tuple[tuple[Path, str], ...]:
367
+ return self.transcript_repository().roots()
368
+
369
+ def latest_transcript_path(self, ttl_seconds: float = 2.0) -> Path | None:
370
+ return self.transcript_repository().latest(ttl_seconds)
371
+
372
+ def transcript_services(self) -> ChannelWakeTranscriptServices:
373
+ return ChannelWakeTranscriptServices(
374
+ claim_prompt=self.transcript_policy.claim_prompt,
375
+ prompt_references_message_id=self.transcript_policy.prompt_references_message,
376
+ prompt_message_ids=self.transcript_policy.prompt_message_ids,
377
+ now=self.transcript.now,
378
+ )
379
+
380
+ def queued_age_seconds_from_text(
381
+ self,
382
+ message_id: int,
383
+ text: str,
384
+ prompt_texts: list[str] | tuple[str, ...] | None = None,
385
+ *,
386
+ now: float | None = None,
387
+ ) -> float | None:
388
+ return self.transcript.queued_age_from_text(
389
+ message_id, text, prompt_texts, self.transcript_services(), now=now
390
+ )
391
+
392
+ def wake_state_from_text(
393
+ self,
394
+ message_id: int,
395
+ text: str,
396
+ prompt_texts: list[str] | tuple[str, ...] | None = None,
397
+ ) -> str:
398
+ return self.transcript.wake_state_from_text(
399
+ message_id, text, prompt_texts, self.transcript_services()
400
+ )
401
+
402
+ def wake_state_reader(self) -> ChannelWakeStateReader:
403
+ return ChannelWakeStateReader(
404
+ ChannelWakeStateReaderPorts(
405
+ self.transcript_policy.latest_transcript,
406
+ self.transcript.read_tail,
407
+ self.wake_state_from_text,
408
+ self.queued_age_seconds_from_text,
409
+ self.transcript_policy.inflight_stale_seconds,
410
+ )
411
+ )
412
+
413
+ def active_tool_call(self) -> bool:
414
+ path = self.transcript_policy.latest_transcript()
415
+ text = self.transcript.read_tail(path) if path is not None else ""
416
+ return bool(text and self.transcript.active_tool_call_from_text(text))
417
+
418
+ def active_turn(self) -> bool:
419
+ path = self.transcript_policy.latest_transcript()
420
+ text = self.transcript.read_tail(path) if path is not None else ""
421
+ return bool(text and self.transcript.active_turn_from_text(text))
422
+
423
+ def queued_command_ids_from_text(self, text: str) -> set[int]:
424
+ return self.transcript_policy.queued_ids_from_text(
425
+ text, self.transcript_services()
426
+ )
427
+
428
+ def cursor_recovery_service(self) -> ChannelCursorRecoveryService:
429
+ return ChannelCursorRecoveryService(
430
+ cache=self.transcript.recovery_cache,
431
+ policy=ChannelCursorRecoveryPolicy(),
432
+ ports=ChannelCursorRecoveryPorts(
433
+ latest_transcript=self.transcript_policy.latest_transcript,
434
+ read_tail=self.transcript.read_tail,
435
+ queued_command_ids=self.queued_command_ids_from_text,
436
+ wake_state=self.wake_state_from_text,
437
+ clamp_to_clear_floor=self.cursor.clamp_to_clear_floor,
438
+ now=self.transcript.now,
439
+ log=self.transcript_policy.log,
440
+ ),
441
+ )
442
+
443
+ def pending_injection_services(self) -> ChannelInjectionServices:
444
+ return ChannelInjectionServices(
445
+ state=ChannelInjectionState(
446
+ active_tool_call=self.pending_state.active_tool_call,
447
+ active_turn=self.pending_state.active_turn,
448
+ recover_cursor=self.pending_state.recover_cursor,
449
+ pending_scan_limit=self.pending_state.pending_scan_limit,
450
+ superseded_ids=self.pending_state.superseded_ids,
451
+ message_is_web_chat=self.pending_state.message_is_web_chat,
452
+ message_skip_reason=self.pending_state.message_skip_reason,
453
+ event_identity_key=self.pending_state.event_identity_key,
454
+ wake_state_for_message=self.pending_state.wake_state_for_message,
455
+ queued_wake_is_stale=self.pending_state.queued_wake_is_stale,
456
+ ),
457
+ prompts=ChannelInjectionPrompts(
458
+ llm_delivery=self.pending_delivery.format_llm_delivery,
459
+ web_chat=self.pending_delivery.format_web_chat,
460
+ standard=self.pending_delivery.format_standard,
461
+ enter_bytes=self.enter_bytes,
462
+ enter_label=self.pending_delivery.enter_label,
463
+ ),
464
+ wake_store=ChannelInjectionWakeStore(
465
+ claim_for_nonblocking_scan=self.transcript_policy.claim_prompt,
466
+ claim_prompt=self.claim_wake_prompt,
467
+ clear_claim=self.clear_wake_claim,
468
+ release_stale=self.pending_delivery.release_stale,
469
+ mark_delivered=self.pending_delivery.mark_delivered,
470
+ record_prompts=self.pending_delivery.record_prompts,
471
+ rollback=self.pending_delivery.rollback,
472
+ commit_cursor=self.pending_delivery.commit_cursor,
473
+ ),
474
+ io=ChannelInjectionIO(
475
+ inject_lock=self.pending_io.inject_lock,
476
+ read_messages=self.pending_io.read_messages,
477
+ write_prompt=self.pending_io.write_prompt,
478
+ log=self.pending_io.log,
479
+ ),
480
+ policy=ChannelInjectionPolicy(
481
+ wake_batch_limit=self.pending_policy.wake_batch_limit,
482
+ replay_skip_reason=ChannelReplaySafetyPolicy(
483
+ self.pending_policy.now,
484
+ self.pending_policy.replay_ttl_seconds,
485
+ self.pending_policy.timestamp_seconds,
486
+ self.pending_policy.is_web_chat,
487
+ ).skip_reason,
488
+ ),
489
+ )
490
+
491
+ def inject_pending(
492
+ self,
493
+ master_fd: int,
494
+ last_id: int,
495
+ enter_bytes: bytes | None = None,
496
+ **options: Any,
497
+ ) -> int:
498
+ return inject_pending_channel_messages(
499
+ master_fd,
500
+ last_id,
501
+ enter_bytes,
502
+ services=self.pending_injection_services(),
503
+ **options,
504
+ )
505
+
506
+ def inject_compact(
507
+ self,
508
+ master_fd: int,
509
+ enter_bytes: bytes | None = None,
510
+ **options: Any,
511
+ ) -> str:
512
+ return ChannelCompactInjectionService(
513
+ request=ChannelCompactRequestPorts(
514
+ read=self.pending_io.compact_read,
515
+ clear=self.pending_io.compact_clear,
516
+ ),
517
+ runtime=ChannelCompactRuntimePorts(
518
+ active_tool_call=self.pending_state.active_tool_call,
519
+ active_turn=self.pending_state.active_turn,
520
+ enter_bytes=self.enter_bytes,
521
+ write_prompt=self.pending_io.write_prompt,
522
+ enter_label=self.pending_delivery.enter_label,
523
+ ),
524
+ log=self.pending_io.log,
525
+ ).inject(master_fd, enter_bytes, **options)
526
+
527
+ def messages_file_marker(self) -> tuple[float, int]:
528
+ try:
529
+ stat = self.pending_io.messages_path.stat()
530
+ return (stat.st_mtime, stat.st_size)
531
+ except Exception:
532
+ return (0.0, 0)
@@ -178,6 +178,65 @@ class ClaudeModelAliasPolicy:
178
178
  return result
179
179
 
180
180
 
181
+ @dataclass(frozen=True)
182
+ class ClaudeModelAliasCompatibilityApi:
183
+ policy: Callable[[], ClaudeModelAliasPolicy]
184
+ context_limit: Callable[[str, dict[str, Any]], int | None]
185
+
186
+ def claims_one_million_context(
187
+ self,
188
+ provider: str,
189
+ config: dict[str, Any],
190
+ model: str,
191
+ *,
192
+ include_current: bool = True,
193
+ ) -> bool:
194
+ return self.policy().claims_one_million_context(
195
+ provider,
196
+ config,
197
+ model,
198
+ include_current=include_current,
199
+ context_limit=(
200
+ self.context_limit(provider, config) if include_current else None
201
+ ),
202
+ )
203
+
204
+ def context_model_alias(
205
+ self,
206
+ provider: str,
207
+ config: dict[str, Any],
208
+ model: str,
209
+ upstream_model: str | None = None,
210
+ ) -> str:
211
+ return self.policy().context_model_alias(
212
+ provider,
213
+ config,
214
+ model,
215
+ upstream_model,
216
+ context_limit=(
217
+ self.context_limit(provider, config)
218
+ if upstream_model is None
219
+ else None
220
+ ),
221
+ )
222
+
223
+ def matches_family(self, model_id: str, family: str) -> bool:
224
+ return self.policy().matches_family(model_id, family)
225
+
226
+ def default_model_aliases(
227
+ self,
228
+ provider: str,
229
+ config: dict[str, Any],
230
+ current_model_alias: str,
231
+ ) -> dict[str, str]:
232
+ return self.policy().default_model_aliases(
233
+ provider,
234
+ config,
235
+ current_model_alias,
236
+ context_limit=self.context_limit(provider, config),
237
+ )
238
+
239
+
181
240
  @dataclass(frozen=True)
182
241
  class ClaudeEnvironmentSourcePorts:
183
242
  load_config: Callable[[], dict[str, Any]]
@@ -373,6 +432,7 @@ __all__ = [
373
432
  "ClaudeLimitPolicy",
374
433
  "ClaudeLimitPorts",
375
434
  "ClaudeModelAliasPolicy",
435
+ "ClaudeModelAliasCompatibilityApi",
376
436
  "ClaudeModelPorts",
377
437
  "ClaudeRuntimeSettingsPolicy",
378
438
  "ClaudeRuntimeSettingsPorts",