@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.
- package/README.md +132 -0
- package/ciel-runtime-menu.py +56 -6
- package/ciel_runtime.py +9379 -35909
- package/ciel_runtime_support/advisor_client.py +193 -0
- package/ciel_runtime_support/advisor_policy.py +320 -0
- package/ciel_runtime_support/advisor_refinement.py +160 -0
- package/ciel_runtime_support/advisor_request_builder.py +261 -0
- package/ciel_runtime_support/agy_installer.py +169 -0
- package/ciel_runtime_support/agy_mcp_restore.py +182 -0
- package/ciel_runtime_support/anthropic_model_policy.py +186 -0
- package/ciel_runtime_support/anthropic_response_writer.py +255 -0
- package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
- package/ciel_runtime_support/api_key_cooldown.py +159 -0
- package/ciel_runtime_support/architecture.py +488 -1
- package/ciel_runtime_support/architecture_budget.py +42 -0
- package/ciel_runtime_support/channel_backlog.py +90 -0
- package/ciel_runtime_support/channel_cli.py +119 -0
- package/ciel_runtime_support/channel_compact_injection.py +82 -0
- package/ciel_runtime_support/channel_compact_poll.py +67 -0
- package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
- package/ciel_runtime_support/channel_config_service.py +281 -0
- package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
- package/ciel_runtime_support/channel_connection_registry.py +128 -0
- package/ciel_runtime_support/channel_connection_worker.py +284 -0
- package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
- package/ciel_runtime_support/channel_cursor_repository.py +89 -0
- package/ciel_runtime_support/channel_cursor_service.py +178 -0
- package/ciel_runtime_support/channel_event_identity.py +212 -0
- package/ciel_runtime_support/channel_event_projection.py +315 -0
- package/ciel_runtime_support/channel_inflight.py +127 -0
- package/ciel_runtime_support/channel_injection.py +115 -0
- package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
- package/ciel_runtime_support/channel_launch_policy.py +180 -0
- package/ciel_runtime_support/channel_llm_context.py +156 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
- package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
- package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
- package/ciel_runtime_support/channel_mcp_tools.py +240 -0
- package/ciel_runtime_support/channel_mcp_transport.py +394 -0
- package/ciel_runtime_support/channel_message_dedupe.py +65 -0
- package/ciel_runtime_support/channel_message_policy.py +256 -0
- package/ciel_runtime_support/channel_message_prompt.py +305 -0
- package/ciel_runtime_support/channel_message_repository.py +234 -0
- package/ciel_runtime_support/channel_notification_projection.py +217 -0
- package/ciel_runtime_support/channel_panel.py +162 -0
- package/ciel_runtime_support/channel_pending_injection.py +209 -0
- package/ciel_runtime_support/channel_pending_poll.py +109 -0
- package/ciel_runtime_support/channel_probe_cache.py +433 -0
- package/ciel_runtime_support/channel_probe_report.py +101 -0
- package/ciel_runtime_support/channel_runtime_environment.py +181 -0
- package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
- package/ciel_runtime_support/channel_session_repository.py +90 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
- package/ciel_runtime_support/channel_terminal_input.py +277 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
- package/ciel_runtime_support/channel_tool_context.py +166 -0
- package/ciel_runtime_support/channel_transcript.py +414 -0
- package/ciel_runtime_support/channel_transcript_repository.py +96 -0
- package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
- package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
- package/ciel_runtime_support/chat_files.py +138 -0
- package/ciel_runtime_support/chat_http_controller.py +235 -0
- package/ciel_runtime_support/claude_environment.py +375 -0
- package/ciel_runtime_support/claude_router.py +247 -193
- package/ciel_runtime_support/cli_dispatch.py +792 -0
- package/ciel_runtime_support/cli_parser.py +165 -0
- package/ciel_runtime_support/cli_usage.py +100 -0
- package/ciel_runtime_support/codex_app_server.py +20 -5
- package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
- package/ciel_runtime_support/codex_cli.py +42 -6
- package/ciel_runtime_support/codex_config.py +323 -0
- package/ciel_runtime_support/codex_launch_configuration.py +240 -0
- package/ciel_runtime_support/codex_launch_policy.py +66 -0
- package/ciel_runtime_support/codex_mcp_integration.py +195 -0
- package/ciel_runtime_support/codex_mcp_restore.py +304 -0
- package/ciel_runtime_support/codex_model_catalog.py +133 -0
- package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
- package/ciel_runtime_support/codex_router.py +147 -1
- package/ciel_runtime_support/codex_session_repository.py +115 -0
- package/ciel_runtime_support/codex_session_selection.py +114 -0
- package/ciel_runtime_support/command_asset_installer.py +103 -0
- package/ciel_runtime_support/compatibility_probe.py +295 -0
- package/ciel_runtime_support/compatibility_protocol.py +251 -0
- package/ciel_runtime_support/compatibility_runtime.py +166 -0
- package/ciel_runtime_support/compatibility_test.py +370 -0
- package/ciel_runtime_support/config_migrations.py +307 -0
- package/ciel_runtime_support/config_repository.py +175 -0
- package/ciel_runtime_support/config_value_codec.py +64 -0
- package/ciel_runtime_support/configuration_cli.py +374 -0
- package/ciel_runtime_support/context_compaction.py +280 -0
- package/ciel_runtime_support/context_setup.py +208 -0
- package/ciel_runtime_support/context_summary_policy.py +392 -0
- package/ciel_runtime_support/credential_cli.py +104 -0
- package/ciel_runtime_support/credential_management.py +261 -0
- package/ciel_runtime_support/credentials.py +269 -0
- package/ciel_runtime_support/executable_discovery.py +141 -0
- package/ciel_runtime_support/github_copilot_oauth.py +335 -0
- package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
- package/ciel_runtime_support/header_forwarding.py +73 -0
- package/ciel_runtime_support/headless_config.py +221 -0
- package/ciel_runtime_support/http_response.py +129 -0
- package/ciel_runtime_support/install_diagnostics.py +149 -0
- package/ciel_runtime_support/kimi_identity.py +123 -0
- package/ciel_runtime_support/launch_diagnostics.py +204 -0
- package/ciel_runtime_support/launch_state.py +127 -0
- package/ciel_runtime_support/live_api_key_controller.py +58 -0
- package/ciel_runtime_support/llm_config_http.py +148 -0
- package/ciel_runtime_support/llm_option_config.py +259 -0
- package/ciel_runtime_support/llm_presentation_data.py +447 -0
- package/ciel_runtime_support/llm_presets.py +773 -0
- package/ciel_runtime_support/lm_studio_runtime.py +401 -0
- package/ciel_runtime_support/managed_mcp_config.py +144 -0
- package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
- package/ciel_runtime_support/managed_service_cleanup.py +89 -0
- package/ciel_runtime_support/mcp_config_reader.py +230 -0
- package/ciel_runtime_support/mcp_http_proxy.py +607 -0
- package/ciel_runtime_support/mcp_inventory.py +59 -0
- package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
- package/ciel_runtime_support/mcp_probe_codec.py +136 -0
- package/ciel_runtime_support/mcp_probe_transport.py +328 -0
- package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
- package/ciel_runtime_support/mcp_proxy_config.py +107 -0
- package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
- package/ciel_runtime_support/mcp_proxy_process.py +560 -0
- package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
- package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
- package/ciel_runtime_support/mcp_transport.py +146 -0
- package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
- package/ciel_runtime_support/model_catalog_projection.py +61 -0
- package/ciel_runtime_support/model_context_hints.py +109 -0
- package/ciel_runtime_support/model_panel.py +147 -0
- package/ciel_runtime_support/model_registry_repository.py +231 -0
- package/ciel_runtime_support/npm_runtime.py +191 -0
- package/ciel_runtime_support/ollama_catalog.py +462 -0
- package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
- package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
- package/ciel_runtime_support/ollama_context_sync.py +87 -0
- package/ciel_runtime_support/ollama_forwarding.py +449 -0
- package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
- package/ciel_runtime_support/openai_chat_router.py +64 -0
- package/ciel_runtime_support/openai_forwarding.py +194 -0
- package/ciel_runtime_support/openai_responses_router.py +291 -0
- package/ciel_runtime_support/openai_responses_stream.py +135 -0
- package/ciel_runtime_support/output_budget.py +89 -0
- package/ciel_runtime_support/package_lifecycle.py +217 -0
- package/ciel_runtime_support/plan_artifact_controller.py +104 -0
- package/ciel_runtime_support/prelaunch.py +959 -0
- package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
- package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
- package/ciel_runtime_support/prelaunch_terminal.py +764 -0
- package/ciel_runtime_support/process_control.py +708 -0
- package/ciel_runtime_support/prompt_compaction.py +322 -0
- package/ciel_runtime_support/prompt_injection.py +176 -0
- package/ciel_runtime_support/protocols/__init__.py +24 -0
- package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
- package/ciel_runtime_support/protocols/chat_projection.py +315 -0
- package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
- package/ciel_runtime_support/protocols/ollama_response.py +231 -0
- package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
- package/ciel_runtime_support/protocols/openai_responses.py +271 -0
- package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
- package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
- package/ciel_runtime_support/provider_adapters.py +160 -0
- package/ciel_runtime_support/provider_catalog_sources.py +316 -0
- package/ciel_runtime_support/provider_choice.py +201 -0
- package/ciel_runtime_support/provider_compatibility.py +165 -0
- package/ciel_runtime_support/provider_config_mutations.py +361 -0
- package/ciel_runtime_support/provider_configuration_service.py +162 -0
- package/ciel_runtime_support/provider_context.py +308 -0
- package/ciel_runtime_support/provider_contract_projection.py +73 -0
- package/ciel_runtime_support/provider_descriptor.py +82 -0
- package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
- package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
- package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
- package/ciel_runtime_support/provider_limits.py +457 -0
- package/ciel_runtime_support/provider_model_identity.py +139 -0
- package/ciel_runtime_support/provider_model_selection.py +431 -0
- package/ciel_runtime_support/provider_model_specs.py +142 -0
- package/ciel_runtime_support/provider_models.py +263 -0
- package/ciel_runtime_support/provider_network.py +176 -0
- package/ciel_runtime_support/provider_option_cli.py +238 -0
- package/ciel_runtime_support/provider_option_panel.py +275 -0
- package/ciel_runtime_support/provider_option_status.py +192 -0
- package/ciel_runtime_support/provider_policy.py +101 -0
- package/ciel_runtime_support/provider_query_policy.py +67 -0
- package/ciel_runtime_support/provider_readiness.py +112 -0
- package/ciel_runtime_support/provider_request_access.py +131 -0
- package/ciel_runtime_support/provider_request_builder.py +250 -0
- package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
- package/ciel_runtime_support/provider_runtime_info.py +113 -0
- package/ciel_runtime_support/provider_runtime_modes.py +150 -0
- package/ciel_runtime_support/provider_sampling_policy.py +46 -0
- package/ciel_runtime_support/provider_status.py +145 -0
- package/ciel_runtime_support/provider_timeout_policy.py +184 -0
- package/ciel_runtime_support/provider_tool_policy.py +145 -0
- package/ciel_runtime_support/providers/__init__.py +65 -0
- package/ciel_runtime_support/providers/anthropic.py +160 -0
- package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
- package/ciel_runtime_support/providers/base.py +232 -0
- package/ciel_runtime_support/providers/catalog.py +326 -0
- package/ciel_runtime_support/providers/cloud.py +194 -0
- package/ciel_runtime_support/providers/constants.py +54 -0
- package/ciel_runtime_support/providers/deepseek.py +115 -0
- package/ciel_runtime_support/providers/fireworks.py +158 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
- package/ciel_runtime_support/providers/kimi.py +297 -0
- package/ciel_runtime_support/providers/lm_studio.py +76 -0
- package/ciel_runtime_support/providers/meta.py +257 -0
- package/ciel_runtime_support/providers/native.py +194 -0
- package/ciel_runtime_support/providers/nim.py +69 -0
- package/ciel_runtime_support/providers/nvidia.py +158 -0
- package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
- package/ciel_runtime_support/providers/ollama.py +181 -0
- package/ciel_runtime_support/providers/ollama_context.py +195 -0
- package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
- package/ciel_runtime_support/providers/opencode.py +220 -0
- package/ciel_runtime_support/providers/opencode_go.py +37 -0
- package/ciel_runtime_support/providers/openrouter.py +57 -0
- package/ciel_runtime_support/providers/vllm.py +65 -0
- package/ciel_runtime_support/providers/zai.py +119 -0
- package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
- package/ciel_runtime_support/rate_limit_policy.py +117 -0
- package/ciel_runtime_support/rate_limit_repository.py +154 -0
- package/ciel_runtime_support/registry.py +46 -0
- package/ciel_runtime_support/request_shortcuts.py +253 -0
- package/ciel_runtime_support/request_trace.py +323 -0
- package/ciel_runtime_support/response_collection.py +209 -0
- package/ciel_runtime_support/router_access.py +238 -0
- package/ciel_runtime_support/router_client_lifecycle.py +366 -0
- package/ciel_runtime_support/router_health_policy.py +101 -0
- package/ciel_runtime_support/router_http.py +513 -0
- package/ciel_runtime_support/router_process_lifecycle.py +401 -0
- package/ciel_runtime_support/router_rate_limit_service.py +285 -0
- package/ciel_runtime_support/router_server_runtime.py +103 -0
- package/ciel_runtime_support/router_shortcuts.py +201 -0
- package/ciel_runtime_support/routing_fallback.py +73 -0
- package/ciel_runtime_support/runtime_activity_repository.py +143 -0
- package/ciel_runtime_support/runtime_adapters.py +104 -0
- package/ciel_runtime_support/runtime_command_factory.py +73 -0
- package/ciel_runtime_support/runtime_compatibility.py +50 -0
- package/ciel_runtime_support/runtime_constants.py +178 -0
- package/ciel_runtime_support/runtime_launch.py +1602 -0
- package/ciel_runtime_support/runtime_llm_options.py +312 -0
- package/ciel_runtime_support/runtime_logging.py +161 -0
- package/ciel_runtime_support/runtime_paths.py +157 -0
- package/ciel_runtime_support/runtime_restart.py +84 -0
- package/ciel_runtime_support/runtime_upgrade.py +149 -0
- package/ciel_runtime_support/secure_json_repository.py +55 -0
- package/ciel_runtime_support/session_import.py +356 -0
- package/ciel_runtime_support/settings_repository.py +8 -0
- package/ciel_runtime_support/slash_command_assets.py +211 -0
- package/ciel_runtime_support/sse_stream.py +57 -0
- package/ciel_runtime_support/sse_trace.py +225 -0
- package/ciel_runtime_support/statusline_script.py +593 -0
- package/ciel_runtime_support/statusline_settings.py +53 -0
- package/ciel_runtime_support/stream_chunk_policy.py +18 -0
- package/ciel_runtime_support/streaming_anthropic.py +1955 -0
- package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
- package/ciel_runtime_support/terminal_platform_io.py +127 -0
- package/ciel_runtime_support/timeout_profile.py +196 -0
- package/ciel_runtime_support/tool_dialects.py +85 -0
- package/ciel_runtime_support/tool_exposure_policy.py +63 -0
- package/ciel_runtime_support/tool_guard_hooks.py +218 -0
- package/ciel_runtime_support/tool_request_projection.py +96 -0
- package/ciel_runtime_support/tool_schema.py +483 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
- package/ciel_runtime_support/ui_text.py +266 -0
- package/ciel_runtime_support/upstream_error_policy.py +104 -0
- package/ciel_runtime_support/upstream_retry.py +419 -0
- package/ciel_runtime_support/upstream_stream_io.py +106 -0
- package/ciel_runtime_support/usage_events.py +96 -0
- package/ciel_runtime_support/visible_stream_filters.py +130 -0
- package/ciel_runtime_support/web_endpoints.py +447 -0
- package/ciel_runtime_support/web_ui.py +915 -0
- package/ciel_runtime_support/web_ui_controller.py +189 -0
- package/ciel_runtime_support/windows_console_input.py +137 -0
- package/ciel_runtime_support/windows_console_mode.py +112 -0
- package/docs/Architecture.md +54 -0
- package/docs/Configuration.md +17 -0
- package/docs/Module-Map.md +1093 -26
- package/docs/Providers.md +46 -1
- package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
- package/npm-bin/run-ciel-runtime.js +22 -2
- package/package.json +9 -2
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Codex child-process records, discovery, and shutdown lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class CodexProcessPorts:
|
|
16
|
+
pid_running: Callable[[int], bool]
|
|
17
|
+
command_line: Callable[[int], str]
|
|
18
|
+
managed_process: Callable[[int, str | None], bool]
|
|
19
|
+
terminate_tree: Callable[[int, str, bool], bool]
|
|
20
|
+
process_rows: Callable[[], list[tuple[int, str, str]]]
|
|
21
|
+
process_cwd: Callable[[int], Path | None]
|
|
22
|
+
parent_info: Callable[[int], tuple[int, str] | None]
|
|
23
|
+
log: Callable[[str, str], None]
|
|
24
|
+
current_pid: Callable[[], int]
|
|
25
|
+
parent_pid: Callable[[], int]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CodexProcessRepository:
|
|
29
|
+
def __init__(self, process_dir: Path, log: Callable[[str, str], None]) -> None:
|
|
30
|
+
self.process_dir = process_dir
|
|
31
|
+
self.log = log
|
|
32
|
+
|
|
33
|
+
def write(
|
|
34
|
+
self,
|
|
35
|
+
path: Path | None,
|
|
36
|
+
pid: int,
|
|
37
|
+
command: list[str],
|
|
38
|
+
cwd: Path | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
if path is None or pid <= 0:
|
|
41
|
+
return
|
|
42
|
+
try:
|
|
43
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
payload = {
|
|
45
|
+
"pid": int(pid),
|
|
46
|
+
"owner_pid": os.getpid(),
|
|
47
|
+
"started_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
48
|
+
"cwd": str(cwd or Path.cwd()),
|
|
49
|
+
"cmd": [str(item) for item in command],
|
|
50
|
+
}
|
|
51
|
+
path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
|
|
52
|
+
try:
|
|
53
|
+
os.chmod(path, 0o600)
|
|
54
|
+
except Exception as exc:
|
|
55
|
+
self.log(
|
|
56
|
+
"WARN",
|
|
57
|
+
"codex_child_process_record_chmod_failed "
|
|
58
|
+
f"path={path} error={type(exc).__name__}: {exc}",
|
|
59
|
+
)
|
|
60
|
+
self.log("INFO", f"codex_child_process_registered pid={pid} path={path}")
|
|
61
|
+
except Exception as exc:
|
|
62
|
+
self.log(
|
|
63
|
+
"WARN",
|
|
64
|
+
f"codex_child_process_register_failed pid={pid} "
|
|
65
|
+
f"error={type(exc).__name__}: {exc}",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def release(self, path: Path | None, pid: int | None = None) -> None:
|
|
69
|
+
if path is None:
|
|
70
|
+
return
|
|
71
|
+
try:
|
|
72
|
+
if pid is not None and path.exists():
|
|
73
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
74
|
+
if int(data.get("pid") or 0) != int(pid):
|
|
75
|
+
return
|
|
76
|
+
path.unlink()
|
|
77
|
+
self.log("INFO", f"codex_child_process_released path={path}")
|
|
78
|
+
except FileNotFoundError:
|
|
79
|
+
return
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
self.log(
|
|
82
|
+
"WARN",
|
|
83
|
+
f"codex_child_process_release_failed path={path} "
|
|
84
|
+
f"error={type(exc).__name__}: {exc}",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def records(self) -> list[tuple[Path, int]]:
|
|
88
|
+
if not self.process_dir.exists():
|
|
89
|
+
return []
|
|
90
|
+
records: list[tuple[Path, int]] = []
|
|
91
|
+
for path in sorted(self.process_dir.glob("*.json")):
|
|
92
|
+
try:
|
|
93
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
94
|
+
records.append((path, int(data.get("pid") or 0)))
|
|
95
|
+
except (
|
|
96
|
+
OSError,
|
|
97
|
+
UnicodeError,
|
|
98
|
+
json.JSONDecodeError,
|
|
99
|
+
AttributeError,
|
|
100
|
+
TypeError,
|
|
101
|
+
ValueError,
|
|
102
|
+
) as exc:
|
|
103
|
+
self.log(
|
|
104
|
+
"WARN",
|
|
105
|
+
f"codex_tracked_process_record_invalid path={path} "
|
|
106
|
+
f"error={type(exc).__name__}: {exc}",
|
|
107
|
+
)
|
|
108
|
+
try:
|
|
109
|
+
path.unlink()
|
|
110
|
+
except FileNotFoundError:
|
|
111
|
+
pass
|
|
112
|
+
except OSError as unlink_exc:
|
|
113
|
+
self.log(
|
|
114
|
+
"WARN",
|
|
115
|
+
f"codex_tracked_process_record_cleanup_failed path={path} "
|
|
116
|
+
f"error={type(unlink_exc).__name__}: {unlink_exc}",
|
|
117
|
+
)
|
|
118
|
+
return records
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def managed_process(
|
|
122
|
+
pid: int,
|
|
123
|
+
command: str | None,
|
|
124
|
+
*,
|
|
125
|
+
managed_environment: Callable[[int, str, str | None], bool],
|
|
126
|
+
command_line: Callable[[int], str],
|
|
127
|
+
) -> bool:
|
|
128
|
+
if managed_environment(pid, "CIEL_RUNTIME_CODEX_MANAGED", "1"):
|
|
129
|
+
return True
|
|
130
|
+
text = (command if command is not None else command_line(pid)).lower()
|
|
131
|
+
return "codex" in text and ("--yolo" in text or "app-server" in text)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def terminate_recorded_child(
|
|
135
|
+
process: Any,
|
|
136
|
+
label: str,
|
|
137
|
+
*,
|
|
138
|
+
terminate_tree: Callable[[int, str, bool], bool],
|
|
139
|
+
log: Callable[[str, str], None],
|
|
140
|
+
) -> None:
|
|
141
|
+
try:
|
|
142
|
+
pid = int(getattr(process, "pid", 0) or 0)
|
|
143
|
+
except (TypeError, ValueError) as exc:
|
|
144
|
+
log("WARN", f"codex_child_process_pid_invalid error={type(exc).__name__}: {exc}")
|
|
145
|
+
return
|
|
146
|
+
if pid <= 0:
|
|
147
|
+
return
|
|
148
|
+
try:
|
|
149
|
+
if process.poll() is not None:
|
|
150
|
+
return
|
|
151
|
+
except Exception as exc:
|
|
152
|
+
log(
|
|
153
|
+
"WARN",
|
|
154
|
+
f"codex_child_process_poll_failed pid={pid} error={type(exc).__name__}: {exc}",
|
|
155
|
+
)
|
|
156
|
+
terminate_tree(pid, label, True)
|
|
157
|
+
try:
|
|
158
|
+
process.wait(timeout=2)
|
|
159
|
+
except Exception as wait_exc:
|
|
160
|
+
log(
|
|
161
|
+
"WARN",
|
|
162
|
+
f"codex_child_process_wait_failed pid={pid} "
|
|
163
|
+
f"error={type(wait_exc).__name__}: {wait_exc}",
|
|
164
|
+
)
|
|
165
|
+
try:
|
|
166
|
+
process.kill()
|
|
167
|
+
except Exception as kill_exc:
|
|
168
|
+
log(
|
|
169
|
+
"WARN",
|
|
170
|
+
f"codex_child_process_kill_failed pid={pid} "
|
|
171
|
+
f"error={type(kill_exc).__name__}: {kill_exc}",
|
|
172
|
+
)
|
|
173
|
+
try:
|
|
174
|
+
process.wait(timeout=2)
|
|
175
|
+
except Exception as final_wait_exc:
|
|
176
|
+
log(
|
|
177
|
+
"WARN",
|
|
178
|
+
f"codex_child_process_final_wait_failed pid={pid} "
|
|
179
|
+
f"error={type(final_wait_exc).__name__}: {final_wait_exc}",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class CodexProcessLifecycle:
|
|
184
|
+
def __init__(self, repository: CodexProcessRepository, ports: CodexProcessPorts) -> None:
|
|
185
|
+
self.repository = repository
|
|
186
|
+
self.ports = ports
|
|
187
|
+
|
|
188
|
+
def terminate_tracked(self, reason: str, quiet: bool = True) -> bool:
|
|
189
|
+
stopped = False
|
|
190
|
+
for path, pid in self.repository.records():
|
|
191
|
+
if pid <= 0 or not self.ports.pid_running(pid):
|
|
192
|
+
self.repository.release(path, pid)
|
|
193
|
+
continue
|
|
194
|
+
command = self.ports.command_line(pid)
|
|
195
|
+
if not self.ports.managed_process(pid, command):
|
|
196
|
+
self.ports.log(
|
|
197
|
+
"WARN",
|
|
198
|
+
f"codex_tracked_process_skipped_unexpected_command pid={pid} path={path}",
|
|
199
|
+
)
|
|
200
|
+
self.repository.release(path, pid)
|
|
201
|
+
continue
|
|
202
|
+
stopped = self.ports.terminate_tree(pid, "previous Codex", quiet) or stopped
|
|
203
|
+
self.repository.release(path, pid)
|
|
204
|
+
self.ports.log(
|
|
205
|
+
"INFO",
|
|
206
|
+
f"codex_tracked_processes_terminated reason={reason} "
|
|
207
|
+
f"stopped={str(stopped).lower()}",
|
|
208
|
+
)
|
|
209
|
+
return stopped
|
|
210
|
+
|
|
211
|
+
def ancestor_pids(self, platform_name: str, limit: int = 12) -> set[int]:
|
|
212
|
+
ancestors = {self.ports.current_pid(), self.ports.parent_pid()}
|
|
213
|
+
if platform_name == "nt":
|
|
214
|
+
return ancestors
|
|
215
|
+
current = self.ports.current_pid()
|
|
216
|
+
for _ in range(max(0, limit)):
|
|
217
|
+
info = self.ports.parent_info(current)
|
|
218
|
+
if info is None:
|
|
219
|
+
break
|
|
220
|
+
parent, _command = info
|
|
221
|
+
if parent <= 0 or parent in ancestors:
|
|
222
|
+
break
|
|
223
|
+
ancestors.add(parent)
|
|
224
|
+
current = parent
|
|
225
|
+
return ancestors
|
|
226
|
+
|
|
227
|
+
def untracked_pids(
|
|
228
|
+
self,
|
|
229
|
+
cwd: Path | None,
|
|
230
|
+
*,
|
|
231
|
+
platform_name: str,
|
|
232
|
+
enabled: bool,
|
|
233
|
+
) -> list[int]:
|
|
234
|
+
if platform_name == "nt" or not enabled:
|
|
235
|
+
return []
|
|
236
|
+
target_cwd = (cwd or Path.cwd()).resolve()
|
|
237
|
+
protected = self.ancestor_pids(platform_name)
|
|
238
|
+
matches = [
|
|
239
|
+
pid
|
|
240
|
+
for pid, stat, command in self.ports.process_rows()
|
|
241
|
+
if pid not in protected
|
|
242
|
+
and not stat.startswith("Z")
|
|
243
|
+
and self.ports.managed_process(pid, command)
|
|
244
|
+
and self.ports.process_cwd(pid) == target_cwd
|
|
245
|
+
]
|
|
246
|
+
return sorted(set(matches))
|
|
247
|
+
|
|
248
|
+
def terminate_untracked(
|
|
249
|
+
self,
|
|
250
|
+
reason: str,
|
|
251
|
+
cwd: Path | None,
|
|
252
|
+
quiet: bool,
|
|
253
|
+
*,
|
|
254
|
+
platform_name: str,
|
|
255
|
+
enabled: bool,
|
|
256
|
+
) -> bool:
|
|
257
|
+
pids = self.untracked_pids(cwd, platform_name=platform_name, enabled=enabled)
|
|
258
|
+
if not pids:
|
|
259
|
+
return False
|
|
260
|
+
stopped = False
|
|
261
|
+
for pid in pids:
|
|
262
|
+
stopped = (
|
|
263
|
+
self.ports.terminate_tree(pid, "previous untracked Codex", quiet)
|
|
264
|
+
or stopped
|
|
265
|
+
)
|
|
266
|
+
self.ports.log(
|
|
267
|
+
"WARN",
|
|
268
|
+
f"codex_untracked_processes_terminated reason={reason} "
|
|
269
|
+
f"pids={','.join(map(str, pids))} stopped={str(stopped).lower()}",
|
|
270
|
+
)
|
|
271
|
+
return stopped
|
|
@@ -2,11 +2,149 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
5
7
|
from typing import Any, Callable
|
|
6
8
|
|
|
7
9
|
from .agent_router import COMMON_RUNTIME_ROUTER_CAPABILITIES, RouterCapability
|
|
8
10
|
|
|
9
11
|
|
|
12
|
+
CODEX_CAPACITY_ERROR_CODES = frozenset({"server_is_overloaded", "slow_down"})
|
|
13
|
+
CODEX_RESPONSE_PREAMBLE_LIMIT = 256 * 1024
|
|
14
|
+
_CODEX_NON_OUTPUT_EVENT_TYPES = frozenset({"response.created", "response.in_progress"})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class CodexResponsePreamble:
|
|
19
|
+
payload: bytes
|
|
20
|
+
capacity_error_code: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class CodexChannelContextPorts:
|
|
25
|
+
to_anthropic: Callable[[dict[str, Any], str], dict[str, Any]]
|
|
26
|
+
inject_pending: Callable[[dict[str, Any]], dict[str, Any]]
|
|
27
|
+
inject_tool_results: Callable[[dict[str, Any]], dict[str, Any]]
|
|
28
|
+
content_to_text: Callable[[Any], str]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CodexChannelContextProjector:
|
|
32
|
+
def __init__(self, ports: CodexChannelContextPorts) -> None:
|
|
33
|
+
self._ports = ports
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def input_items(value: Any) -> list[dict[str, Any]]:
|
|
37
|
+
if isinstance(value, str):
|
|
38
|
+
return [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": value}]}]
|
|
39
|
+
if isinstance(value, dict):
|
|
40
|
+
return [dict(value)]
|
|
41
|
+
if isinstance(value, list):
|
|
42
|
+
return [dict(item) for item in value if isinstance(item, dict)]
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def message_item(role: str, text: str) -> dict[str, Any]:
|
|
47
|
+
role = role if role in {"user", "assistant", "system", "developer"} else "user"
|
|
48
|
+
text_type = "output_text" if role == "assistant" else "input_text"
|
|
49
|
+
return {"type": "message", "role": role, "content": [{"type": text_type, "text": text}]}
|
|
50
|
+
|
|
51
|
+
def project(self, body: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
52
|
+
delivery = self._ports.to_anthropic(body, str(body.get("model") or ""))
|
|
53
|
+
original_count = len(delivery.get("messages") or [])
|
|
54
|
+
delivery = self._ports.inject_pending(delivery)
|
|
55
|
+
delivery = self._ports.inject_tool_results(delivery)
|
|
56
|
+
messages = [message for message in delivery.get("messages") or [] if isinstance(message, dict)]
|
|
57
|
+
additions = messages[original_count:]
|
|
58
|
+
metadata = delivery.get("metadata") if isinstance(delivery.get("metadata"), dict) else {}
|
|
59
|
+
projected = dict(body)
|
|
60
|
+
projected.pop("metadata", None)
|
|
61
|
+
if not additions and not metadata:
|
|
62
|
+
return projected, delivery
|
|
63
|
+
input_items = self.input_items(body.get("input", []))
|
|
64
|
+
for message in additions:
|
|
65
|
+
text = self._ports.content_to_text(message.get("content"))
|
|
66
|
+
if text.strip():
|
|
67
|
+
input_items.append(self.message_item(str(message.get("role") or "user"), text))
|
|
68
|
+
if input_items:
|
|
69
|
+
projected["input"] = input_items
|
|
70
|
+
return projected, delivery
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _codex_sse_event(block: bytes) -> dict[str, Any] | None:
|
|
74
|
+
text = block.decode("utf-8", errors="replace").strip()
|
|
75
|
+
if not text:
|
|
76
|
+
return None
|
|
77
|
+
data_lines = []
|
|
78
|
+
for line in text.splitlines():
|
|
79
|
+
if line.startswith("data:"):
|
|
80
|
+
data_lines.append(line[5:].lstrip())
|
|
81
|
+
candidate = "\n".join(data_lines).strip() if data_lines else text
|
|
82
|
+
try:
|
|
83
|
+
value = json.loads(candidate)
|
|
84
|
+
except (TypeError, ValueError):
|
|
85
|
+
return None
|
|
86
|
+
return value if isinstance(value, dict) else None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _codex_capacity_error_code(event: dict[str, Any]) -> str | None:
|
|
90
|
+
if event.get("type") != "response.failed":
|
|
91
|
+
return None
|
|
92
|
+
response = event.get("response")
|
|
93
|
+
if not isinstance(response, dict):
|
|
94
|
+
return None
|
|
95
|
+
error = response.get("error")
|
|
96
|
+
if not isinstance(error, dict):
|
|
97
|
+
return None
|
|
98
|
+
code = str(error.get("code") or "").strip()
|
|
99
|
+
return code if code in CODEX_CAPACITY_ERROR_CODES else None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def read_codex_response_preamble(
|
|
103
|
+
stream: Any,
|
|
104
|
+
*,
|
|
105
|
+
max_bytes: int = CODEX_RESPONSE_PREAMBLE_LIMIT,
|
|
106
|
+
) -> CodexResponsePreamble:
|
|
107
|
+
"""Read control-only SSE events until output starts or a safe retry is known."""
|
|
108
|
+
buffered = bytearray()
|
|
109
|
+
while len(buffered) < max_bytes:
|
|
110
|
+
block = bytearray()
|
|
111
|
+
reached_eof = False
|
|
112
|
+
while len(buffered) + len(block) < max_bytes:
|
|
113
|
+
remaining = max_bytes - len(buffered) - len(block)
|
|
114
|
+
line = stream.readline(remaining + 1)
|
|
115
|
+
if not line:
|
|
116
|
+
reached_eof = True
|
|
117
|
+
break
|
|
118
|
+
block.extend(line)
|
|
119
|
+
if line in (b"\n", b"\r\n"):
|
|
120
|
+
break
|
|
121
|
+
if len(block) > remaining:
|
|
122
|
+
break
|
|
123
|
+
if not block:
|
|
124
|
+
return CodexResponsePreamble(bytes(buffered))
|
|
125
|
+
buffered.extend(block)
|
|
126
|
+
if len(buffered) >= max_bytes:
|
|
127
|
+
return CodexResponsePreamble(bytes(buffered))
|
|
128
|
+
|
|
129
|
+
event = _codex_sse_event(bytes(block))
|
|
130
|
+
if event is None:
|
|
131
|
+
# SSE comments and keepalives carry no model output.
|
|
132
|
+
meaningful = [line for line in block.splitlines() if line and not line.startswith(b":")]
|
|
133
|
+
if not meaningful and not reached_eof:
|
|
134
|
+
continue
|
|
135
|
+
return CodexResponsePreamble(bytes(buffered))
|
|
136
|
+
|
|
137
|
+
capacity_code = _codex_capacity_error_code(event)
|
|
138
|
+
if capacity_code:
|
|
139
|
+
return CodexResponsePreamble(bytes(buffered), capacity_code)
|
|
140
|
+
if event.get("type") not in _CODEX_NON_OUTPUT_EVENT_TYPES:
|
|
141
|
+
return CodexResponsePreamble(bytes(buffered))
|
|
142
|
+
if reached_eof:
|
|
143
|
+
return CodexResponsePreamble(bytes(buffered))
|
|
144
|
+
|
|
145
|
+
return CodexResponsePreamble(bytes(buffered))
|
|
146
|
+
|
|
147
|
+
|
|
10
148
|
class CodexRouter:
|
|
11
149
|
name = "codex"
|
|
12
150
|
runtime = "codex"
|
|
@@ -21,6 +159,7 @@ class CodexRouter:
|
|
|
21
159
|
("pending_delivery_ack", "Injected channel cursors are committed after successful delivery."),
|
|
22
160
|
("request_observability", "Responses requests are traced and published to the runtime event bus."),
|
|
23
161
|
("upstream_error_mapping", "Upstream HTTP and client disconnect errors are mapped for Codex."),
|
|
162
|
+
("capacity_retry", "Capacity-only failures are retried before any output or tool call is delivered."),
|
|
24
163
|
("backend_passthrough", "Non-responses Codex backend endpoints are passed through."),
|
|
25
164
|
("legacy_responses", "The legacy /v1/responses path remains supported."),
|
|
26
165
|
)
|
|
@@ -77,4 +216,11 @@ class CodexRouter:
|
|
|
77
216
|
assert all(any(capability.name == required for capability in CodexRouter.capabilities) for required in COMMON_RUNTIME_ROUTER_CAPABILITIES)
|
|
78
217
|
|
|
79
218
|
|
|
80
|
-
__all__ = [
|
|
219
|
+
__all__ = [
|
|
220
|
+
"CODEX_CAPACITY_ERROR_CODES",
|
|
221
|
+
"CodexChannelContextPorts",
|
|
222
|
+
"CodexChannelContextProjector",
|
|
223
|
+
"CodexResponsePreamble",
|
|
224
|
+
"CodexRouter",
|
|
225
|
+
"read_codex_response_preamble",
|
|
226
|
+
]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Read-only repository for the Codex local resume index."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sqlite3
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ciel_runtime_support.codex_config import (
|
|
14
|
+
codex_config_paths_for_launch,
|
|
15
|
+
toml_scalar_without_comment,
|
|
16
|
+
unquote_toml_string,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def codex_sqlite_home(
|
|
21
|
+
passthrough: list[str] | None = None,
|
|
22
|
+
env: dict[str, str] | None = None,
|
|
23
|
+
cwd: Path | None = None,
|
|
24
|
+
) -> Path:
|
|
25
|
+
launch_env = env or os.environ
|
|
26
|
+
launch_cwd = (cwd or Path.cwd()).resolve()
|
|
27
|
+
configured: str | None = None
|
|
28
|
+
for path in codex_config_paths_for_launch(
|
|
29
|
+
passthrough or [], env=launch_env, cwd=launch_cwd
|
|
30
|
+
):
|
|
31
|
+
try:
|
|
32
|
+
text = path.read_text(encoding="utf-8")
|
|
33
|
+
except OSError:
|
|
34
|
+
continue
|
|
35
|
+
table = ""
|
|
36
|
+
for line in text.splitlines():
|
|
37
|
+
stripped = toml_scalar_without_comment(line)
|
|
38
|
+
if not stripped:
|
|
39
|
+
continue
|
|
40
|
+
table_match = re.fullmatch(r"\[(.+)\]", stripped)
|
|
41
|
+
if table_match:
|
|
42
|
+
table = table_match.group(1).strip()
|
|
43
|
+
continue
|
|
44
|
+
if table:
|
|
45
|
+
continue
|
|
46
|
+
match = re.match(r"sqlite_home\s*=\s*(.+)$", stripped)
|
|
47
|
+
if match:
|
|
48
|
+
configured = unquote_toml_string(match.group(1))
|
|
49
|
+
raw = configured or str(launch_env.get("CODEX_SQLITE_HOME") or "").strip()
|
|
50
|
+
if raw:
|
|
51
|
+
path = Path(os.path.expandvars(raw)).expanduser()
|
|
52
|
+
return path if path.is_absolute() else launch_cwd / path
|
|
53
|
+
return Path(launch_env.get("CODEX_HOME") or (Path.home() / ".codex")).expanduser()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CodexSessionRepository:
|
|
57
|
+
def __init__(self, database: Path, log: Callable[[str, str], None]) -> None:
|
|
58
|
+
self.database = database
|
|
59
|
+
self.log = log
|
|
60
|
+
|
|
61
|
+
def resumable(
|
|
62
|
+
self,
|
|
63
|
+
limit: int = 200,
|
|
64
|
+
*,
|
|
65
|
+
include_non_interactive: bool = False,
|
|
66
|
+
cwd: Path | None = None,
|
|
67
|
+
) -> list[dict[str, Any]]:
|
|
68
|
+
if not self.database.is_file():
|
|
69
|
+
return []
|
|
70
|
+
sources = ["cli", "vscode"]
|
|
71
|
+
if include_non_interactive:
|
|
72
|
+
sources.extend(["exec", "app-server"])
|
|
73
|
+
placeholders = ", ".join("?" for _ in sources)
|
|
74
|
+
where = f"archived = 0 AND source IN ({placeholders})"
|
|
75
|
+
parameters: list[Any] = list(sources)
|
|
76
|
+
if cwd is not None:
|
|
77
|
+
normalized_cwd = str(cwd.resolve()).replace("\\", "/").rstrip("/")
|
|
78
|
+
slash_cwd = "REPLACE(cwd, CHAR(92), '/')"
|
|
79
|
+
portable_cwd = (
|
|
80
|
+
f"CASE WHEN {slash_cwd} LIKE '//?/UNC/%' "
|
|
81
|
+
f"THEN '//' || SUBSTR({slash_cwd}, 9) "
|
|
82
|
+
f"WHEN {slash_cwd} LIKE '//?/%' THEN SUBSTR({slash_cwd}, 5) "
|
|
83
|
+
f"ELSE {slash_cwd} END"
|
|
84
|
+
)
|
|
85
|
+
cwd_expression = f"RTRIM(({portable_cwd}), '/')"
|
|
86
|
+
if os.name == "nt":
|
|
87
|
+
cwd_expression = f"LOWER({cwd_expression})"
|
|
88
|
+
normalized_cwd = normalized_cwd.lower()
|
|
89
|
+
where += f" AND {cwd_expression} = ?"
|
|
90
|
+
parameters.append(normalized_cwd)
|
|
91
|
+
uri = self.database.resolve().as_uri() + "?mode=ro"
|
|
92
|
+
try:
|
|
93
|
+
with contextlib.closing(
|
|
94
|
+
sqlite3.connect(uri, uri=True, timeout=1.0)
|
|
95
|
+
) as connection:
|
|
96
|
+
connection.row_factory = sqlite3.Row
|
|
97
|
+
rows = connection.execute(
|
|
98
|
+
f"""
|
|
99
|
+
SELECT id, title, first_user_message, cwd, model_provider,
|
|
100
|
+
COALESCE(NULLIF(updated_at_ms, 0), updated_at * 1000) AS activity_ms
|
|
101
|
+
FROM threads
|
|
102
|
+
WHERE {where}
|
|
103
|
+
ORDER BY COALESCE(NULLIF(recency_at_ms, 0),
|
|
104
|
+
NULLIF(updated_at_ms, 0), updated_at * 1000) DESC
|
|
105
|
+
LIMIT ?
|
|
106
|
+
""",
|
|
107
|
+
(*parameters, max(1, min(1000, int(limit)))),
|
|
108
|
+
).fetchall()
|
|
109
|
+
except (OSError, sqlite3.Error) as exc:
|
|
110
|
+
self.log(
|
|
111
|
+
"WARN",
|
|
112
|
+
f"codex_resume_index_read_failed error={type(exc).__name__}: {exc}",
|
|
113
|
+
)
|
|
114
|
+
return []
|
|
115
|
+
return [dict(row) for row in rows]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Codex resume-session query, presentation, and selection workflow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class CodexSessionRepositoryPorts:
|
|
15
|
+
sqlite_home: Callable[..., Path]
|
|
16
|
+
resumable: Callable[[Path, int, bool, Path | None], list[dict[str, Any]]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class CodexSessionPresentationPorts:
|
|
21
|
+
select: Callable[..., int | None]
|
|
22
|
+
compact_text: Callable[[str, int], str]
|
|
23
|
+
output: Callable[[str], None]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class CodexSessionSelectionService:
|
|
28
|
+
repository: CodexSessionRepositoryPorts
|
|
29
|
+
presentation: CodexSessionPresentationPorts
|
|
30
|
+
|
|
31
|
+
def sqlite_home_for_launch(
|
|
32
|
+
self,
|
|
33
|
+
passthrough: list[str] | None = None,
|
|
34
|
+
env: dict[str, str] | None = None,
|
|
35
|
+
cwd: Path | None = None,
|
|
36
|
+
) -> Path:
|
|
37
|
+
return self.repository.sqlite_home(passthrough, env=env, cwd=cwd)
|
|
38
|
+
|
|
39
|
+
def local_resume_sessions(
|
|
40
|
+
self,
|
|
41
|
+
env: dict[str, str] | None = None,
|
|
42
|
+
limit: int = 200,
|
|
43
|
+
include_non_interactive: bool = False,
|
|
44
|
+
passthrough: list[str] | None = None,
|
|
45
|
+
cwd: Path | None = None,
|
|
46
|
+
session_cwd: Path | None = None,
|
|
47
|
+
) -> list[dict[str, Any]]:
|
|
48
|
+
database = (
|
|
49
|
+
self.sqlite_home_for_launch(passthrough, env=env, cwd=cwd)
|
|
50
|
+
/ "state_5.sqlite"
|
|
51
|
+
)
|
|
52
|
+
return self.repository.resumable(
|
|
53
|
+
database,
|
|
54
|
+
limit,
|
|
55
|
+
include_non_interactive,
|
|
56
|
+
session_cwd,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def resume_session_row(self, session: dict[str, Any]) -> str:
|
|
60
|
+
title = str(
|
|
61
|
+
session.get("title")
|
|
62
|
+
or session.get("first_user_message")
|
|
63
|
+
or "Untitled session"
|
|
64
|
+
).strip()
|
|
65
|
+
title = re.sub(r"\s+", " ", title)
|
|
66
|
+
cwd = str(session.get("cwd") or "").strip()
|
|
67
|
+
folder = Path(cwd).name if cwd else "-"
|
|
68
|
+
provider = str(session.get("model_provider") or "-").strip()
|
|
69
|
+
try:
|
|
70
|
+
activity = datetime.fromtimestamp(
|
|
71
|
+
int(session.get("activity_ms") or 0) / 1000
|
|
72
|
+
).strftime("%Y-%m-%d %H:%M")
|
|
73
|
+
except (OSError, OverflowError, TypeError, ValueError):
|
|
74
|
+
activity = "unknown time"
|
|
75
|
+
title = self.presentation.compact_text(title, 66)
|
|
76
|
+
return f"{title} [{folder} | {provider} | {activity}]"
|
|
77
|
+
|
|
78
|
+
def select_resume_session(
|
|
79
|
+
self,
|
|
80
|
+
env: dict[str, str] | None = None,
|
|
81
|
+
include_non_interactive: bool = False,
|
|
82
|
+
passthrough: list[str] | None = None,
|
|
83
|
+
cwd: Path | None = None,
|
|
84
|
+
) -> str | None:
|
|
85
|
+
launch_cwd = (cwd or Path.cwd()).resolve()
|
|
86
|
+
show_all = "--all" in (passthrough or [])
|
|
87
|
+
sessions = self.local_resume_sessions(
|
|
88
|
+
env,
|
|
89
|
+
include_non_interactive=include_non_interactive,
|
|
90
|
+
passthrough=passthrough,
|
|
91
|
+
cwd=launch_cwd,
|
|
92
|
+
session_cwd=None if show_all else launch_cwd,
|
|
93
|
+
)
|
|
94
|
+
if not sessions:
|
|
95
|
+
database = (
|
|
96
|
+
self.sqlite_home_for_launch(
|
|
97
|
+
passthrough, env=env, cwd=launch_cwd
|
|
98
|
+
) / "state_5.sqlite"
|
|
99
|
+
)
|
|
100
|
+
scope = "" if show_all else f" for the current directory ({launch_cwd})"
|
|
101
|
+
hint = "" if show_all else " Use `codex resume --all` to show every session."
|
|
102
|
+
self.presentation.output(
|
|
103
|
+
f"Ciel Runtime could not find resumable Codex sessions{scope} in: "
|
|
104
|
+
f"{database}.{hint}"
|
|
105
|
+
)
|
|
106
|
+
return None
|
|
107
|
+
selected = self.presentation.select(
|
|
108
|
+
"Resume Codex session",
|
|
109
|
+
[self.resume_session_row(session) for session in sessions],
|
|
110
|
+
footer="Up/Down moves. Enter resumes. Esc/q cancels.",
|
|
111
|
+
)
|
|
112
|
+
if selected is None:
|
|
113
|
+
return ""
|
|
114
|
+
return str(sessions[selected].get("id") or "").strip()
|