@oneciel-ai/ciel-runtime 0.2.22 → 0.2.23
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/CHANGELOG.md +108 -0
- package/README.md +150 -57
- package/ciel-runtime-stop.cmd +1 -0
- package/ciel-runtime.cmd +1 -0
- package/ciel-runtimectl.cmd +1 -0
- package/ciel_runtime.py +56 -53
- package/ciel_runtime_support/anthropic_tool_turns.py +81 -0
- package/ciel_runtime_support/architecture.py +17 -0
- package/ciel_runtime_support/channel_injection.py +113 -3
- package/ciel_runtime_support/channel_message_prompt.py +15 -0
- package/ciel_runtime_support/channel_pending_injection.py +28 -4
- package/ciel_runtime_support/channel_pending_poll.py +2 -0
- package/ciel_runtime_support/channel_terminal_context.py +6 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +2 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +8 -2
- package/ciel_runtime_support/channel_wake_claim_repository.py +7 -0
- package/ciel_runtime_support/channel_wake_context.py +7 -3
- package/ciel_runtime_support/cli_parser.py +6 -0
- package/ciel_runtime_support/codex_backend_context.py +4 -0
- package/ciel_runtime_support/codex_turn_recovery.py +75 -12
- package/ciel_runtime_support/config_migrations.py +102 -10
- package/ciel_runtime_support/config_repository.py +9 -0
- package/ciel_runtime_support/event_settings_cli.py +284 -2
- package/ciel_runtime_support/openai_chat_passthrough.py +2 -0
- package/ciel_runtime_support/protocols/openai_responses.py +22 -18
- package/ciel_runtime_support/provider_adapters.py +7 -0
- package/ciel_runtime_support/provider_policy.py +10 -1
- package/ciel_runtime_support/provider_request_builder.py +9 -2
- package/ciel_runtime_support/provider_responses_passthrough.py +4 -0
- package/ciel_runtime_support/providers/__init__.py +2 -0
- package/ciel_runtime_support/providers/alibaba.py +33 -10
- package/ciel_runtime_support/providers/constants.py +1 -0
- package/ciel_runtime_support/providers/opencode.py +25 -5
- package/ciel_runtime_support/providers/opencode_go.py +3 -3
- package/ciel_runtime_support/providers/openrouter.py +76 -3
- package/ciel_runtime_support/providers/tabitoken.py +130 -0
- package/ciel_runtime_support/remote_instructions.py +20 -6
- package/ciel_runtime_support/remote_memory.py +515 -55
- package/ciel_runtime_support/responses_input_compatibility.py +14 -0
- package/ciel_runtime_support/router_http.py +5 -0
- package/ciel_runtime_support/runtime_constants.py +1 -1
- package/ciel_runtime_support/runtime_launch.py +9 -2
- package/ciel_runtime_support/runtime_paths.py +2 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +7 -4
- package/ciel_runtime_support/transcript_delta_delivery.py +333 -0
- package/ciel_runtime_support/usage_events.py +45 -3
- package/ciel_runtime_support/usage_service.py +871 -0
- package/ciel_runtime_support/windows_conpty.py +108 -2
- package/ciel_runtime_support/workspace_mcp.py +4 -0
- package/docs/CLI-Reference.md +11 -0
- package/docs/Configuration.md +7 -2
- package/docs/MCP-Channels.md +7 -3
- package/docs/Module-Map.md +4 -0
- package/docs/Providers.md +32 -0
- package/docs/Remote-Memory.md +29 -10
- package/docs/Test-Suite.md +1 -0
- package/docs/journal/2026/08/20/kevin-wake-orphan-tool-repair.md +161 -0
- package/docs/journal/2026/08/20/nightly-push-local-deployment.md +59 -2
- package/docs/journal/2026/08/20/wing-remote-memory-replication-verification.md +107 -0
- package/docs/journal/2026/08/21/alibaba-singapore-qwen38-official-alignment.md +64 -0
- package/docs/journal/2026/08/21/codex-visible-sse-wake-submit-regression.md +85 -0
- package/docs/journal/2026/08/21/external-sse-wake-only-change-analysis.md +131 -0
- package/docs/journal/2026/08/21/hyundai-invalid-replayed-tool-name-repair.okf +88 -0
- package/docs/journal/2026/08/21/local-stale-temporary-runtime-pin-repair.okf +118 -0
- package/docs/journal/2026/08/21/transcript-compaction-external-events.okf +131 -0
- package/docs/journal/2026/08/21/wing-ai-net-mcp-boundary-diagnosis.okf +129 -0
- package/docs/journal/2026/08/21/wing-cold-start-wake-submit-repair.okf +161 -0
- package/docs/journal/2026/08/21/wing-powershell-memory-runaway.okf +101 -0
- package/docs/journal/2026/08/21/wing-repeated-reasoning-tool-turn-repair.okf +112 -0
- package/docs/journal/2026/08/21/wing-repeated-tool-guard-finalization.okf +81 -0
- package/docs/journal/2026/08/21/wing-working-directory-memory-deployment.md +110 -0
- package/docs/journal/2026/08/21/wing-workspace-state-memory-prompt-injection.md +88 -0
- package/docs/journal/2026/08/22/alibaba-singapore-dual-endpoint-wire-alignment.okf +142 -0
- package/docs/journal/2026/08/22/delluhiold-claude-compaction-empty-tool-name.okf +168 -0
- package/docs/journal/2026/08/22/remote-memory-relative-prompt-paths.okf +79 -0
- package/docs/journal/2026/08/22/remote-memory-root-system-prompt.okf +121 -0
- package/docs/journal/2026/08/23/claude-web-chat-stateless-mcp-repair.okf +97 -0
- package/docs/journal/2026/08/23/cross-runtime-token-usage-interfaces-research.okf +378 -0
- package/docs/journal/2026/08/23/main-0.2.23-readme-release.okf +81 -0
- package/docs/journal/2026/08/23/mia-openrouter-ox-alpha-latency-diagnosis.okf +146 -0
- package/docs/journal/2026/08/23/openrouter-opencode-ox-alpha-support.okf +114 -0
- package/docs/journal/2026/08/23/tabitoken-provider-model-catalog.okf +97 -0
- package/docs/journal/2026/08/23/usage-stream-snapshot-implementation.okf +134 -0
- package/docs/usage-observability.md +141 -0
- package/install.ps1 +24 -3
- package/package.json +2 -1
|
@@ -497,3 +497,18 @@ def format_llm_delivery_wake_prompt(messages: list[dict[str, Any]]) -> str:
|
|
|
497
497
|
ids = [str(message.get("id") or "").strip() for message in messages]
|
|
498
498
|
ids = [message_id for message_id in ids if message_id]
|
|
499
499
|
return f"[ciel-wake] pending_ids={','.join(ids) or '-'}"
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def format_visible_llm_delivery_wake_prompt(
|
|
503
|
+
messages: list[dict[str, Any]],
|
|
504
|
+
) -> str:
|
|
505
|
+
"""Show a Codex wake body while retaining router replacement semantics.
|
|
506
|
+
|
|
507
|
+
The marker must remain the first physical line: conversation-turn policy
|
|
508
|
+
recognizes that prefix and removes the entire terminal-originated message
|
|
509
|
+
before the queued body is appended to the routed model request.
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
marker = format_llm_delivery_wake_prompt(messages)
|
|
513
|
+
body = format_llm_batch_prompt(messages)
|
|
514
|
+
return f"{marker}\n\n{body}" if body else marker
|
|
@@ -31,6 +31,7 @@ class ChannelInjectionState:
|
|
|
31
31
|
@dataclass(frozen=True, slots=True)
|
|
32
32
|
class ChannelInjectionPrompts:
|
|
33
33
|
llm_delivery: Callable[..., str]
|
|
34
|
+
visible_llm_delivery: Callable[..., str]
|
|
34
35
|
web_chat: Callable[..., str]
|
|
35
36
|
standard: Callable[..., str]
|
|
36
37
|
enter_bytes: Callable[..., bytes]
|
|
@@ -87,6 +88,7 @@ def inject_pending_channel_messages(
|
|
|
87
88
|
*,
|
|
88
89
|
web_chat_only: bool = False,
|
|
89
90
|
wake_for_llm_delivery: bool = False,
|
|
91
|
+
display_llm_delivery_body: bool = False,
|
|
90
92
|
commit_cursor: bool = True,
|
|
91
93
|
injected_message_ids: list[int] | None = None,
|
|
92
94
|
submit_retry_count: int = 1,
|
|
@@ -170,7 +172,12 @@ def inject_pending_channel_messages(
|
|
|
170
172
|
io.log("INFO", f"channel_stdin_proxy_skipped_noise message_id={message_id} channel={channel} reason=duplicate_channel_event")
|
|
171
173
|
continue
|
|
172
174
|
if candidate_uses_router:
|
|
173
|
-
|
|
175
|
+
formatter = (
|
|
176
|
+
prompts.visible_llm_delivery
|
|
177
|
+
if display_llm_delivery_body
|
|
178
|
+
else prompts.llm_delivery
|
|
179
|
+
)
|
|
180
|
+
message_prompt = formatter([message])
|
|
174
181
|
elif web_chat_only and state.message_is_web_chat(message):
|
|
175
182
|
message_prompt = prompts.web_chat([message])
|
|
176
183
|
else:
|
|
@@ -228,7 +235,12 @@ def inject_pending_channel_messages(
|
|
|
228
235
|
if not pending:
|
|
229
236
|
return last_id
|
|
230
237
|
if pending_uses_router:
|
|
231
|
-
|
|
238
|
+
formatter = (
|
|
239
|
+
prompts.visible_llm_delivery
|
|
240
|
+
if display_llm_delivery_body
|
|
241
|
+
else prompts.llm_delivery
|
|
242
|
+
)
|
|
243
|
+
prompt = formatter(pending)
|
|
232
244
|
elif web_chat_only and all(state.message_is_web_chat(message) for message in pending):
|
|
233
245
|
prompt = prompts.web_chat(pending)
|
|
234
246
|
else:
|
|
@@ -245,7 +257,7 @@ def inject_pending_channel_messages(
|
|
|
245
257
|
claimed_ids.append(claim_id)
|
|
246
258
|
submit_bytes = prompts.enter_bytes(enter_bytes)
|
|
247
259
|
try:
|
|
248
|
-
io.write_prompt(
|
|
260
|
+
submitted = io.write_prompt(
|
|
249
261
|
master_fd,
|
|
250
262
|
prompt,
|
|
251
263
|
submit_bytes,
|
|
@@ -254,6 +266,15 @@ def inject_pending_channel_messages(
|
|
|
254
266
|
bracketed_paste=bracketed_paste,
|
|
255
267
|
submit_delay_seconds=submit_delay_seconds,
|
|
256
268
|
)
|
|
269
|
+
if submitted is False:
|
|
270
|
+
wake_store.rollback(pending, claimed_ids)
|
|
271
|
+
ids = ",".join(str(message.get("id") or "") for message in pending)
|
|
272
|
+
io.log(
|
|
273
|
+
"WARN",
|
|
274
|
+
"channel_stdin_proxy_deferred "
|
|
275
|
+
f"message_ids={ids} reason=prompt_not_submitted",
|
|
276
|
+
)
|
|
277
|
+
return return_last_id if pending_uses_router else previous_last_id
|
|
257
278
|
wake_store.record_prompts(pending, prompt)
|
|
258
279
|
except Exception:
|
|
259
280
|
wake_store.rollback(pending, claimed_ids)
|
|
@@ -267,6 +288,9 @@ def inject_pending_channel_messages(
|
|
|
267
288
|
channels = ",".join(sorted({str(message.get("channel") or "default") for message in pending}))
|
|
268
289
|
io.log(
|
|
269
290
|
"INFO",
|
|
270
|
-
f"channel_stdin_proxy_injected count={len(pending)} message_ids={ids}
|
|
291
|
+
f"channel_stdin_proxy_injected count={len(pending)} message_ids={ids} "
|
|
292
|
+
f"channels={channels} enter={prompts.enter_label(submit_bytes)} "
|
|
293
|
+
f"commit_cursor={commit_cursor} "
|
|
294
|
+
f"display_body={bool(pending_uses_router and display_llm_delivery_body)}",
|
|
271
295
|
)
|
|
272
296
|
return return_last_id if pending_uses_router else last_id
|
|
@@ -66,6 +66,7 @@ class ChannelPendingInjectionOptions:
|
|
|
66
66
|
enabled: bool
|
|
67
67
|
web_chat_only: bool
|
|
68
68
|
wake_for_llm_delivery: bool
|
|
69
|
+
display_llm_delivery_body: bool
|
|
69
70
|
submit_retry_count: int
|
|
70
71
|
confirm_submit: bool
|
|
71
72
|
bracketed_paste: bool
|
|
@@ -138,6 +139,7 @@ def poll_pending_channel_messages(
|
|
|
138
139
|
enter_bytes,
|
|
139
140
|
web_chat_only=options.web_chat_only,
|
|
140
141
|
wake_for_llm_delivery=options.wake_for_llm_delivery,
|
|
142
|
+
display_llm_delivery_body=options.display_llm_delivery_body,
|
|
141
143
|
commit_cursor=False,
|
|
142
144
|
injected_message_ids=injected_ids,
|
|
143
145
|
submit_retry_count=options.submit_retry_count,
|
|
@@ -212,6 +212,7 @@ class ChannelTerminalContext:
|
|
|
212
212
|
inject_channel_messages: bool = True,
|
|
213
213
|
inject_web_chat_only: bool = False,
|
|
214
214
|
wake_for_llm_delivery: bool = False,
|
|
215
|
+
channel_wake_display_body: bool = False,
|
|
215
216
|
synthetic_enter_bytes: str | bytes | None = None,
|
|
216
217
|
normalize_bare_cr_for_synthetic_enter: bool = True,
|
|
217
218
|
channel_wake_submit_retries: int = 1,
|
|
@@ -228,6 +229,7 @@ class ChannelTerminalContext:
|
|
|
228
229
|
inject_channel_messages=inject_channel_messages,
|
|
229
230
|
inject_web_chat_only=inject_web_chat_only,
|
|
230
231
|
wake_for_llm_delivery=wake_for_llm_delivery,
|
|
232
|
+
channel_wake_display_body=channel_wake_display_body,
|
|
231
233
|
synthetic_enter_bytes=synthetic_enter_bytes,
|
|
232
234
|
normalize_bare_cr_for_synthetic_enter=(
|
|
233
235
|
normalize_bare_cr_for_synthetic_enter
|
|
@@ -278,6 +280,7 @@ class ChannelTerminalCompatibilityApi:
|
|
|
278
280
|
inject_channel_messages: bool = True,
|
|
279
281
|
inject_web_chat_only: bool = False,
|
|
280
282
|
wake_for_llm_delivery: bool = False,
|
|
283
|
+
channel_wake_display_body: bool = False,
|
|
281
284
|
synthetic_enter_bytes: str | bytes | None = None,
|
|
282
285
|
normalize_bare_cr_for_synthetic_enter: bool = True,
|
|
283
286
|
channel_wake_submit_retries: int = 1,
|
|
@@ -292,6 +295,7 @@ class ChannelTerminalCompatibilityApi:
|
|
|
292
295
|
inject_channel_messages=inject_channel_messages,
|
|
293
296
|
inject_web_chat_only=inject_web_chat_only,
|
|
294
297
|
wake_for_llm_delivery=wake_for_llm_delivery,
|
|
298
|
+
channel_wake_display_body=channel_wake_display_body,
|
|
295
299
|
synthetic_enter_bytes=synthetic_enter_bytes,
|
|
296
300
|
normalize_bare_cr_for_synthetic_enter=(
|
|
297
301
|
normalize_bare_cr_for_synthetic_enter
|
|
@@ -311,6 +315,7 @@ class ChannelTerminalCompatibilityApi:
|
|
|
311
315
|
inject_channel_messages: bool = True,
|
|
312
316
|
inject_web_chat_only: bool = False,
|
|
313
317
|
wake_for_llm_delivery: bool = False,
|
|
318
|
+
channel_wake_display_body: bool = False,
|
|
314
319
|
synthetic_enter_bytes: str | bytes | None = None,
|
|
315
320
|
normalize_bare_cr_for_synthetic_enter: bool = True,
|
|
316
321
|
channel_wake_submit_retries: int = 1,
|
|
@@ -325,6 +330,7 @@ class ChannelTerminalCompatibilityApi:
|
|
|
325
330
|
inject_channel_messages=inject_channel_messages,
|
|
326
331
|
inject_web_chat_only=inject_web_chat_only,
|
|
327
332
|
wake_for_llm_delivery=wake_for_llm_delivery,
|
|
333
|
+
channel_wake_display_body=channel_wake_display_body,
|
|
328
334
|
synthetic_enter_bytes=synthetic_enter_bytes,
|
|
329
335
|
normalize_bare_cr_for_synthetic_enter=(
|
|
330
336
|
normalize_bare_cr_for_synthetic_enter
|
|
@@ -47,6 +47,7 @@ class ChannelTerminalDispatchService:
|
|
|
47
47
|
inject_channel_messages: bool = True,
|
|
48
48
|
inject_web_chat_only: bool = False,
|
|
49
49
|
wake_for_llm_delivery: bool = False,
|
|
50
|
+
channel_wake_display_body: bool = False,
|
|
50
51
|
synthetic_enter_bytes: str | bytes | None = None,
|
|
51
52
|
normalize_bare_cr_for_synthetic_enter: bool = True,
|
|
52
53
|
channel_wake_submit_retries: int = 1,
|
|
@@ -59,6 +60,7 @@ class ChannelTerminalDispatchService:
|
|
|
59
60
|
"inject_channel_messages": inject_channel_messages,
|
|
60
61
|
"inject_web_chat_only": inject_web_chat_only,
|
|
61
62
|
"wake_for_llm_delivery": wake_for_llm_delivery,
|
|
63
|
+
"channel_wake_display_body": channel_wake_display_body,
|
|
62
64
|
"synthetic_enter_bytes": synthetic_enter_bytes,
|
|
63
65
|
"normalize_bare_cr_for_synthetic_enter": (
|
|
64
66
|
normalize_bare_cr_for_synthetic_enter
|
|
@@ -122,6 +122,7 @@ def run_windows_channel_terminal_proxy(
|
|
|
122
122
|
inject_channel_messages: bool = True,
|
|
123
123
|
inject_web_chat_only: bool = False,
|
|
124
124
|
wake_for_llm_delivery: bool = False,
|
|
125
|
+
channel_wake_display_body: bool = False,
|
|
125
126
|
synthetic_enter_bytes: str | bytes | None = None,
|
|
126
127
|
channel_wake_submit_retries: int = 1,
|
|
127
128
|
channel_wake_confirm_submit: bool = False,
|
|
@@ -172,6 +173,7 @@ def run_windows_channel_terminal_proxy(
|
|
|
172
173
|
enabled=inject_channel_messages,
|
|
173
174
|
web_chat_only=inject_web_chat_only,
|
|
174
175
|
wake_for_llm_delivery=wake_for_llm_delivery,
|
|
176
|
+
display_llm_delivery_body=channel_wake_display_body,
|
|
175
177
|
submit_retry_count=submit_retry_count,
|
|
176
178
|
confirm_submit=channel_wake_confirm_submit,
|
|
177
179
|
bracketed_paste=windows_bracketed_paste,
|
|
@@ -194,7 +196,8 @@ def run_windows_channel_terminal_proxy(
|
|
|
194
196
|
f"pid={proc.pid} transport={'conpty' if conpty is not None else 'console-input'} "
|
|
195
197
|
f"enter={policy.enter_label(channel_enter_bytes)} "
|
|
196
198
|
f"submit_retries={submit_retry_count} confirm_submit={bool(channel_wake_confirm_submit)} "
|
|
197
|
-
f"bracketed_paste={windows_bracketed_paste}"
|
|
199
|
+
f"bracketed_paste={windows_bracketed_paste} "
|
|
200
|
+
f"display_body={bool(channel_wake_display_body)}",
|
|
198
201
|
)
|
|
199
202
|
try:
|
|
200
203
|
while proc.poll() is None:
|
|
@@ -308,6 +311,7 @@ def run_posix_channel_terminal_proxy(
|
|
|
308
311
|
inject_channel_messages: bool = True,
|
|
309
312
|
inject_web_chat_only: bool = False,
|
|
310
313
|
wake_for_llm_delivery: bool = False,
|
|
314
|
+
channel_wake_display_body: bool = False,
|
|
311
315
|
synthetic_enter_bytes: str | bytes | None = None,
|
|
312
316
|
normalize_bare_cr_for_synthetic_enter: bool = True,
|
|
313
317
|
channel_wake_submit_retries: int = 1,
|
|
@@ -353,6 +357,7 @@ def run_posix_channel_terminal_proxy(
|
|
|
353
357
|
enabled=inject_channel_messages,
|
|
354
358
|
web_chat_only=inject_web_chat_only,
|
|
355
359
|
wake_for_llm_delivery=wake_for_llm_delivery,
|
|
360
|
+
display_llm_delivery_body=channel_wake_display_body,
|
|
356
361
|
submit_retry_count=submit_retry_count,
|
|
357
362
|
confirm_submit=channel_wake_confirm_submit,
|
|
358
363
|
bracketed_paste=channel_wake_bracketed_paste,
|
|
@@ -372,7 +377,8 @@ def run_posix_channel_terminal_proxy(
|
|
|
372
377
|
"channel_stdin_proxy_enter_default "
|
|
373
378
|
f"enter={policy.enter_label(channel_enter_bytes)} os={os.name} platform={sys.platform} "
|
|
374
379
|
f"submit_retries={submit_retry_count} confirm_submit={bool(channel_wake_confirm_submit)} "
|
|
375
|
-
f"bracketed_paste={bool(channel_wake_bracketed_paste)}"
|
|
380
|
+
f"bracketed_paste={bool(channel_wake_bracketed_paste)} "
|
|
381
|
+
f"display_body={bool(channel_wake_display_body)}",
|
|
376
382
|
)
|
|
377
383
|
terminal.reset_input_mode()
|
|
378
384
|
|
|
@@ -24,6 +24,13 @@ def prompt_contains(candidate: str, prompt: str) -> bool:
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def prompt_message_ids(text: str) -> set[int]:
|
|
27
|
+
normalized = re.sub(r"^[\x00-\x1f\x7f\s]+", "", str(text or ""))
|
|
28
|
+
if normalized.startswith("[ciel-wake]"):
|
|
29
|
+
# A visible Codex wake may contain an untrusted original body after
|
|
30
|
+
# the first line. Only the trusted marker line owns correlation IDs;
|
|
31
|
+
# strings such as ``id=999`` in the external body must not claim an
|
|
32
|
+
# unrelated queued message.
|
|
33
|
+
text = normalized.splitlines()[0]
|
|
27
34
|
ids: set[int] = set()
|
|
28
35
|
for match in _PROMPT_IDS_RE.finditer(str(text or "")):
|
|
29
36
|
for raw in re.split(r"\D+", match.group(1)):
|
|
@@ -129,6 +129,7 @@ class ChannelPendingStatePorts:
|
|
|
129
129
|
@dataclass(frozen=True, slots=True)
|
|
130
130
|
class ChannelPendingDeliveryPorts:
|
|
131
131
|
format_llm_delivery: Callable[[list[dict[str, Any]]], str]
|
|
132
|
+
format_visible_llm_delivery: Callable[[list[dict[str, Any]]], str]
|
|
132
133
|
format_web_chat: Callable[[list[dict[str, Any]]], str]
|
|
133
134
|
format_standard: Callable[[list[dict[str, Any]]], str]
|
|
134
135
|
enter_label: Callable[[bytes], str]
|
|
@@ -143,7 +144,7 @@ class ChannelPendingDeliveryPorts:
|
|
|
143
144
|
class ChannelPendingIoPorts:
|
|
144
145
|
inject_lock: Any
|
|
145
146
|
read_messages: Callable[..., list[dict[str, Any]]]
|
|
146
|
-
write_prompt: Callable[...,
|
|
147
|
+
write_prompt: Callable[..., bool]
|
|
147
148
|
compact_read: Callable[[], dict[str, Any] | None]
|
|
148
149
|
compact_clear: Callable[[], None]
|
|
149
150
|
messages_path: Path
|
|
@@ -331,7 +332,7 @@ class ChannelWakeContext:
|
|
|
331
332
|
submit_delay_seconds: float | None = None,
|
|
332
333
|
write_all: Callable[[Any, bytes], None] | None = None,
|
|
333
334
|
snapshot: Callable[[], str | None] | None = None,
|
|
334
|
-
) ->
|
|
335
|
+
) -> bool:
|
|
335
336
|
delay = (
|
|
336
337
|
self.input.submit_delay_seconds()
|
|
337
338
|
if submit_delay_seconds is None
|
|
@@ -343,7 +344,7 @@ class ChannelWakeContext:
|
|
|
343
344
|
snapshot=snapshot or self.current_tmux_pane_text,
|
|
344
345
|
log=self.input.log,
|
|
345
346
|
)
|
|
346
|
-
injector.inject(
|
|
347
|
+
return injector.inject(
|
|
347
348
|
channel_injection.CallableInputTransport(
|
|
348
349
|
master_fd, write_all or self.write_all
|
|
349
350
|
),
|
|
@@ -530,6 +531,9 @@ class ChannelWakeContext:
|
|
|
530
531
|
),
|
|
531
532
|
prompts=ChannelInjectionPrompts(
|
|
532
533
|
llm_delivery=self.pending_delivery.format_llm_delivery,
|
|
534
|
+
visible_llm_delivery=(
|
|
535
|
+
self.pending_delivery.format_visible_llm_delivery
|
|
536
|
+
),
|
|
533
537
|
web_chat=self.pending_delivery.format_web_chat,
|
|
534
538
|
standard=self.pending_delivery.format_standard,
|
|
535
539
|
enter_bytes=self.enter_bytes,
|
|
@@ -34,6 +34,9 @@ class CliParserSettings:
|
|
|
34
34
|
web_fetch: CliHandler
|
|
35
35
|
log_level: CliHandler
|
|
36
36
|
external_events: CliHandler
|
|
37
|
+
transcript_events: CliHandler
|
|
38
|
+
usage_events: CliHandler
|
|
39
|
+
usage_api_key: CliHandler
|
|
37
40
|
remote_instructions: CliHandler
|
|
38
41
|
remote_memory: CliHandler
|
|
39
42
|
|
|
@@ -91,6 +94,9 @@ def build_cli_parser(services: CliParserServices) -> argparse.ArgumentParser:
|
|
|
91
94
|
_add_optional_value_command(commands, "web-fetch", services.settings.web_fetch)
|
|
92
95
|
_add_optional_value_command(commands, "log-level", services.settings.log_level)
|
|
93
96
|
_add_values_command(commands, "external-events", services.settings.external_events)
|
|
97
|
+
_add_values_command(commands, "transcript-events", services.settings.transcript_events)
|
|
98
|
+
_add_values_command(commands, "usage-events", services.settings.usage_events)
|
|
99
|
+
_add_values_command(commands, "usage-api-key", services.settings.usage_api_key)
|
|
94
100
|
_add_values_command(commands, "remote-instructions", services.settings.remote_instructions)
|
|
95
101
|
_add_values_command(commands, "remote-memory", services.settings.remote_memory)
|
|
96
102
|
_add_optional_value_command(commands, "ollama-native", services.provider.ollama_native)
|
|
@@ -67,6 +67,8 @@ class ProviderPassthroughProjectionPorts:
|
|
|
67
67
|
responses_request_max_bytes: Callable[[str, dict[str, Any]], int | None] = (
|
|
68
68
|
lambda _provider, _config: None
|
|
69
69
|
)
|
|
70
|
+
finalize_chat: Callable[[dict[str, Any]], dict[str, Any]] = lambda body: body
|
|
71
|
+
finalize_responses: Callable[[dict[str, Any]], dict[str, Any]] = lambda body: body
|
|
70
72
|
|
|
71
73
|
|
|
72
74
|
@dataclass(frozen=True, slots=True)
|
|
@@ -249,6 +251,7 @@ class CodexBackendContext:
|
|
|
249
251
|
urlopen=self.provider_transport.urlopen,
|
|
250
252
|
timeout_seconds=self.provider_transport.timeout_seconds,
|
|
251
253
|
copy_response_headers=self.provider_transport.copy_response_headers,
|
|
254
|
+
finalize_body=self.provider_projection.finalize_chat,
|
|
252
255
|
)
|
|
253
256
|
)
|
|
254
257
|
|
|
@@ -279,6 +282,7 @@ class CodexBackendContext:
|
|
|
279
282
|
request_max_bytes=self.provider_projection.responses_request_max_bytes,
|
|
280
283
|
estimate_tokens=self.replay.estimate_tokens,
|
|
281
284
|
compact_responses=self.replay.compact_responses,
|
|
285
|
+
finalize_body=self.provider_projection.finalize_responses,
|
|
282
286
|
)
|
|
283
287
|
)
|
|
284
288
|
|
|
@@ -27,6 +27,13 @@ CODEX_EMPTY_REASONING_CONTINUATION_NUDGE = (
|
|
|
27
27
|
"or provide the concrete final answer if no tool is needed."
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
+
CODEX_REPEATED_TOOL_CONTINUATION_NUDGE = (
|
|
31
|
+
"The runtime stopped an exact tool call because the same call and result are "
|
|
32
|
+
"already present twice in this turn. Do not repeat that call. Use the existing "
|
|
33
|
+
"result, choose a different action if one is required, or provide the concrete "
|
|
34
|
+
"final answer now."
|
|
35
|
+
)
|
|
36
|
+
|
|
30
37
|
RUNTIME_REASONING_ONLY_NOTICE_PREFIXES = (
|
|
31
38
|
"[ciel-runtime] Upstream model returned reasoning without a final answer or tool call.",
|
|
32
39
|
"[ciel-runtime] Upstream model exhausted its output budget during reasoning",
|
|
@@ -37,6 +44,13 @@ RUNTIME_EMPTY_END_TURN_NOTICE_PREFIX = (
|
|
|
37
44
|
"tool call."
|
|
38
45
|
)
|
|
39
46
|
|
|
47
|
+
RUNTIME_REPEATED_TOOL_NOTICE_PREFIX = (
|
|
48
|
+
"[ciel-runtime] Stopped an identical completed tool call from repeating."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
RUNTIME_CONTROL_MESSAGE_KEY = "ciel_runtime_control"
|
|
52
|
+
RUNTIME_REPEATED_TOOL_RECOVERY = "repeated_tool_call_recovery"
|
|
53
|
+
|
|
40
54
|
KIMI_FOLLOWUP_PROMISE_RE = re.compile(
|
|
41
55
|
r"(?:겠습니다|할게요|해볼게요|하겠습니다|"
|
|
42
56
|
r"i(?:'|’)ll\b[^\n]*|i\s+will\b[^\n]*|let\s+me\b[^\n]*)[.!?。!?]?\s*$",
|
|
@@ -97,6 +111,21 @@ def message_has_only_empty_end_turn_notice(message: dict[str, Any]) -> bool:
|
|
|
97
111
|
)
|
|
98
112
|
|
|
99
113
|
|
|
114
|
+
def message_has_only_repeated_tool_notice(message: dict[str, Any]) -> bool:
|
|
115
|
+
"""Identify the runtime's internal repeated-tool guard projection."""
|
|
116
|
+
|
|
117
|
+
texts = [
|
|
118
|
+
str(block.get("text") or "").strip()
|
|
119
|
+
for block in message.get("content") or []
|
|
120
|
+
if isinstance(block, dict)
|
|
121
|
+
and block.get("type") == "text"
|
|
122
|
+
and str(block.get("text") or "").strip()
|
|
123
|
+
]
|
|
124
|
+
return bool(texts) and all(
|
|
125
|
+
text.startswith(RUNTIME_REPEATED_TOOL_NOTICE_PREFIX) for text in texts
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
100
129
|
def kimi_message_promises_followup(message: dict[str, Any]) -> bool:
|
|
101
130
|
"""Recognize Kimi ending a reasoning turn with an unperformed next action."""
|
|
102
131
|
|
|
@@ -129,8 +158,24 @@ def message_without_empty_end_turn_notice(message: dict[str, Any]) -> dict[str,
|
|
|
129
158
|
return projected
|
|
130
159
|
|
|
131
160
|
|
|
161
|
+
def message_without_repeated_tool_notice(message: dict[str, Any]) -> dict[str, Any]:
|
|
162
|
+
if not message_has_only_repeated_tool_notice(message):
|
|
163
|
+
return message
|
|
164
|
+
projected = dict(message)
|
|
165
|
+
projected["content"] = [
|
|
166
|
+
dict(block) if isinstance(block, dict) else block
|
|
167
|
+
for block in message.get("content") or []
|
|
168
|
+
if not (isinstance(block, dict) and block.get("type") == "text")
|
|
169
|
+
]
|
|
170
|
+
return projected
|
|
171
|
+
|
|
172
|
+
|
|
132
173
|
def body_with_continuation_nudge(
|
|
133
|
-
body: dict[str, Any],
|
|
174
|
+
body: dict[str, Any],
|
|
175
|
+
message: dict[str, Any],
|
|
176
|
+
nudge: str = CODEX_CONTINUATION_NUDGE,
|
|
177
|
+
*,
|
|
178
|
+
control: str | None = None,
|
|
134
179
|
) -> dict[str, Any]:
|
|
135
180
|
"""Replay the request with the stalled reply and an explicit continue turn."""
|
|
136
181
|
|
|
@@ -138,7 +183,10 @@ def body_with_continuation_nudge(
|
|
|
138
183
|
assistant_text = message_text(message).strip()
|
|
139
184
|
if assistant_text:
|
|
140
185
|
messages.append({"role": "assistant", "content": [{"type": "text", "text": assistant_text}]})
|
|
141
|
-
|
|
186
|
+
continuation = {"role": "user", "content": [{"type": "text", "text": nudge}]}
|
|
187
|
+
if control:
|
|
188
|
+
continuation[RUNTIME_CONTROL_MESSAGE_KEY] = control
|
|
189
|
+
messages.append(continuation)
|
|
142
190
|
retried = dict(body)
|
|
143
191
|
retried["messages"] = messages
|
|
144
192
|
return retried
|
|
@@ -197,9 +245,9 @@ def recover_preamble_only_turn(
|
|
|
197
245
|
return message
|
|
198
246
|
text = message_text(message)
|
|
199
247
|
empty_end_turn = message_has_only_empty_end_turn_notice(message)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
248
|
+
repeated_tool_guard = message_has_only_repeated_tool_notice(message)
|
|
249
|
+
reasoning_only = (
|
|
250
|
+
message_has_reasoning(message)
|
|
203
251
|
and (not text.strip() or message_has_only_reasoning_notice(message))
|
|
204
252
|
)
|
|
205
253
|
kimi_promised_followup = (
|
|
@@ -209,7 +257,8 @@ def recover_preamble_only_turn(
|
|
|
209
257
|
)
|
|
210
258
|
if (
|
|
211
259
|
not empty_end_turn
|
|
212
|
-
and not
|
|
260
|
+
and not repeated_tool_guard
|
|
261
|
+
and not reasoning_only
|
|
213
262
|
and not kimi_promised_followup
|
|
214
263
|
and not services.should_retry(body, text, [])
|
|
215
264
|
):
|
|
@@ -218,8 +267,10 @@ def recover_preamble_only_turn(
|
|
|
218
267
|
reason = (
|
|
219
268
|
"empty_end_turn"
|
|
220
269
|
if empty_end_turn
|
|
270
|
+
else "repeated_tool_call"
|
|
271
|
+
if repeated_tool_guard
|
|
221
272
|
else "reasoning_only"
|
|
222
|
-
if
|
|
273
|
+
if reasoning_only
|
|
223
274
|
else "promised_followup"
|
|
224
275
|
if kimi_promised_followup
|
|
225
276
|
else "preamble_only"
|
|
@@ -231,8 +282,11 @@ def recover_preamble_only_turn(
|
|
|
231
282
|
)
|
|
232
283
|
try:
|
|
233
284
|
nudge = (
|
|
285
|
+
CODEX_REPEATED_TOOL_CONTINUATION_NUDGE
|
|
286
|
+
if repeated_tool_guard
|
|
287
|
+
else
|
|
234
288
|
CODEX_EMPTY_REASONING_CONTINUATION_NUDGE
|
|
235
|
-
if empty_end_turn or
|
|
289
|
+
if empty_end_turn or reasoning_only
|
|
236
290
|
else CODEX_CONTINUATION_NUDGE
|
|
237
291
|
)
|
|
238
292
|
recovery_config = dict(pcfg)
|
|
@@ -244,12 +298,15 @@ def recover_preamble_only_turn(
|
|
|
244
298
|
recovery_config,
|
|
245
299
|
body_with_continuation_nudge(
|
|
246
300
|
body,
|
|
247
|
-
|
|
301
|
+
message_without_repeated_tool_notice(message)
|
|
302
|
+
if repeated_tool_guard
|
|
303
|
+
else message_without_empty_end_turn_notice(message)
|
|
248
304
|
if empty_end_turn
|
|
249
305
|
else message_without_reasoning_notice(message)
|
|
250
|
-
if
|
|
306
|
+
if reasoning_only
|
|
251
307
|
else message,
|
|
252
308
|
nudge,
|
|
309
|
+
control=(RUNTIME_REPEATED_TOOL_RECOVERY if repeated_tool_guard else None),
|
|
253
310
|
),
|
|
254
311
|
)
|
|
255
312
|
except Exception as exc: # noqa: BLE001 - recovery must never fail the turn
|
|
@@ -260,8 +317,11 @@ def recover_preamble_only_turn(
|
|
|
260
317
|
return message
|
|
261
318
|
if not isinstance(retried, dict):
|
|
262
319
|
return message
|
|
263
|
-
if empty_end_turn or
|
|
264
|
-
if
|
|
320
|
+
if empty_end_turn or reasoning_only or repeated_tool_guard:
|
|
321
|
+
if (
|
|
322
|
+
message_has_only_repeated_tool_notice(retried)
|
|
323
|
+
or (not message_has_tool_use(retried) and not message_text(retried).strip())
|
|
324
|
+
):
|
|
265
325
|
return message
|
|
266
326
|
return retried
|
|
267
327
|
if not message_has_tool_use(retried):
|
|
@@ -286,6 +346,7 @@ def _merged(original: dict[str, Any], retried: dict[str, Any]) -> dict[str, Any]
|
|
|
286
346
|
__all__ = [
|
|
287
347
|
"CODEX_CONTINUATION_NUDGE",
|
|
288
348
|
"CODEX_EMPTY_REASONING_CONTINUATION_NUDGE",
|
|
349
|
+
"CODEX_REPEATED_TOOL_CONTINUATION_NUDGE",
|
|
289
350
|
"RUNTIME_EMPTY_END_TURN_NOTICE_PREFIX",
|
|
290
351
|
"CodexTurnRecoveryServices",
|
|
291
352
|
"body_with_codex_compat_instructions",
|
|
@@ -294,8 +355,10 @@ __all__ = [
|
|
|
294
355
|
"message_has_reasoning",
|
|
295
356
|
"message_has_only_reasoning_notice",
|
|
296
357
|
"message_has_only_empty_end_turn_notice",
|
|
358
|
+
"message_has_only_repeated_tool_notice",
|
|
297
359
|
"kimi_message_promises_followup",
|
|
298
360
|
"message_without_empty_end_turn_notice",
|
|
361
|
+
"message_without_repeated_tool_notice",
|
|
299
362
|
"message_without_reasoning_notice",
|
|
300
363
|
"message_text",
|
|
301
364
|
"recover_preamble_only_turn",
|
|
@@ -34,6 +34,52 @@ def apply_config_migrations(cfg: dict[str, Any], *, policy: ConfigMigrationPolic
|
|
|
34
34
|
migrations = {}
|
|
35
35
|
cfg["migrations"] = migrations
|
|
36
36
|
|
|
37
|
+
marker = "ox_alpha_provider_catalogs_20260823"
|
|
38
|
+
if not migrations.get(marker):
|
|
39
|
+
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
40
|
+
additions = {
|
|
41
|
+
"openrouter": ("stealth/ox-alpha", None),
|
|
42
|
+
"opencode": ("x-preview-f-free", "openai-chat"),
|
|
43
|
+
"opencode-go": ("ox-alpha-free", "openai-chat"),
|
|
44
|
+
}
|
|
45
|
+
for provider_name, (model, endpoint) in additions.items():
|
|
46
|
+
pcfg = providers.get(provider_name)
|
|
47
|
+
if not isinstance(pcfg, dict):
|
|
48
|
+
continue
|
|
49
|
+
custom = pcfg.get("custom_models")
|
|
50
|
+
if not isinstance(custom, list):
|
|
51
|
+
custom = []
|
|
52
|
+
pcfg["custom_models"] = custom
|
|
53
|
+
known = {
|
|
54
|
+
normalize_model_id(provider_name, str(item))
|
|
55
|
+
for item in custom
|
|
56
|
+
if str(item).strip()
|
|
57
|
+
}
|
|
58
|
+
if normalize_model_id(provider_name, model) not in known:
|
|
59
|
+
custom.append(model)
|
|
60
|
+
if endpoint:
|
|
61
|
+
model_endpoints = pcfg.get("model_endpoints")
|
|
62
|
+
if not isinstance(model_endpoints, dict):
|
|
63
|
+
model_endpoints = {}
|
|
64
|
+
pcfg["model_endpoints"] = model_endpoints
|
|
65
|
+
model_endpoints.setdefault(model, endpoint)
|
|
66
|
+
migrations[marker] = True
|
|
67
|
+
|
|
68
|
+
marker = "opencode_catalog_protocol_refresh_20260823"
|
|
69
|
+
if not migrations.get(marker):
|
|
70
|
+
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
71
|
+
zen = providers.get("opencode")
|
|
72
|
+
if isinstance(zen, dict):
|
|
73
|
+
custom = zen.get("custom_models")
|
|
74
|
+
if isinstance(custom, list):
|
|
75
|
+
zen["custom_models"] = [
|
|
76
|
+
model
|
|
77
|
+
for model in custom
|
|
78
|
+
if normalize_model_id("opencode", str(model))
|
|
79
|
+
!= "qwen3.6-plus-free"
|
|
80
|
+
]
|
|
81
|
+
migrations[marker] = True
|
|
82
|
+
|
|
37
83
|
marker = "alibaba_provider_catalogs_20260806"
|
|
38
84
|
if not migrations.get(marker):
|
|
39
85
|
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
@@ -73,6 +119,62 @@ def apply_config_migrations(cfg: dict[str, Any], *, policy: ConfigMigrationPolic
|
|
|
73
119
|
custom.append(model)
|
|
74
120
|
migrations[marker] = True
|
|
75
121
|
|
|
122
|
+
marker = "alibaba_qwen38_singapore_20260821"
|
|
123
|
+
if not migrations.get(marker):
|
|
124
|
+
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
125
|
+
model_studio = providers.get("alims-intl")
|
|
126
|
+
if isinstance(model_studio, dict):
|
|
127
|
+
custom = model_studio.get("custom_models")
|
|
128
|
+
if not isinstance(custom, list):
|
|
129
|
+
custom = []
|
|
130
|
+
model_studio["custom_models"] = custom
|
|
131
|
+
known = {
|
|
132
|
+
normalize_model_id("alims-intl", str(model))
|
|
133
|
+
for model in custom
|
|
134
|
+
if str(model).strip()
|
|
135
|
+
}
|
|
136
|
+
if normalize_model_id("alims-intl", "qwen3.8-max") not in known:
|
|
137
|
+
custom.insert(0, "qwen3.8-max")
|
|
138
|
+
model_studio["region"] = "ap-southeast-1"
|
|
139
|
+
old_default_profile = (
|
|
140
|
+
normalize_model_id(
|
|
141
|
+
"alims-intl", str(model_studio.get("current_model") or "")
|
|
142
|
+
)
|
|
143
|
+
== "qwen3.7-max"
|
|
144
|
+
and normalize_model_id(
|
|
145
|
+
"alims-intl", str(model_studio.get("opus_model") or "")
|
|
146
|
+
)
|
|
147
|
+
== "qwen3.7-max"
|
|
148
|
+
and (positive_int(model_studio.get("max_output_tokens")) or 0) == 65_536
|
|
149
|
+
and str(model_studio.get("effort_level") or "").strip().lower()
|
|
150
|
+
== "high"
|
|
151
|
+
)
|
|
152
|
+
if old_default_profile:
|
|
153
|
+
model_studio.update(
|
|
154
|
+
{
|
|
155
|
+
"current_model": "qwen3.8-max",
|
|
156
|
+
"opus_model": "qwen3.8-max",
|
|
157
|
+
"context_window": 1_000_000,
|
|
158
|
+
"max_model_len": 1_000_000,
|
|
159
|
+
"max_output_tokens": 131_072,
|
|
160
|
+
"effort_level": "xhigh",
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
for provider_name in ("alitoken", "alitoken-individual"):
|
|
164
|
+
pcfg = providers.get(provider_name)
|
|
165
|
+
if not isinstance(pcfg, dict):
|
|
166
|
+
continue
|
|
167
|
+
selected = normalize_model_id(
|
|
168
|
+
provider_name, str(pcfg.get("current_model") or "")
|
|
169
|
+
)
|
|
170
|
+
if selected != "qwen3.8-max":
|
|
171
|
+
continue
|
|
172
|
+
if (positive_int(pcfg.get("context_window")) or 0) == 1_048_576:
|
|
173
|
+
pcfg["context_window"] = 1_000_000
|
|
174
|
+
if (positive_int(pcfg.get("max_model_len")) or 0) == 1_048_576:
|
|
175
|
+
pcfg["max_model_len"] = 1_000_000
|
|
176
|
+
migrations[marker] = True
|
|
177
|
+
|
|
76
178
|
marker = "alibaba_token_plan_singapore_20260806"
|
|
77
179
|
if not migrations.get(marker):
|
|
78
180
|
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
@@ -310,16 +412,6 @@ def apply_config_migrations(cfg: dict[str, Any], *, policy: ConfigMigrationPolic
|
|
|
310
412
|
|
|
311
413
|
marker = "opencode_zen_qwen36_plus_free_model_20260614"
|
|
312
414
|
if not migrations.get(marker):
|
|
313
|
-
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
314
|
-
pcfg = providers.get("opencode")
|
|
315
|
-
if isinstance(pcfg, dict):
|
|
316
|
-
custom = pcfg.get("custom_models")
|
|
317
|
-
if not isinstance(custom, list):
|
|
318
|
-
custom = []
|
|
319
|
-
pcfg["custom_models"] = custom
|
|
320
|
-
normalized_custom = {normalize_model_id("opencode", str(mid)) for mid in custom if str(mid).strip()}
|
|
321
|
-
if "qwen3.6-plus-free" not in normalized_custom:
|
|
322
|
-
custom.append("qwen3.6-plus-free")
|
|
323
415
|
migrations[marker] = True
|
|
324
416
|
|
|
325
417
|
marker = "opencode_qwen36_plus_parameters_20260614"
|
|
@@ -112,6 +112,15 @@ def build_default_config(provider_defaults: dict[str, Any]) -> dict[str, Any]:
|
|
|
112
112
|
"web_backends": {},
|
|
113
113
|
"workspace_mcp": {"servers": {}},
|
|
114
114
|
"external_event_receivers": {},
|
|
115
|
+
"transcript_events": {
|
|
116
|
+
"enabled": False,
|
|
117
|
+
"url": "",
|
|
118
|
+
"authorization": "",
|
|
119
|
+
"timeout_seconds": 5,
|
|
120
|
+
"poll_interval_ms": 1000,
|
|
121
|
+
"max_batch_bytes": 1048576,
|
|
122
|
+
"start_mode": "tail",
|
|
123
|
+
},
|
|
115
124
|
"remote_instructions": {
|
|
116
125
|
"enabled": False,
|
|
117
126
|
"claude_url": "",
|