@oneciel-ai/ciel-runtime 0.2.37 → 0.2.38
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 +78 -0
- package/README.md +5 -2
- package/ciel_runtime.py +94 -96
- package/ciel_runtime_support/anthropic_model_policy.py +20 -3
- package/ciel_runtime_support/architecture.py +5 -0
- package/ciel_runtime_support/channel_inflight.py +4 -4
- package/ciel_runtime_support/channel_injection.py +56 -1
- package/ciel_runtime_support/channel_mcp_tools.py +34 -0
- package/ciel_runtime_support/channel_message_policy.py +9 -1
- package/ciel_runtime_support/channel_message_prompt.py +23 -0
- package/ciel_runtime_support/channel_pending_injection.py +86 -32
- package/ciel_runtime_support/channel_pending_poll.py +6 -2
- package/ciel_runtime_support/channel_runtime_environment.py +0 -11
- package/ciel_runtime_support/channel_terminal_context.py +0 -6
- package/ciel_runtime_support/channel_terminal_proxy.py +3 -42
- package/ciel_runtime_support/channel_transcript_repository.py +16 -1
- package/ciel_runtime_support/channel_wake_claim_repository.py +0 -19
- package/ciel_runtime_support/channel_wake_context.py +6 -13
- package/ciel_runtime_support/channel_wake_delivery_repository.py +50 -0
- package/ciel_runtime_support/chat_http_controller.py +88 -5
- package/ciel_runtime_support/claude_environment.py +100 -3
- package/ciel_runtime_support/claude_launch_assembly.py +4 -0
- package/ciel_runtime_support/claude_session_socket.py +181 -0
- package/ciel_runtime_support/cli_application_context.py +18 -10
- package/ciel_runtime_support/cli_dispatch.py +19 -3
- package/ciel_runtime_support/cli_parser.py +2 -0
- package/ciel_runtime_support/cli_usage.py +3 -2
- package/ciel_runtime_support/codex_backend_context.py +8 -3
- package/ciel_runtime_support/codex_completion_gate.py +198 -0
- package/ciel_runtime_support/codex_turn_recovery.py +240 -83
- package/ciel_runtime_support/compatibility_protocol.py +5 -2
- package/ciel_runtime_support/config_migrations.py +79 -0
- package/ciel_runtime_support/config_repository.py +7 -0
- package/ciel_runtime_support/context_summary_policy.py +24 -4
- package/ciel_runtime_support/external_event_menu.py +107 -0
- package/ciel_runtime_support/external_event_receiver.py +19 -1
- package/ciel_runtime_support/launch_state.py +2 -0
- package/ciel_runtime_support/managed_tool_injection.py +33 -0
- package/ciel_runtime_support/muse_runtime_context.py +291 -0
- package/ciel_runtime_support/ollama_thinking.py +12 -26
- package/ciel_runtime_support/prelaunch.py +13 -0
- package/ciel_runtime_support/prelaunch_launch_panel.py +2 -1
- package/ciel_runtime_support/prompt_compaction.py +39 -0
- package/ciel_runtime_support/provider_files_proxy.py +248 -0
- package/ciel_runtime_support/provider_model_identity.py +10 -2
- package/ciel_runtime_support/provider_option_cli.py +1 -1
- package/ciel_runtime_support/provider_request_access.py +54 -1
- package/ciel_runtime_support/provider_responses_passthrough.py +152 -15
- package/ciel_runtime_support/providers/alibaba.py +34 -1
- package/ciel_runtime_support/providers/anthropic.py +18 -1
- package/ciel_runtime_support/providers/meta.py +252 -21
- package/ciel_runtime_support/pseudo_tool_parser.py +17 -1
- package/ciel_runtime_support/remote_bridge.py +4 -0
- package/ciel_runtime_support/remote_instructions.py +5 -0
- package/ciel_runtime_support/remote_memory.py +1 -1
- package/ciel_runtime_support/responses_cache_diagnostics.py +82 -0
- package/ciel_runtime_support/responses_custom_tool_bridge.py +282 -0
- package/ciel_runtime_support/responses_input_compatibility.py +19 -3
- package/ciel_runtime_support/router_http.py +301 -4
- package/ciel_runtime_support/router_observability_context.py +26 -0
- package/ciel_runtime_support/router_server_context.py +1 -0
- package/ciel_runtime_support/runtime_adapters.py +31 -0
- package/ciel_runtime_support/runtime_constants.py +23 -1
- package/ciel_runtime_support/runtime_input_gateway.py +85 -5
- package/ciel_runtime_support/runtime_input_status.py +150 -0
- package/ciel_runtime_support/runtime_launch.py +27 -4
- package/ciel_runtime_support/runtime_paths.py +1 -0
- package/ciel_runtime_support/speech_http_controller.py +3 -2
- package/ciel_runtime_support/streaming_anthropic.py +3 -0
- package/ciel_runtime_support/tool_call_events.py +98 -0
- package/ciel_runtime_support/transcript_delta_delivery.py +140 -1
- package/ciel_runtime_support/ui_text.py +1 -0
- package/ciel_runtime_support/web_search_result_events.py +130 -0
- package/ciel_runtime_support/web_ui.py +1 -0
- package/ciel_runtime_support/windows_conpty.py +92 -8
- package/ciel_runtime_support/workspace_mcp.py +4 -1
- package/ciel_runtime_support/workspace_state.py +1 -0
- package/docs/Configuration.md +20 -4
- package/docs/MCP-Channels.md +79 -6
- package/docs/Managed-Tool-Injection.md +25 -0
- package/docs/Module-Map.md +4 -0
- package/docs/Muse-Code.md +73 -0
- package/docs/Observability.md +39 -0
- package/docs/Providers.md +29 -2
- package/docs/Remote-Bridge.md +12 -0
- package/docs/Router.md +1 -0
- package/docs/journal/2026/09/01/diagnostics/anthropic/router/fable51-usage-credits.okf +70 -0
- package/docs/journal/2026/09/01/implementation/claude/session-socket/all-input-paths/default-delivery.okf +133 -0
- package/docs/journal/2026/09/01/implementation/claude/session-socket/windows-direct-input.okf +112 -0
- package/docs/journal/2026/09/02/diagnostics/cache/alibaba/qwen38-post-restart-hit-rate.okf +107 -0
- package/docs/journal/2026/09/02/diagnostics/cache/ollama-cloud/kimi-k3-codex-hit-rate.okf +91 -0
- package/docs/journal/2026/09/02/diagnostics/codex/tui/statusline-cache-metrics.okf +44 -0
- package/docs/journal/2026/09/02/diagnostics/input/transport/fallback-behavior.okf +31 -0
- package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-agent-turn-recovery.okf +125 -0
- package/docs/journal/2026/09/02/diagnostics/providers/ollama-cloud/kimi-k3-parameters.okf +128 -0
- package/docs/journal/2026/09/02/diagnostics/remote/mia/socket-tui-visibility.okf +47 -0
- package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-latency.okf +108 -0
- package/docs/journal/2026/09/02/diagnostics/runtime/alibaba/token-plan/qwen38-vs-gpt56-latency.okf +92 -0
- package/docs/journal/2026/09/02/implementation/anthropic/context/one-million-defaults.okf +85 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.okf +104 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/qwen38-cache-hit-improvement.png +0 -0
- package/docs/journal/2026/09/02/implementation/cache/alibaba/responses-session-cache.okf +97 -0
- package/docs/journal/2026/09/02/implementation/providers/alibaba/qwen38/0902-parameter-alignment.okf +97 -0
- package/docs/journal/2026/09/02/implementation/providers/meta/multimodal-tools/contributor-protocol-adoption.okf +140 -0
- package/docs/journal/2026/09/02/implementation/providers/meta/muse-spark-1.3-support.okf +53 -0
- package/docs/journal/2026/09/02/monitoring/cache/alibaba/cielarvis-qwen38-live-hit-rate.okf +858 -0
- package/docs/journal/2026/09/02/operations/release/main-merge-local-deploy.okf +36 -0
- package/docs/journal/2026/09/02/operations/release/nightly/local-cache-deployment.okf +54 -0
- package/docs/journal/2026/09/02/operations/release/nightly/ollama-kimi-recovery-model-audit-deployment.okf +58 -0
- package/docs/journal/2026/09/02/operations/release/nightly/qwen38-cache-improvement-deployment.okf +72 -0
- package/docs/journal/2026/09/02/research/cache/alibaba/qwen38-hit-rate-improvement.okf +233 -0
- package/docs/journal/2026/09/02/research/cache/codex/provider-scope.okf +77 -0
- package/docs/journal/2026/09/02/research/providers/ollama-cloud/desktop-model-exhaustive-audit.okf +159 -0
- package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-cache-hit.okf +148 -0
- package/docs/journal/2026/09/03/diagnostics/providers/meta/muse-spark-contributor-required-schema.okf +127 -0
- package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-resumed-session-stall.okf +160 -0
- package/docs/journal/2026/09/03/diagnostics/providers/ollama-cloud/kimi-k3-substantive-dangling-action.okf +94 -0
- package/docs/journal/2026/09/03/diagnostics/remote/kevin/codex-gpt-early-turn-completion.okf +162 -0
- package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/default-yolo.okf +40 -0
- package/docs/journal/2026/09/03/implementation/runtimes/meta/muse-code/native-subscription-router.okf +86 -0
- package/docs/journal/2026/09/03/operations/deployment/local/session-socket-default-fallback.okf +41 -0
- package/docs/journal/2026/09/03/research/deployment/colab/tailscale/credential-storage.okf +41 -0
- package/docs/journal/2026/09/03/research/providers/meta/muse-code/subscription-billing-boundary.okf +64 -0
- package/docs/journal/2026/09/03/research/web-chat/voice/instruction-format.okf +135 -0
- package/docs/journal/2026/09/04/diagnostics/claude/context/early-auto-compaction.okf +75 -0
- package/docs/journal/2026/09/04/diagnostics/claude/context/fable-51-status-200k.okf +54 -0
- package/docs/journal/2026/09/04/diagnostics/runtime/codex/responses/replayed-item-id-validation.okf +74 -0
- package/docs/journal/2026/09/04/diagnostics/workspaces/onecieldmsui/codex/replay-stall.okf +100 -0
- package/docs/journal/2026/09/04/implementation/observability/tool-calls/websocket-stream.okf +49 -0
- package/docs/journal/2026/09/04/implementation/web-chat/input/raw-injection.okf +42 -0
- package/docs/journal/2026/09/05/diagnostics/tty/claude-raw-idle-render.okf +25 -0
- package/docs/journal/2026/09/05/implementation/events/search-response-urls.okf +23 -0
- package/docs/journal/2026/09/05/implementation/tools/native-injection.okf +19 -0
- package/docs/journal/2026/09/05/implementation/windows/conpty/prompt-delivery-lifecycle.okf +43 -0
- package/docs/journal/2026/09/05/release/nightly/conpty-native-tools.okf +13 -0
- package/docs/journal/2026/09/05/release/nightly/search-result-urls.okf +14 -0
- package/docs/journal/2026/09/06/implementation/codex/inherited-web-mcp.okf +22 -0
- package/docs/journal/2026/09/07/integration/main/nightly-merge.okf +13 -0
- package/docs/journal/2026/09/07/release/stable/0.2.38.okf +10 -0
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ same protection without synthesizing a tool call the Codex client never offered.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import copy
|
|
13
14
|
import re
|
|
14
15
|
from dataclasses import dataclass
|
|
15
16
|
from typing import Any, Callable
|
|
@@ -56,11 +57,16 @@ RUNTIME_CONTROL_MESSAGE_KEY = "ciel_runtime_control"
|
|
|
56
57
|
RUNTIME_REPEATED_TOOL_RECOVERY = "repeated_tool_call_recovery"
|
|
57
58
|
|
|
58
59
|
_REASONING_RECOVERY_MIN_OUTPUT_TOKENS = 8192
|
|
60
|
+
_KIMI_CONTINUATION_MAX_ATTEMPTS = 6
|
|
61
|
+
_KIMI_STRICT_CONTINUATION_AFTER_ATTEMPT = 3
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
CODEX_COMPLETION_TOOL_NAME = "ciel_runtime_confirm_completion"
|
|
64
|
+
|
|
65
|
+
CODEX_STRICT_CONTINUATION_NUDGE = (
|
|
66
|
+
"This is a runtime completion check. Call the supplied "
|
|
67
|
+
f"{CODEX_COMPLETION_TOOL_NAME} tool only if every requested action is complete. "
|
|
68
|
+
"Otherwise call the next actual work tool now, using an exact tool name and "
|
|
69
|
+
"every required field from the supplied schema. Do not answer with text."
|
|
64
70
|
)
|
|
65
71
|
|
|
66
72
|
|
|
@@ -304,12 +310,40 @@ def message_has_only_runtime_stall_notice(message: dict[str, Any]) -> bool:
|
|
|
304
310
|
)
|
|
305
311
|
|
|
306
312
|
|
|
307
|
-
def
|
|
308
|
-
"""Recognize
|
|
313
|
+
def message_confirms_completion(message: dict[str, Any]) -> bool:
|
|
314
|
+
"""Recognize the private, language-independent completion tool call."""
|
|
309
315
|
|
|
310
|
-
|
|
316
|
+
return any(
|
|
317
|
+
isinstance(block, dict)
|
|
318
|
+
and block.get("type") == "tool_use"
|
|
319
|
+
and block.get("name") == CODEX_COMPLETION_TOOL_NAME
|
|
320
|
+
for block in message.get("content") or []
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def message_requires_completion_check(
|
|
325
|
+
body: dict[str, Any], message: dict[str, Any]
|
|
326
|
+
) -> bool:
|
|
327
|
+
"""Gate a no-tool reasoning response without inspecting natural language."""
|
|
328
|
+
|
|
329
|
+
return bool(
|
|
330
|
+
body.get("tools")
|
|
331
|
+
and message_has_reasoning(message)
|
|
332
|
+
and not message_has_tool_use(message)
|
|
333
|
+
and message_text(message).strip()
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _is_kimi_turn(provider: str, body: dict[str, Any]) -> bool:
|
|
338
|
+
"""Identify Kimi across its native and Ollama Cloud provider routes."""
|
|
339
|
+
|
|
340
|
+
provider_name = str(provider or "").strip().casefold()
|
|
341
|
+
if provider_name == "kimi":
|
|
342
|
+
return True
|
|
343
|
+
if provider_name not in {"ollama", "ollama-cloud"}:
|
|
311
344
|
return False
|
|
312
|
-
|
|
345
|
+
model = str(body.get("model") or "").strip().casefold()
|
|
346
|
+
return "kimi-k3" in model
|
|
313
347
|
|
|
314
348
|
|
|
315
349
|
def message_without_reasoning_notice(message: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -358,9 +392,26 @@ def body_with_continuation_nudge(
|
|
|
358
392
|
"""Replay the request with the stalled reply and an explicit continue turn."""
|
|
359
393
|
|
|
360
394
|
messages = list(body.get("messages") or [])
|
|
361
|
-
|
|
362
|
-
if
|
|
363
|
-
|
|
395
|
+
assistant_content = message.get("content")
|
|
396
|
+
if isinstance(assistant_content, list) and assistant_content:
|
|
397
|
+
# Ollama Desktop retains the complete assistant response before the
|
|
398
|
+
# next agent-loop pass. Kimi K3 likewise requires preserved thinking
|
|
399
|
+
# history, so do not reduce a stalled response to visible text only.
|
|
400
|
+
messages.append(
|
|
401
|
+
{
|
|
402
|
+
"role": "assistant",
|
|
403
|
+
"content": copy.deepcopy(assistant_content),
|
|
404
|
+
}
|
|
405
|
+
)
|
|
406
|
+
else:
|
|
407
|
+
assistant_text = message_text(message).strip()
|
|
408
|
+
if assistant_text:
|
|
409
|
+
messages.append(
|
|
410
|
+
{
|
|
411
|
+
"role": "assistant",
|
|
412
|
+
"content": [{"type": "text", "text": assistant_text}],
|
|
413
|
+
}
|
|
414
|
+
)
|
|
364
415
|
continuation = {"role": "user", "content": [{"type": "text", "text": nudge}]}
|
|
365
416
|
if control:
|
|
366
417
|
continuation[RUNTIME_CONTROL_MESSAGE_KEY] = control
|
|
@@ -370,6 +421,36 @@ def body_with_continuation_nudge(
|
|
|
370
421
|
return retried
|
|
371
422
|
|
|
372
423
|
|
|
424
|
+
def body_with_completion_check(
|
|
425
|
+
body: dict[str, Any], message: dict[str, Any]
|
|
426
|
+
) -> dict[str, Any]:
|
|
427
|
+
"""Offer a runtime-owned completion tool only to the private check call."""
|
|
428
|
+
|
|
429
|
+
projected = body_with_continuation_nudge(
|
|
430
|
+
body, message, CODEX_STRICT_CONTINUATION_NUDGE
|
|
431
|
+
)
|
|
432
|
+
tools = copy.deepcopy(list(projected.get("tools") or []))
|
|
433
|
+
if not any(
|
|
434
|
+
isinstance(tool, dict) and tool.get("name") == CODEX_COMPLETION_TOOL_NAME
|
|
435
|
+
for tool in tools
|
|
436
|
+
):
|
|
437
|
+
tools.append(
|
|
438
|
+
{
|
|
439
|
+
"name": CODEX_COMPLETION_TOOL_NAME,
|
|
440
|
+
"description": "Confirm that every action requested by the user is complete.",
|
|
441
|
+
"input_schema": {
|
|
442
|
+
"type": "object",
|
|
443
|
+
"properties": {},
|
|
444
|
+
"required": [],
|
|
445
|
+
"additionalProperties": False,
|
|
446
|
+
},
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
projected["tools"] = tools
|
|
450
|
+
projected["tool_choice"] = {"type": "any"}
|
|
451
|
+
return projected
|
|
452
|
+
|
|
453
|
+
|
|
373
454
|
def body_with_codex_compat_instructions(
|
|
374
455
|
body: dict[str, Any],
|
|
375
456
|
compat_prompt: str,
|
|
@@ -380,12 +461,13 @@ def body_with_codex_compat_instructions(
|
|
|
380
461
|
"""Append the routed Codex compatibility instruction to a Responses request.
|
|
381
462
|
|
|
382
463
|
Codex rejects ``--append-system-prompt`` as a Claude-only flag, so the
|
|
383
|
-
instruction has to travel in the request body.
|
|
384
|
-
|
|
385
|
-
rewriting instructions there would only invalidate the cached prefix.
|
|
464
|
+
instruction has to travel in the request body. The text is constant and
|
|
465
|
+
appended only once to keep the upstream cached prefix stable.
|
|
386
466
|
"""
|
|
387
467
|
|
|
388
|
-
|
|
468
|
+
# Native Codex needs the same pre-completion contract. Its separate SSE
|
|
469
|
+
# completion gate validates the response structure after generation.
|
|
470
|
+
if not isinstance(body, dict) or (not is_native_codex and not compat_enabled):
|
|
389
471
|
return body
|
|
390
472
|
existing = str(body.get("instructions") or "")
|
|
391
473
|
if compat_prompt in existing:
|
|
@@ -416,11 +498,12 @@ def recover_preamble_only_turn(
|
|
|
416
498
|
message: dict[str, Any],
|
|
417
499
|
services: CodexTurnRecoveryServices,
|
|
418
500
|
) -> dict[str, Any]:
|
|
419
|
-
"""Retry
|
|
501
|
+
"""Retry when the model announced work but called no tool.
|
|
420
502
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
503
|
+
Ordinary providers get one extra upstream call. Kimi gets a bounded series
|
|
504
|
+
because observed routed Codex turns can return several consecutive progress
|
|
505
|
+
announcements. A retry wins if it produces a tool call or a substantive
|
|
506
|
+
concrete answer; repeated announcements do not replace the original reply.
|
|
424
507
|
"""
|
|
425
508
|
|
|
426
509
|
if not isinstance(message, dict) or message_has_tool_use(message):
|
|
@@ -435,16 +518,18 @@ def recover_preamble_only_turn(
|
|
|
435
518
|
reasoning_output_budget = (
|
|
436
519
|
reasoning_only and message_exhausted_reasoning_output_budget(message)
|
|
437
520
|
)
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
and
|
|
521
|
+
kimi_turn = _is_kimi_turn(provider, body)
|
|
522
|
+
completion_check = (
|
|
523
|
+
not empty_end_turn
|
|
524
|
+
and not repeated_tool_guard
|
|
525
|
+
and not reasoning_only
|
|
526
|
+
and message_requires_completion_check(body, message)
|
|
442
527
|
)
|
|
443
528
|
if (
|
|
444
529
|
not empty_end_turn
|
|
445
530
|
and not repeated_tool_guard
|
|
446
531
|
and not reasoning_only
|
|
447
|
-
and not
|
|
532
|
+
and not completion_check
|
|
448
533
|
and not services.should_retry(body, text, [])
|
|
449
534
|
):
|
|
450
535
|
return message
|
|
@@ -458,8 +543,8 @@ def recover_preamble_only_turn(
|
|
|
458
543
|
if reasoning_output_budget
|
|
459
544
|
else "reasoning_only"
|
|
460
545
|
if reasoning_only
|
|
461
|
-
else "
|
|
462
|
-
if
|
|
546
|
+
else "completion_check"
|
|
547
|
+
if completion_check
|
|
463
548
|
else "preamble_only"
|
|
464
549
|
)
|
|
465
550
|
services.log(
|
|
@@ -467,73 +552,142 @@ def recover_preamble_only_turn(
|
|
|
467
552
|
f"codex_turn_retry provider={provider} reason={reason} "
|
|
468
553
|
f"model={str(body.get('model') or '-')} chars={len(text.strip())}",
|
|
469
554
|
)
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
555
|
+
nudge = (
|
|
556
|
+
CODEX_REPEATED_TOOL_CONTINUATION_NUDGE
|
|
557
|
+
if repeated_tool_guard
|
|
558
|
+
else CODEX_EMPTY_REASONING_CONTINUATION_NUDGE
|
|
559
|
+
if empty_end_turn or reasoning_only
|
|
560
|
+
else CODEX_STRICT_CONTINUATION_NUDGE
|
|
561
|
+
if completion_check
|
|
562
|
+
else CODEX_CONTINUATION_NUDGE
|
|
563
|
+
)
|
|
564
|
+
recovery_config = dict(pcfg)
|
|
565
|
+
if kimi_turn:
|
|
566
|
+
recovery_config["gateway_retries"] = 0
|
|
567
|
+
# Keep the original request as the stable prefix for every retry. Only the
|
|
568
|
+
# latest stalled assistant response is replayed below; accumulating every
|
|
569
|
+
# failed announcement teaches the same bad pattern back to the model and
|
|
570
|
+
# grows an already-large Codex request on every attempt.
|
|
571
|
+
replay_body = body
|
|
572
|
+
replay_message = (
|
|
573
|
+
message_without_repeated_tool_notice(message)
|
|
574
|
+
if repeated_tool_guard
|
|
575
|
+
else message_without_empty_end_turn_notice(message)
|
|
576
|
+
if empty_end_turn
|
|
577
|
+
else message_without_reasoning_notice(message)
|
|
578
|
+
if reasoning_only
|
|
579
|
+
else message
|
|
580
|
+
)
|
|
581
|
+
# Ollama Desktop keeps looping while agent work is active. Codex itself
|
|
582
|
+
# owns tool execution, so Ciel can only continue no-tool announcements.
|
|
583
|
+
# Kimi K3 gets a small bounded loop because one retry is observably not
|
|
584
|
+
# enough when it emits another progress announcement.
|
|
585
|
+
max_attempts = (
|
|
586
|
+
_KIMI_CONTINUATION_MAX_ATTEMPTS
|
|
587
|
+
if kimi_turn and not (empty_end_turn or reasoning_only or repeated_tool_guard)
|
|
588
|
+
else 1
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
for attempt in range(1, max_attempts + 1):
|
|
592
|
+
try:
|
|
593
|
+
attempt_nudge = (
|
|
594
|
+
CODEX_STRICT_CONTINUATION_NUDGE
|
|
595
|
+
if kimi_turn
|
|
596
|
+
and attempt > _KIMI_STRICT_CONTINUATION_AFTER_ATTEMPT
|
|
597
|
+
and not (empty_end_turn or reasoning_only or repeated_tool_guard)
|
|
598
|
+
else nudge
|
|
599
|
+
)
|
|
600
|
+
retry_body = (
|
|
601
|
+
body_with_completion_check(replay_body, replay_message)
|
|
602
|
+
if completion_check
|
|
603
|
+
else body_with_continuation_nudge(
|
|
604
|
+
replay_body,
|
|
605
|
+
replay_message,
|
|
606
|
+
attempt_nudge,
|
|
607
|
+
control=(
|
|
608
|
+
RUNTIME_REPEATED_TOOL_RECOVERY if repeated_tool_guard else None
|
|
609
|
+
),
|
|
501
610
|
)
|
|
502
|
-
|
|
611
|
+
)
|
|
612
|
+
recovery_strategy = "prompt_only"
|
|
613
|
+
if (
|
|
614
|
+
attempt == 1
|
|
615
|
+
and reasoning_output_budget
|
|
616
|
+
and services.prepare_reasoning_budget_retry
|
|
617
|
+
):
|
|
618
|
+
try:
|
|
619
|
+
recovery_config, retry_body, recovery_strategy = (
|
|
620
|
+
services.prepare_reasoning_budget_retry(
|
|
621
|
+
provider, recovery_config, retry_body
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
except Exception as exc: # noqa: BLE001 - retain safe prompt-only retry
|
|
625
|
+
services.log(
|
|
626
|
+
"WARN",
|
|
627
|
+
"codex_reasoning_budget_recovery_projection_failed "
|
|
628
|
+
f"provider={provider} error={type(exc).__name__}: {exc}",
|
|
629
|
+
)
|
|
503
630
|
services.log(
|
|
504
631
|
"WARN",
|
|
505
|
-
"
|
|
506
|
-
f"
|
|
632
|
+
f"codex_reasoning_budget_recovery provider={provider} "
|
|
633
|
+
f"model={str(body.get('model') or '-')} strategy={recovery_strategy}",
|
|
507
634
|
)
|
|
635
|
+
retried = services.collect_message(
|
|
636
|
+
handler,
|
|
637
|
+
provider,
|
|
638
|
+
recovery_config,
|
|
639
|
+
retry_body,
|
|
640
|
+
)
|
|
641
|
+
except Exception as exc: # noqa: BLE001 - recovery must never fail the turn
|
|
508
642
|
services.log(
|
|
509
643
|
"WARN",
|
|
510
|
-
f"
|
|
511
|
-
f"
|
|
644
|
+
f"codex_preamble_only_turn_retry_failed attempt={attempt}/{max_attempts} "
|
|
645
|
+
f"error={type(exc).__name__}: {exc}",
|
|
646
|
+
)
|
|
647
|
+
return message
|
|
648
|
+
if not isinstance(retried, dict):
|
|
649
|
+
return message
|
|
650
|
+
if empty_end_turn or reasoning_only or repeated_tool_guard:
|
|
651
|
+
if message_confirms_completion(retried):
|
|
652
|
+
return message
|
|
653
|
+
if (
|
|
654
|
+
message_has_only_runtime_stall_notice(retried)
|
|
655
|
+
or (
|
|
656
|
+
not message_has_tool_use(retried)
|
|
657
|
+
and not message_text(retried).strip()
|
|
658
|
+
)
|
|
659
|
+
):
|
|
660
|
+
return message
|
|
661
|
+
return retried
|
|
662
|
+
if message_confirms_completion(retried):
|
|
663
|
+
services.log(
|
|
664
|
+
"INFO",
|
|
665
|
+
f"codex_completion_confirmed provider={provider} "
|
|
666
|
+
f"attempt={attempt}/{max_attempts}",
|
|
512
667
|
)
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
668
|
+
return message
|
|
669
|
+
if message_has_tool_use(retried):
|
|
670
|
+
return _merged(message, retried)
|
|
671
|
+
|
|
672
|
+
retried_text = message_text(retried)
|
|
673
|
+
retryable = services.should_retry(retry_body, retried_text, []) or (
|
|
674
|
+
kimi_turn and message_requires_completion_check(retry_body, retried)
|
|
518
675
|
)
|
|
519
|
-
except Exception as exc: # noqa: BLE001 - recovery must never fail the turn
|
|
520
676
|
services.log(
|
|
521
|
-
"WARN",
|
|
522
|
-
f"
|
|
677
|
+
"WARN" if retryable else "INFO",
|
|
678
|
+
f"codex_turn_retry_result provider={provider} "
|
|
679
|
+
f"attempt={attempt}/{max_attempts} retryable={str(retryable).lower()} "
|
|
680
|
+
f"chars={len(retried_text.strip())}",
|
|
523
681
|
)
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if
|
|
529
|
-
message_has_only_runtime_stall_notice(retried)
|
|
530
|
-
or (not message_has_tool_use(retried) and not message_text(retried).strip())
|
|
531
|
-
):
|
|
682
|
+
if not retryable and retried_text.strip():
|
|
683
|
+
# A concrete no-tool answer is a valid completion. The old path
|
|
684
|
+
# discarded it and exposed the original announcement instead.
|
|
685
|
+
return retried
|
|
686
|
+
if attempt == max_attempts:
|
|
532
687
|
return message
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
return _merged(message, retried)
|
|
688
|
+
replay_message = retried
|
|
689
|
+
|
|
690
|
+
return message
|
|
537
691
|
|
|
538
692
|
|
|
539
693
|
def _merged(original: dict[str, Any], retried: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -552,12 +706,14 @@ def _merged(original: dict[str, Any], retried: dict[str, Any]) -> dict[str, Any]
|
|
|
552
706
|
|
|
553
707
|
__all__ = [
|
|
554
708
|
"CODEX_CONTINUATION_NUDGE",
|
|
709
|
+
"CODEX_COMPLETION_TOOL_NAME",
|
|
555
710
|
"CODEX_EMPTY_REASONING_CONTINUATION_NUDGE",
|
|
556
711
|
"CODEX_REPEATED_TOOL_CONTINUATION_NUDGE",
|
|
557
712
|
"RUNTIME_EMPTY_END_TURN_NOTICE_PREFIX",
|
|
558
713
|
"RUNTIME_REASONING_OUTPUT_BUDGET_NOTICE_PREFIX",
|
|
559
714
|
"CodexTurnRecoveryServices",
|
|
560
715
|
"body_with_codex_compat_instructions",
|
|
716
|
+
"body_with_completion_check",
|
|
561
717
|
"body_with_continuation_nudge",
|
|
562
718
|
"message_has_tool_use",
|
|
563
719
|
"message_exhausted_reasoning_output_budget",
|
|
@@ -566,7 +722,8 @@ __all__ = [
|
|
|
566
722
|
"message_has_only_empty_end_turn_notice",
|
|
567
723
|
"message_has_only_repeated_tool_notice",
|
|
568
724
|
"message_has_only_runtime_stall_notice",
|
|
569
|
-
"
|
|
725
|
+
"message_confirms_completion",
|
|
726
|
+
"message_requires_completion_check",
|
|
570
727
|
"message_without_empty_end_turn_notice",
|
|
571
728
|
"message_without_repeated_tool_notice",
|
|
572
729
|
"message_without_reasoning_notice",
|
|
@@ -51,7 +51,10 @@ class CompatibilityProtocolCodec:
|
|
|
51
51
|
def tool_request(self, model: str) -> dict[str, Any]:
|
|
52
52
|
return {
|
|
53
53
|
"model": model,
|
|
54
|
-
|
|
54
|
+
# Reasoning models spend from the same output-token budget before
|
|
55
|
+
# emitting a tool call. Keep the historical floor for compact
|
|
56
|
+
# models, while allowing model presets to reserve enough room.
|
|
57
|
+
"max_tokens": max(128, self.ports.max_tokens_for_model(model)),
|
|
55
58
|
"stream": False,
|
|
56
59
|
"messages": [
|
|
57
60
|
{
|
|
@@ -76,7 +79,7 @@ class CompatibilityProtocolCodec:
|
|
|
76
79
|
)
|
|
77
80
|
return {
|
|
78
81
|
"model": model,
|
|
79
|
-
"max_tokens": 64,
|
|
82
|
+
"max_tokens": max(64, self.ports.max_tokens_for_model(model)),
|
|
80
83
|
"stream": False,
|
|
81
84
|
"messages": [
|
|
82
85
|
{
|
|
@@ -8,6 +8,7 @@ from typing import Any, Callable
|
|
|
8
8
|
|
|
9
9
|
@dataclass(frozen=True, slots=True)
|
|
10
10
|
class ConfigMigrationPolicy:
|
|
11
|
+
anthropic_defaults_to_one_million_context: Callable[..., Any]
|
|
11
12
|
default_request_timeout_ms: int
|
|
12
13
|
kimi_k3_model: str
|
|
13
14
|
opencode_provider_names: tuple[str, ...]
|
|
@@ -20,6 +21,9 @@ class ConfigMigrationPolicy:
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def apply_config_migrations(cfg: dict[str, Any], *, policy: ConfigMigrationPolicy) -> None:
|
|
24
|
+
anthropic_defaults_to_one_million_context = (
|
|
25
|
+
policy.anthropic_defaults_to_one_million_context
|
|
26
|
+
)
|
|
23
27
|
DEFAULT_REQUEST_TIMEOUT_MS = policy.default_request_timeout_ms
|
|
24
28
|
KIMI_K3_MODEL = policy.kimi_k3_model
|
|
25
29
|
OPENCODE_PROVIDER_NAMES = policy.opencode_provider_names
|
|
@@ -34,6 +38,62 @@ def apply_config_migrations(cfg: dict[str, Any], *, policy: ConfigMigrationPolic
|
|
|
34
38
|
migrations = {}
|
|
35
39
|
cfg["migrations"] = migrations
|
|
36
40
|
|
|
41
|
+
marker = "meta_muse_spark_13_catalog_20260902"
|
|
42
|
+
if not migrations.get(marker):
|
|
43
|
+
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
44
|
+
pcfg = providers.get("meta")
|
|
45
|
+
if isinstance(pcfg, dict):
|
|
46
|
+
custom = pcfg.get("custom_models")
|
|
47
|
+
if not isinstance(custom, list):
|
|
48
|
+
custom = []
|
|
49
|
+
pcfg["custom_models"] = custom
|
|
50
|
+
known = {
|
|
51
|
+
normalize_model_id("meta", str(model))
|
|
52
|
+
for model in custom
|
|
53
|
+
if str(model).strip()
|
|
54
|
+
}
|
|
55
|
+
for model in (
|
|
56
|
+
"muse-spark-1.3",
|
|
57
|
+
"muse-spark-1.3-contributor",
|
|
58
|
+
"muse-spark-1.2",
|
|
59
|
+
"muse-spark-1.2-contributor",
|
|
60
|
+
"muse-spark-1.1",
|
|
61
|
+
):
|
|
62
|
+
normalized = normalize_model_id("meta", model)
|
|
63
|
+
if normalized not in known:
|
|
64
|
+
custom.append(model)
|
|
65
|
+
known.add(normalized)
|
|
66
|
+
migrations[marker] = True
|
|
67
|
+
|
|
68
|
+
marker = "anthropic_default_1m_model_ids_20260902"
|
|
69
|
+
if not migrations.get(marker):
|
|
70
|
+
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
71
|
+
pcfg = providers.get("anthropic")
|
|
72
|
+
if isinstance(pcfg, dict):
|
|
73
|
+
current = str(pcfg.get("current_model") or "").strip()
|
|
74
|
+
if (
|
|
75
|
+
current
|
|
76
|
+
and "[1m]" not in current.lower()
|
|
77
|
+
and anthropic_defaults_to_one_million_context(current)
|
|
78
|
+
):
|
|
79
|
+
pcfg["current_model"] = f"{current}[1m]"
|
|
80
|
+
custom = pcfg.get("custom_models")
|
|
81
|
+
if not isinstance(custom, list):
|
|
82
|
+
custom = []
|
|
83
|
+
pcfg["custom_models"] = custom
|
|
84
|
+
known = {str(model).strip().casefold() for model in custom}
|
|
85
|
+
for model in (
|
|
86
|
+
"claude-fable-5-1[1m]",
|
|
87
|
+
"claude-fable-5[1m]",
|
|
88
|
+
"claude-opus-5[1m]",
|
|
89
|
+
"claude-opus-4-8[1m]",
|
|
90
|
+
"claude-sonnet-5[1m]",
|
|
91
|
+
"claude-sonnet-4-6[1m]",
|
|
92
|
+
):
|
|
93
|
+
if model.casefold() not in known:
|
|
94
|
+
custom.append(model)
|
|
95
|
+
migrations[marker] = True
|
|
96
|
+
|
|
37
97
|
marker = "zcode_wire_version_0163_20260824"
|
|
38
98
|
if not migrations.get(marker):
|
|
39
99
|
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
@@ -191,6 +251,25 @@ def apply_config_migrations(cfg: dict[str, Any], *, policy: ConfigMigrationPolic
|
|
|
191
251
|
pcfg["max_model_len"] = 1_000_000
|
|
192
252
|
migrations[marker] = True
|
|
193
253
|
|
|
254
|
+
marker = "alibaba_qwen38_0902_catalog_20260902"
|
|
255
|
+
if not migrations.get(marker):
|
|
256
|
+
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
257
|
+
model_studio = providers.get("alims-intl")
|
|
258
|
+
if isinstance(model_studio, dict):
|
|
259
|
+
custom = model_studio.get("custom_models")
|
|
260
|
+
if not isinstance(custom, list):
|
|
261
|
+
custom = []
|
|
262
|
+
model_studio["custom_models"] = custom
|
|
263
|
+
known = {
|
|
264
|
+
normalize_model_id("alims-intl", str(model))
|
|
265
|
+
for model in custom
|
|
266
|
+
if str(model).strip()
|
|
267
|
+
}
|
|
268
|
+
snapshot = "qwen3.8-max-0902"
|
|
269
|
+
if normalize_model_id("alims-intl", snapshot) not in known:
|
|
270
|
+
custom.insert(1 if custom else 0, snapshot)
|
|
271
|
+
migrations[marker] = True
|
|
272
|
+
|
|
194
273
|
marker = "alibaba_kimi_k3_20260831"
|
|
195
274
|
if not migrations.get(marker):
|
|
196
275
|
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
@@ -125,6 +125,13 @@ def build_default_config(provider_defaults: dict[str, Any]) -> dict[str, Any]:
|
|
|
125
125
|
"max_batch_bytes": 1048576,
|
|
126
126
|
"start_mode": "tail",
|
|
127
127
|
},
|
|
128
|
+
"tool_call_events": {
|
|
129
|
+
"enabled": True,
|
|
130
|
+
"poll_interval_ms": 500,
|
|
131
|
+
"max_batch_bytes": 1048576,
|
|
132
|
+
"start_mode": "tail",
|
|
133
|
+
"include_arguments": True,
|
|
134
|
+
},
|
|
128
135
|
"remote_instructions": {
|
|
129
136
|
"enabled": False,
|
|
130
137
|
"claude_url": "",
|
|
@@ -161,6 +161,25 @@ class ContextSummaryPolicy:
|
|
|
161
161
|
if (start := (index * count) // chunks) < (end := ((index + 1) * count) // chunks)
|
|
162
162
|
]
|
|
163
163
|
|
|
164
|
+
@staticmethod
|
|
165
|
+
def cache_stable_summary_budget(budget_tokens: int) -> int:
|
|
166
|
+
"""Bucket a wire-fit budget before it can shape a cached summary.
|
|
167
|
+
|
|
168
|
+
Provider request fitting recalculates ``budget_tokens`` from the exact
|
|
169
|
+
serialized byte count. Appending one turn can therefore move that
|
|
170
|
+
value by a few tokens even when the omitted history is unchanged. A
|
|
171
|
+
prompt cache requires an exact prefix, so embedding or otherwise using
|
|
172
|
+
that per-request value would regenerate the first compacted item.
|
|
173
|
+
|
|
174
|
+
Keep the real budget in the compactor's final fit checks. The summary
|
|
175
|
+
projection only needs a conservative, stable size class; rounding down
|
|
176
|
+
to the compactor's minimum quantum makes nearby fits byte-identical and
|
|
177
|
+
never grants the summary a larger budget than the request has.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
normalized = max(8192, int(budget_tokens))
|
|
181
|
+
return max(8192, (normalized // 8192) * 8192)
|
|
182
|
+
|
|
164
183
|
def guard_chunk_count(
|
|
165
184
|
self,
|
|
166
185
|
omitted_messages: list[dict[str, Any]],
|
|
@@ -185,13 +204,14 @@ class ContextSummaryPolicy:
|
|
|
185
204
|
if count <= 0:
|
|
186
205
|
return (
|
|
187
206
|
"[ciel-runtime context guard: older conversation history was compacted because "
|
|
188
|
-
|
|
207
|
+
"the provider context budget was exceeded.]"
|
|
189
208
|
)
|
|
190
|
-
|
|
209
|
+
summary_budget = self.cache_stable_summary_budget(budget_tokens)
|
|
210
|
+
max_tokens = max(1024, min(24576, summary_budget // 10))
|
|
191
211
|
max_chars = max_tokens * 4
|
|
192
|
-
chunks = self.guard_chunk_count(omitted_messages,
|
|
212
|
+
chunks = self.guard_chunk_count(omitted_messages, summary_budget)
|
|
193
213
|
lines = [
|
|
194
|
-
f"[ciel-runtime context guard: compacted {count} older messages, approx {tokens} tokens, because the provider context budget
|
|
214
|
+
f"[ciel-runtime context guard: compacted {count} older messages, approx {tokens} tokens, because the provider context budget was exceeded.]",
|
|
195
215
|
"The recent tail is preserved verbatim. Older history is represented below as deterministic chunk summaries; use file reads or MCP queries if exact old content is needed.",
|
|
196
216
|
]
|
|
197
217
|
for number, (start, end) in enumerate(self.chunk_ranges(count, chunks), start=1):
|