@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.
- package/ciel_runtime.py +2553 -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/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 +239 -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 +82 -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/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/workspace_router_selection.py +86 -0
- package/docs/Configuration.md +50 -0
- package/docs/Test-Suite.md +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Project provider-owned Ollama request settings onto wire payloads.
|
|
2
|
+
|
|
3
|
+
The application uses several Ollama request paths (interactive forwarding,
|
|
4
|
+
Advisor, and optional LLM compaction). This policy is their single boundary
|
|
5
|
+
for optional model/runtime settings. It deliberately distinguishes required
|
|
6
|
+
protocol fields from explicit operator overrides so persisted adapter defaults
|
|
7
|
+
do not silently become upstream request parameters.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any, Callable, Mapping
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class OllamaWireProjectionPorts:
|
|
18
|
+
think_value: Callable[
|
|
19
|
+
[str, str | None, dict[str, Any], Mapping[str, Any]], bool | str | None
|
|
20
|
+
]
|
|
21
|
+
positive_int: Callable[[Any], int | None]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class OllamaWireProjection:
|
|
26
|
+
ports: OllamaWireProjectionPorts
|
|
27
|
+
legacy_keep_alive_default: str = "5m"
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def _marked_keys(config: Mapping[str, Any], name: str) -> set[str]:
|
|
31
|
+
raw = config.get(name)
|
|
32
|
+
if not isinstance(raw, (list, tuple, set)):
|
|
33
|
+
return set()
|
|
34
|
+
return {str(item) for item in raw if str(item).strip()}
|
|
35
|
+
|
|
36
|
+
def explicit_options(self, config: Mapping[str, Any]) -> dict[str, Any]:
|
|
37
|
+
raw = config.get("ollama_options")
|
|
38
|
+
if not isinstance(raw, Mapping):
|
|
39
|
+
return {}
|
|
40
|
+
allowed = self._marked_keys(
|
|
41
|
+
config, "ollama_explicit_options"
|
|
42
|
+
) | self._marked_keys(config, "ollama_transient_options")
|
|
43
|
+
return {
|
|
44
|
+
str(key): value
|
|
45
|
+
for key, value in raw.items()
|
|
46
|
+
if str(key) in allowed and value is not None
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def keep_alive(self, config: Mapping[str, Any]) -> str | None:
|
|
50
|
+
value = config.get("keep_alive")
|
|
51
|
+
if value is None or str(value).strip() == "":
|
|
52
|
+
return None
|
|
53
|
+
if config.get("keep_alive_explicit"):
|
|
54
|
+
return str(value)
|
|
55
|
+
# Before provenance markers existed, any non-default persisted value
|
|
56
|
+
# could only have come from an operator edit. Preserve that intent.
|
|
57
|
+
if str(value) != self.legacy_keep_alive_default:
|
|
58
|
+
return str(value)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def options(
|
|
62
|
+
self,
|
|
63
|
+
config: Mapping[str, Any],
|
|
64
|
+
*,
|
|
65
|
+
output_limit: int | None = None,
|
|
66
|
+
) -> dict[str, Any]:
|
|
67
|
+
options = self.explicit_options(config)
|
|
68
|
+
raw_num_ctx = config.get("num_ctx", "auto")
|
|
69
|
+
if not (
|
|
70
|
+
isinstance(raw_num_ctx, str)
|
|
71
|
+
and raw_num_ctx.strip().lower() in {"", "auto", "dynamic"}
|
|
72
|
+
):
|
|
73
|
+
num_ctx = self.ports.positive_int(raw_num_ctx)
|
|
74
|
+
if num_ctx:
|
|
75
|
+
options["num_ctx"] = num_ctx
|
|
76
|
+
|
|
77
|
+
output_is_explicit = bool(config.get("output_tokens_explicit")) or (
|
|
78
|
+
"num_predict"
|
|
79
|
+
in self._marked_keys(config, "ollama_transient_options")
|
|
80
|
+
)
|
|
81
|
+
if output_is_explicit:
|
|
82
|
+
configured = self.ports.positive_int(
|
|
83
|
+
options.get("num_predict")
|
|
84
|
+
or config.get("max_output_tokens")
|
|
85
|
+
)
|
|
86
|
+
limit = self.ports.positive_int(output_limit)
|
|
87
|
+
if configured and limit:
|
|
88
|
+
options["num_predict"] = min(configured, limit)
|
|
89
|
+
elif configured:
|
|
90
|
+
options["num_predict"] = configured
|
|
91
|
+
elif limit:
|
|
92
|
+
options["num_predict"] = limit
|
|
93
|
+
else:
|
|
94
|
+
options.pop("num_predict", None)
|
|
95
|
+
return options
|
|
96
|
+
|
|
97
|
+
def apply(
|
|
98
|
+
self,
|
|
99
|
+
request: dict[str, Any],
|
|
100
|
+
provider: str,
|
|
101
|
+
model: str | None,
|
|
102
|
+
config: dict[str, Any],
|
|
103
|
+
source_request: Mapping[str, Any] | None = None,
|
|
104
|
+
*,
|
|
105
|
+
output_limit: int | None = None,
|
|
106
|
+
) -> dict[str, Any]:
|
|
107
|
+
projected = dict(request)
|
|
108
|
+
think = self.ports.think_value(
|
|
109
|
+
provider, model, config, source_request or {}
|
|
110
|
+
)
|
|
111
|
+
if think is not None:
|
|
112
|
+
projected["think"] = think
|
|
113
|
+
keep_alive = self.keep_alive(config)
|
|
114
|
+
if keep_alive is not None:
|
|
115
|
+
projected["keep_alive"] = keep_alive
|
|
116
|
+
options = self.options(config, output_limit=output_limit)
|
|
117
|
+
if options:
|
|
118
|
+
projected["options"] = options
|
|
119
|
+
return projected
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True, slots=True)
|
|
123
|
+
class OllamaWireCompatibilityApi:
|
|
124
|
+
"""Stable facade adapter backed by typed projection dependencies."""
|
|
125
|
+
|
|
126
|
+
think_value: Callable[
|
|
127
|
+
[str, str | None, dict[str, Any], Mapping[str, Any]], bool | str | None
|
|
128
|
+
]
|
|
129
|
+
positive_int: Callable[[Any], int | None]
|
|
130
|
+
|
|
131
|
+
def apply(
|
|
132
|
+
self,
|
|
133
|
+
request: dict[str, Any],
|
|
134
|
+
provider: str,
|
|
135
|
+
model: str | None,
|
|
136
|
+
config: dict[str, Any],
|
|
137
|
+
source_request: Mapping[str, Any] | None = None,
|
|
138
|
+
*,
|
|
139
|
+
output_limit: int | None = None,
|
|
140
|
+
) -> dict[str, Any]:
|
|
141
|
+
return OllamaWireProjection(
|
|
142
|
+
OllamaWireProjectionPorts(self.think_value, self.positive_int)
|
|
143
|
+
).apply(
|
|
144
|
+
request,
|
|
145
|
+
provider,
|
|
146
|
+
model,
|
|
147
|
+
config,
|
|
148
|
+
source_request,
|
|
149
|
+
output_limit=output_limit,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
__all__ = [
|
|
154
|
+
"OllamaWireCompatibilityApi",
|
|
155
|
+
"OllamaWireProjection",
|
|
156
|
+
"OllamaWireProjectionPorts",
|
|
157
|
+
]
|
|
@@ -73,6 +73,7 @@ class OpenAIForwardServices:
|
|
|
73
73
|
advisor: OpenAIForwardAdvisor
|
|
74
74
|
streaming: OpenAIForwardStreaming
|
|
75
75
|
response: OpenAIForwardResponse
|
|
76
|
+
hosted_tools: Any
|
|
76
77
|
log: Callable[[str, str], Any]
|
|
77
78
|
|
|
78
79
|
|
|
@@ -100,10 +101,19 @@ def forward_openai_compatible_chat(
|
|
|
100
101
|
original_body = body
|
|
101
102
|
upstream_body = request.body_with_advisor_tool(body, pcfg) if request.advisor_provider_supported(provider) else body
|
|
102
103
|
url = request.join_url(request.upstream_request_base(provider, pcfg), "/v1/chat/completions")
|
|
104
|
+
timeout = rate_limit.request_timeout_seconds(pcfg)
|
|
105
|
+
headers = request.provider_headers(provider, pcfg, handler.headers, "openai_chat")
|
|
103
106
|
waited, rpm_used, rpm_limit = rate_limit.apply(provider, pcfg, model)
|
|
104
107
|
compatibility_test = str(handler.headers.get(policy.compatibility_test_header) or "").strip().lower() in ("1", "true", "yes", "on")
|
|
105
108
|
stream_enabled = bool(pcfg.get("stream_enabled", True))
|
|
106
109
|
stream = policy.provider_requires_streaming(provider, pcfg) or (bool(body.get("stream", stream_enabled)) and stream_enabled)
|
|
110
|
+
collected_request = request.build_chat_request(provider, model, upstream_body, pcfg, stream=False)
|
|
111
|
+
collected_request, hosted_state = services.hosted_tools.prepare(
|
|
112
|
+
provider, pcfg, collected_request, headers, timeout
|
|
113
|
+
)
|
|
114
|
+
if hosted_state.enabled and stream:
|
|
115
|
+
stream = False
|
|
116
|
+
services.log("INFO", f"provider-hosted tools enabled for {provider}; collecting tool rounds internally")
|
|
107
117
|
if stream and advisor.model_enabled(pcfg) and request.advisor_provider_supported(provider):
|
|
108
118
|
stream = False
|
|
109
119
|
services.log("INFO", f"advisor tool enabled for {provider}; collecting this turn so advisor tool calls can be resolved internally")
|
|
@@ -128,10 +138,8 @@ def forward_openai_compatible_chat(
|
|
|
128
138
|
upstream_response = streaming.open_with_retry(
|
|
129
139
|
url,
|
|
130
140
|
req_body,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
),
|
|
134
|
-
rate_limit.request_timeout_seconds(pcfg),
|
|
141
|
+
headers,
|
|
142
|
+
timeout,
|
|
135
143
|
provider,
|
|
136
144
|
pcfg,
|
|
137
145
|
model,
|
|
@@ -151,7 +159,6 @@ def forward_openai_compatible_chat(
|
|
|
151
159
|
)
|
|
152
160
|
if stream_ok:
|
|
153
161
|
response.mark_delivery_success(handler, "openai_stream_message_stop")
|
|
154
|
-
response.write_activity("success", provider, model, tokens=req_tokens, bytes=req_bytes, stream=True)
|
|
155
162
|
else:
|
|
156
163
|
response.mark_delivery_failed(handler, "openai_stream_error")
|
|
157
164
|
except RuntimeError as exc:
|
|
@@ -168,21 +175,36 @@ def forward_openai_compatible_chat(
|
|
|
168
175
|
return
|
|
169
176
|
return
|
|
170
177
|
|
|
171
|
-
req_body =
|
|
178
|
+
req_body = collected_request
|
|
172
179
|
try:
|
|
173
180
|
data = streaming.post_json_with_retry(
|
|
174
181
|
url,
|
|
175
182
|
req_body,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
),
|
|
179
|
-
rate_limit.request_timeout_seconds(pcfg),
|
|
183
|
+
headers,
|
|
184
|
+
timeout,
|
|
180
185
|
provider,
|
|
181
186
|
pcfg,
|
|
182
187
|
model,
|
|
183
188
|
None,
|
|
184
189
|
retry_rate_limits=not compatibility_test,
|
|
185
190
|
)
|
|
191
|
+
data = services.hosted_tools.resolve(
|
|
192
|
+
hosted_state,
|
|
193
|
+
req_body,
|
|
194
|
+
data,
|
|
195
|
+
lambda next_body: streaming.post_json_with_retry(
|
|
196
|
+
url,
|
|
197
|
+
next_body,
|
|
198
|
+
headers,
|
|
199
|
+
timeout,
|
|
200
|
+
provider,
|
|
201
|
+
pcfg,
|
|
202
|
+
model,
|
|
203
|
+
None,
|
|
204
|
+
retry_rate_limits=not compatibility_test,
|
|
205
|
+
),
|
|
206
|
+
timeout,
|
|
207
|
+
)
|
|
186
208
|
except RuntimeError as exc:
|
|
187
209
|
response.write_json(handler, {"type": "error", "error": {"type": "upstream_error", "message": str(exc)}}, 500)
|
|
188
210
|
return
|
|
@@ -41,6 +41,10 @@ class OpenAIResponsesRouting:
|
|
|
41
41
|
dump_request: Callable[..., Any]
|
|
42
42
|
normalize_provider_wire: Callable[..., dict[str, Any]]
|
|
43
43
|
collect_message: Callable[..., dict[str, Any]]
|
|
44
|
+
# Both belong here rather than with the conversions: each one's behaviour is
|
|
45
|
+
# decided by the routed-vs-native split this group already owns.
|
|
46
|
+
apply_codex_compat_instructions: Callable[..., dict[str, Any]]
|
|
47
|
+
recover_preamble_only_turn: Callable[..., dict[str, Any]]
|
|
44
48
|
|
|
45
49
|
|
|
46
50
|
@dataclass(frozen=True, slots=True)
|
|
@@ -82,6 +86,11 @@ def handle_openai_responses_request(
|
|
|
82
86
|
routing = services.routing
|
|
83
87
|
delivery = services.delivery
|
|
84
88
|
output = services.output
|
|
89
|
+
# Codex cannot receive --append-system-prompt, so the routed compatibility
|
|
90
|
+
# instruction has to ride along in the request itself. Do this before the
|
|
91
|
+
# conversion so it reaches both the translated path and a native Responses
|
|
92
|
+
# provider; the native Codex backend is excluded inside the port.
|
|
93
|
+
body = routing.apply_codex_compat_instructions(cfg, provider, pcfg, body)
|
|
85
94
|
anthropic_body = conversion.to_anthropic(body, conversion.current_alias(cfg))
|
|
86
95
|
if routing.maybe_import_session(
|
|
87
96
|
handler,
|
|
@@ -146,6 +155,9 @@ def handle_openai_responses_request(
|
|
|
146
155
|
)
|
|
147
156
|
try:
|
|
148
157
|
message = routing.collect_message(handler, provider, pcfg, anthropic_body)
|
|
158
|
+
message = routing.recover_preamble_only_turn(
|
|
159
|
+
handler, provider, pcfg, anthropic_body, message
|
|
160
|
+
)
|
|
149
161
|
output.write_response(handler, message, source_body=body, stream=stream)
|
|
150
162
|
delivery.mark_success(handler, "responses_json")
|
|
151
163
|
delivery.commit(anthropic_body, handler)
|
|
@@ -3,12 +3,41 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
import ntpath
|
|
6
7
|
import os
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
import subprocess
|
|
9
10
|
from typing import Callable
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
def windows_executable_image_running(executable_name: str) -> bool:
|
|
14
|
+
"""Return whether Windows currently has the executable image loaded.
|
|
15
|
+
|
|
16
|
+
npm replaces native package binaries in place. Windows rejects that copy
|
|
17
|
+
with EBUSY while any process is using the image, so the update must be
|
|
18
|
+
deferred instead of starting an install that cannot complete.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
if os.name != "nt":
|
|
22
|
+
return False
|
|
23
|
+
image_name = ntpath.basename(executable_name)
|
|
24
|
+
if not image_name.casefold().endswith(".exe"):
|
|
25
|
+
image_name += ".exe"
|
|
26
|
+
try:
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
["tasklist", "/FI", f"IMAGENAME eq {image_name}", "/FO", "CSV", "/NH"],
|
|
29
|
+
stdout=subprocess.PIPE,
|
|
30
|
+
stderr=subprocess.DEVNULL,
|
|
31
|
+
text=True,
|
|
32
|
+
timeout=5,
|
|
33
|
+
check=False,
|
|
34
|
+
)
|
|
35
|
+
except (OSError, subprocess.SubprocessError):
|
|
36
|
+
return False
|
|
37
|
+
expected = f'"{image_name}"'.casefold()
|
|
38
|
+
return any(line.strip().casefold().startswith(expected) for line in result.stdout.splitlines())
|
|
39
|
+
|
|
40
|
+
|
|
12
41
|
@dataclass(frozen=True, slots=True)
|
|
13
42
|
class NpmPackageLifecyclePorts:
|
|
14
43
|
find_executable: Callable[[str], str | None]
|
|
@@ -98,6 +127,15 @@ class NpmPackageLifecycle:
|
|
|
98
127
|
if current and not self._ports.version_newer(latest, current):
|
|
99
128
|
self._print(f"{label} is up to date ({current}).")
|
|
100
129
|
return executable
|
|
130
|
+
if executable_name.casefold() == "codex" and windows_executable_image_running(
|
|
131
|
+
executable_name
|
|
132
|
+
):
|
|
133
|
+
self._print(
|
|
134
|
+
f"{label} update deferred: {executable_name}.exe is currently running and "
|
|
135
|
+
"Windows has locked the executable. Continuing with the current version; "
|
|
136
|
+
"the update will be retried on a later launch."
|
|
137
|
+
)
|
|
138
|
+
return executable
|
|
101
139
|
self._print(f"{label} update available: {current or 'unknown'} -> {latest}; upgrading automatically.")
|
|
102
140
|
prefix = self._ports.install_prefix()
|
|
103
141
|
if prefix is not None:
|
|
@@ -214,4 +252,5 @@ __all__ = [
|
|
|
214
252
|
"NpmPackageLifecyclePorts",
|
|
215
253
|
"SelfUpdateLifecycle",
|
|
216
254
|
"SelfUpdatePorts",
|
|
255
|
+
"windows_executable_image_running",
|
|
217
256
|
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Compose the prelaunch application service from bounded UI port groups."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from . import prelaunch
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class PrelaunchAssembly:
|
|
12
|
+
terminal: prelaunch.PrelaunchTerminal
|
|
13
|
+
config: prelaunch.PrelaunchConfig
|
|
14
|
+
launch_policy: prelaunch.PrelaunchLaunchPolicy
|
|
15
|
+
panel_rows: prelaunch.PrelaunchPanelRows
|
|
16
|
+
channel_query: prelaunch.PrelaunchChannelQuery
|
|
17
|
+
channel_commands: prelaunch.PrelaunchChannelCommands
|
|
18
|
+
mutations: prelaunch.PrelaunchMutations
|
|
19
|
+
secrets: prelaunch.PrelaunchSecrets
|
|
20
|
+
options: prelaunch.PrelaunchOptions
|
|
21
|
+
|
|
22
|
+
def services(self) -> prelaunch.PrelaunchServices:
|
|
23
|
+
return prelaunch.PrelaunchServices(
|
|
24
|
+
constants=prelaunch.build_default_prelaunch_constants(),
|
|
25
|
+
terminal=self.terminal,
|
|
26
|
+
config=self.config,
|
|
27
|
+
launch_policy=self.launch_policy,
|
|
28
|
+
panel_rows=self.panel_rows,
|
|
29
|
+
channel_query=self.channel_query,
|
|
30
|
+
channel_commands=self.channel_commands,
|
|
31
|
+
mutations=self.mutations,
|
|
32
|
+
secrets=self.secrets,
|
|
33
|
+
options=self.options,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = ["PrelaunchAssembly"]
|