@oneciel-ai/ciel-runtime 0.2.47 → 0.2.48
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 +4 -4
- package/ciel_runtime_support/architecture.py +28 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +6 -1
- package/ciel_runtime_support/codex_completion_gate.py +17 -3
- package/ciel_runtime_support/codex_turn_recovery.py +42 -6
- package/ciel_runtime_support/compatibility_test.py +3 -2
- package/ciel_runtime_support/config_migrations.py +44 -0
- package/ciel_runtime_support/provider_request_access.py +3 -4
- package/ciel_runtime_support/provider_responses_passthrough.py +167 -26
- package/ciel_runtime_support/providers/meta.py +5 -0
- package/ciel_runtime_support/providers/opencode.py +64 -1
- package/ciel_runtime_support/providers/opencode_catalog.py +8 -2
- package/ciel_runtime_support/providers/opencode_go.py +20 -2
- package/ciel_runtime_support/providers/openrouter.py +26 -2
- package/ciel_runtime_support/runtime_constants.py +1 -1
- package/ciel_runtime_support/windows_conpty.py +14 -0
- package/ciel_runtime_support/windows_console_guard.py +96 -0
- package/docs/journal/2026/09/16/diagnostics/opencode/go-session-header.okf +26 -0
- package/docs/journal/2026/09/16/fixes/opencode/kimi-k3-turn-recovery.okf +21 -0
- package/docs/journal/2026/09/16/providers/opencode/union-alpha.okf +34 -0
- package/docs/journal/2026/09/17/analysis/codebase/structure-survey.okf +22 -0
- package/docs/journal/2026/09/17/diagnostics/riskonnect/wsg-screenshot.okf +15 -0
- package/docs/journal/2026/09/17/diagnostics/yeti-01/duplicate-final-and-stall.okf +29 -0
- package/docs/journal/2026/09/17/fixes/codex/passthrough-compaction-completion-gate.okf +30 -0
- package/docs/journal/2026/09/17/providers/opencode/union-alpha-go-zen-vs-opencode-client.okf +53 -0
- package/docs/journal/2026/09/17/providers/openrouter/union-alpha-catalog.okf +29 -0
- package/docs/journal/2026/09/17/releases/codex/general-completion-gate-deployment.okf +24 -0
- package/docs/journal/2026/09/17/releases/codex/turn-gate-deployment.okf +27 -0
- package/docs/journal/2026/09/17/releases/local-nightly/deploy-bff7596.okf +28 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""OpenCode Zen provider adapter."""
|
|
2
2
|
|
|
3
|
+
import secrets
|
|
3
4
|
from dataclasses import dataclass, field
|
|
4
5
|
from typing import Mapping
|
|
5
6
|
|
|
@@ -20,11 +21,39 @@ from .base import (
|
|
|
20
21
|
provider_configuration,
|
|
21
22
|
)
|
|
22
23
|
from .constants import DEFAULT_REQUEST_TIMEOUT_MS, PROVIDER_DEFAULT_BASE_URLS
|
|
23
|
-
from .opencode_catalog import
|
|
24
|
+
from .opencode_catalog import (
|
|
25
|
+
OPENCODE_UNION_ALPHA_CONTEXT_WINDOW,
|
|
26
|
+
OPENCODE_UNION_ALPHA_MAX_OUTPUT_TOKENS,
|
|
27
|
+
OPENCODE_ZEN_MODEL_PROTOCOLS,
|
|
28
|
+
)
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
OPENCODE_ZEN_OX_ALPHA_FREE_MODEL = "x-preview-f-free"
|
|
27
32
|
OPENCODE_GO_OX_ALPHA_FREE_MODEL = "ox-alpha-free"
|
|
33
|
+
# Latest OpenCode CLI release (2026-09-17, GitHub tag v1.18.31), used only to
|
|
34
|
+
# present the same User-Agent the OpenCode client sends to the opencode
|
|
35
|
+
# gateway (packages/opencode/src/session/llm/request.ts).
|
|
36
|
+
OPENCODE_CLIENT_VERSION = "1.18.31"
|
|
37
|
+
_BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _new_opencode_id(prefix: str) -> str:
|
|
41
|
+
# OpenCode ID shape as stored by the client itself (session: ses_...,
|
|
42
|
+
# message: msg_...), 24 base62 characters after the prefix.
|
|
43
|
+
return prefix + "".join(secrets.choice(_BASE62) for _ in range(24))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def new_opencode_session_id() -> str:
|
|
47
|
+
return _new_opencode_id("ses_")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def new_opencode_message_id() -> str:
|
|
51
|
+
return _new_opencode_id("msg_")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# One router process serves one workspace conversation, so its own requests
|
|
55
|
+
# (advisor, compaction, probes) share one stable session for routing/caching.
|
|
56
|
+
_ROUTER_SESSION_ID = new_opencode_session_id()
|
|
28
57
|
|
|
29
58
|
|
|
30
59
|
@dataclass(frozen=True)
|
|
@@ -87,6 +116,37 @@ class OpenCodeProviderAdapter(HttpBearerProviderAdapter):
|
|
|
87
116
|
hosted_timeout=True,
|
|
88
117
|
)
|
|
89
118
|
|
|
119
|
+
def model_configuration_profile(
|
|
120
|
+
self, config: ProviderConfig
|
|
121
|
+
) -> tuple[Mapping[str, object], str | None]:
|
|
122
|
+
if self.normalize_model_id(config.model).casefold() != "union-alpha":
|
|
123
|
+
return {}, None
|
|
124
|
+
return (
|
|
125
|
+
{
|
|
126
|
+
"context_window": OPENCODE_UNION_ALPHA_CONTEXT_WINDOW,
|
|
127
|
+
"max_model_len": OPENCODE_UNION_ALPHA_CONTEXT_WINDOW,
|
|
128
|
+
"max_output_tokens": OPENCODE_UNION_ALPHA_MAX_OUTPUT_TOKENS,
|
|
129
|
+
"supports_vision": True,
|
|
130
|
+
"model_profile": "opencode-union-alpha-262k",
|
|
131
|
+
},
|
|
132
|
+
"OpenCode Union Alpha profile applied: 262,144-token context and "
|
|
133
|
+
"131,072-token maximum output.",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def session_headers(self, config: ProviderConfig) -> Mapping[str, str]:
|
|
137
|
+
# Present the OpenCode CLI identity to the opencode gateway (Go and
|
|
138
|
+
# Zen): the client sends User-Agent opencode/<version>,
|
|
139
|
+
# x-opencode-client, a stable x-opencode-session per conversation and a
|
|
140
|
+
# per-message x-opencode-request id (packages/opencode/src/session/
|
|
141
|
+
# llm/request.ts). Without the session Go answers 400 MissingSessionID.
|
|
142
|
+
del config
|
|
143
|
+
return {
|
|
144
|
+
"x-opencode-session": _ROUTER_SESSION_ID,
|
|
145
|
+
"x-opencode-request": new_opencode_message_id(),
|
|
146
|
+
"x-opencode-client": "cli",
|
|
147
|
+
"user-agent": f"opencode/{OPENCODE_CLIENT_VERSION}",
|
|
148
|
+
}
|
|
149
|
+
|
|
90
150
|
def router_native_anthropic_enabled(
|
|
91
151
|
self, config: ProviderConfig, model: str | None = None
|
|
92
152
|
) -> bool:
|
|
@@ -242,7 +302,10 @@ class OpenCodeProviderAdapter(HttpBearerProviderAdapter):
|
|
|
242
302
|
|
|
243
303
|
|
|
244
304
|
__all__ = [
|
|
305
|
+
"OPENCODE_CLIENT_VERSION",
|
|
245
306
|
"OPENCODE_GO_OX_ALPHA_FREE_MODEL",
|
|
246
307
|
"OPENCODE_ZEN_OX_ALPHA_FREE_MODEL",
|
|
247
308
|
"OpenCodeProviderAdapter",
|
|
309
|
+
"new_opencode_message_id",
|
|
310
|
+
"new_opencode_session_id",
|
|
248
311
|
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Official OpenCode endpoint snapshot (2026-09-
|
|
1
|
+
"""Official OpenCode endpoint snapshot (Union Alpha verified 2026-09-16).
|
|
2
2
|
|
|
3
3
|
Sources: https://opencode.ai/docs/zen/ and https://opencode.ai/docs/go/.
|
|
4
4
|
Live model discovery remains authoritative for availability; this snapshot
|
|
@@ -6,7 +6,13 @@ provides offline choices and exact per-plan protocol routing. No model capacity
|
|
|
6
6
|
or sampling parameters are inferred from model names.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
# OpenCode's models.dev catalog publishes the same Union Alpha limits for
|
|
10
|
+
# opencode and opencode-go: https://models.dev/api.json
|
|
11
|
+
OPENCODE_UNION_ALPHA_CONTEXT_WINDOW = 262144
|
|
12
|
+
OPENCODE_UNION_ALPHA_MAX_OUTPUT_TOKENS = 131072
|
|
13
|
+
|
|
9
14
|
OPENCODE_ZEN_MODEL_PROTOCOLS = {
|
|
15
|
+
"union-alpha": "anthropic_messages",
|
|
10
16
|
"gpt-6-astra": "openai_responses",
|
|
11
17
|
"gpt-5.6-sol": "openai_responses",
|
|
12
18
|
"gpt-5.6-terra": "openai_responses",
|
|
@@ -79,6 +85,7 @@ OPENCODE_ZEN_MODEL_PROTOCOLS = {
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
OPENCODE_GO_MODEL_PROTOCOLS = {
|
|
88
|
+
"union-alpha": "anthropic_messages",
|
|
82
89
|
"grok-4.6": "openai_responses",
|
|
83
90
|
"gpt-5.6-luna": "openai_responses",
|
|
84
91
|
"glm-5.3-flash": "openai_chat",
|
|
@@ -108,4 +115,3 @@ OPENCODE_GO_MODEL_PROTOCOLS = {
|
|
|
108
115
|
"hy4-preview": "openai_chat",
|
|
109
116
|
"hy3": "openai_chat",
|
|
110
117
|
}
|
|
111
|
-
|
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
from dataclasses import dataclass, field
|
|
4
4
|
from typing import Mapping
|
|
5
5
|
|
|
6
|
-
from ..architecture import MessageProtocol
|
|
6
|
+
from ..architecture import MessageProtocol, ProviderConfig
|
|
7
7
|
|
|
8
8
|
from .base import provider_configuration
|
|
9
9
|
from .constants import DEFAULT_REQUEST_TIMEOUT_MS, PROVIDER_DEFAULT_BASE_URLS
|
|
10
|
-
from .opencode import
|
|
10
|
+
from .opencode import (
|
|
11
|
+
OPENCODE_CLIENT_VERSION,
|
|
12
|
+
OPENCODE_GO_OX_ALPHA_FREE_MODEL,
|
|
13
|
+
OpenCodeProviderAdapter,
|
|
14
|
+
new_opencode_message_id,
|
|
15
|
+
new_opencode_session_id,
|
|
16
|
+
)
|
|
11
17
|
from .opencode_catalog import OPENCODE_GO_MODEL_PROTOCOLS
|
|
12
18
|
|
|
13
19
|
|
|
@@ -38,6 +44,18 @@ class OpenCodeGoProviderAdapter(OpenCodeProviderAdapter):
|
|
|
38
44
|
def documented_model_protocols(self) -> Mapping[str, MessageProtocol]:
|
|
39
45
|
return OPENCODE_GO_MODEL_PROTOCOLS
|
|
40
46
|
|
|
47
|
+
def compatibility_headers(self, config: ProviderConfig) -> Mapping[str, str]:
|
|
48
|
+
# Each compatibility probe is one fresh OpenCode conversation; the
|
|
49
|
+
# identity headers mirror the OpenCode CLI (see OpenCodeProviderAdapter
|
|
50
|
+
# .session_headers).
|
|
51
|
+
del config
|
|
52
|
+
return {
|
|
53
|
+
"x-opencode-session": new_opencode_session_id(),
|
|
54
|
+
"x-opencode-request": new_opencode_message_id(),
|
|
55
|
+
"x-opencode-client": "cli",
|
|
56
|
+
"user-agent": f"opencode/{OPENCODE_CLIENT_VERSION}",
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
api_key_launch_error_value: str = (
|
|
42
60
|
"Launch blocked: OpenCode Go requires a OpenCode Go API key."
|
|
43
61
|
)
|
|
@@ -16,6 +16,14 @@ from .constants import DEFAULT_REQUEST_TIMEOUT_MS, PROVIDER_DEFAULT_BASE_URLS
|
|
|
16
16
|
OPENROUTER_OX_ALPHA_MODEL = "stealth/ox-alpha"
|
|
17
17
|
OPENROUTER_OX_ALPHA_CONTEXT_WINDOW = 1_048_576
|
|
18
18
|
OPENROUTER_OX_ALPHA_MAX_OUTPUT_TOKENS = 131_072
|
|
19
|
+
# stealth/union-alpha ended its testing period on 2026-09-17 (POST replies
|
|
20
|
+
# 404 "This model was Unbiased's Pareto. Use it now:
|
|
21
|
+
# https://openrouter.ai/unbiased/pareto"); the same model continues as
|
|
22
|
+
# unbiased/pareto (GET /api/v1/models 2026-09-17: 262,144-token context).
|
|
23
|
+
# Only Chat Completions is documented, so it stays on openai_chat.
|
|
24
|
+
OPENROUTER_PARETO_MODEL = "unbiased/pareto"
|
|
25
|
+
OPENROUTER_PARETO_CONTEXT_WINDOW = 262_144
|
|
26
|
+
OPENROUTER_PARETO_MAX_OUTPUT_TOKENS = 131_072
|
|
19
27
|
|
|
20
28
|
|
|
21
29
|
@dataclass(frozen=True)
|
|
@@ -25,7 +33,7 @@ class OpenRouterProviderAdapter(OpenAICompatibleProviderAdapter):
|
|
|
25
33
|
configuration_defaults_value: dict = field(
|
|
26
34
|
default_factory=lambda: provider_configuration(
|
|
27
35
|
"nvidia/nemotron-3-ultra-550b-a55b:free",
|
|
28
|
-
custom_models=(OPENROUTER_OX_ALPHA_MODEL,),
|
|
36
|
+
custom_models=(OPENROUTER_OX_ALPHA_MODEL, OPENROUTER_PARETO_MODEL),
|
|
29
37
|
native_compat=False,
|
|
30
38
|
rate_limit_rpm=0,
|
|
31
39
|
rate_limit_status=False,
|
|
@@ -91,7 +99,20 @@ class OpenRouterProviderAdapter(OpenAICompatibleProviderAdapter):
|
|
|
91
99
|
def model_configuration_profile(
|
|
92
100
|
self, config: ProviderConfig
|
|
93
101
|
) -> tuple[Mapping[str, Any], str | None]:
|
|
94
|
-
|
|
102
|
+
selected = self.normalize_model_id(config.model)
|
|
103
|
+
if selected == OPENROUTER_PARETO_MODEL:
|
|
104
|
+
return (
|
|
105
|
+
{
|
|
106
|
+
"context_window": OPENROUTER_PARETO_CONTEXT_WINDOW,
|
|
107
|
+
"max_model_len": OPENROUTER_PARETO_CONTEXT_WINDOW,
|
|
108
|
+
"max_output_tokens": OPENROUTER_PARETO_MAX_OUTPUT_TOKENS,
|
|
109
|
+
"model_profile": "openrouter-pareto-262k",
|
|
110
|
+
"supports_tool_choice": True,
|
|
111
|
+
"supports_vision": True,
|
|
112
|
+
},
|
|
113
|
+
"OpenRouter Pareto profile applied: 262,144-token context and 131,072-token maximum output.",
|
|
114
|
+
)
|
|
115
|
+
if selected != OPENROUTER_OX_ALPHA_MODEL:
|
|
95
116
|
return {}, None
|
|
96
117
|
return (
|
|
97
118
|
{
|
|
@@ -126,5 +147,8 @@ __all__ = [
|
|
|
126
147
|
"OPENROUTER_OX_ALPHA_CONTEXT_WINDOW",
|
|
127
148
|
"OPENROUTER_OX_ALPHA_MAX_OUTPUT_TOKENS",
|
|
128
149
|
"OPENROUTER_OX_ALPHA_MODEL",
|
|
150
|
+
"OPENROUTER_PARETO_CONTEXT_WINDOW",
|
|
151
|
+
"OPENROUTER_PARETO_MAX_OUTPUT_TOKENS",
|
|
152
|
+
"OPENROUTER_PARETO_MODEL",
|
|
129
153
|
"OpenRouterProviderAdapter",
|
|
130
154
|
]
|
|
@@ -17,6 +17,7 @@ from .windows_terminal_modes import (
|
|
|
17
17
|
WindowsTerminalModeFilter,
|
|
18
18
|
)
|
|
19
19
|
from .terminal_input_frames import TerminalInputFrames
|
|
20
|
+
from .windows_console_guard import start_console_guard, stop_console_guard
|
|
20
21
|
from .windows_command_line import command_line_for_create_process
|
|
21
22
|
|
|
22
23
|
|
|
@@ -473,6 +474,10 @@ class WindowsConPtySession:
|
|
|
473
474
|
)
|
|
474
475
|
self._mirror_output = False
|
|
475
476
|
self._restore_parent_console()
|
|
477
|
+
guard = getattr(self, "_console_guard", None)
|
|
478
|
+
if guard is not None:
|
|
479
|
+
stop_console_guard(guard)
|
|
480
|
+
self._console_guard = None
|
|
476
481
|
|
|
477
482
|
def _write_parent_terminal_modes(self, sequence: str) -> bool:
|
|
478
483
|
with self._parent_output_lock():
|
|
@@ -779,6 +784,15 @@ class WindowsConPtySession:
|
|
|
779
784
|
self._old_output_mode = old_output_mode
|
|
780
785
|
self._stdout_console_handle = output_handle
|
|
781
786
|
self._parent_vt_output_ready = True
|
|
787
|
+
# The guard only restores console modes if this process dies
|
|
788
|
+
# before its own restore path runs; a failed start must not
|
|
789
|
+
# take the whole ConPTY session down with it.
|
|
790
|
+
try:
|
|
791
|
+
self._console_guard = start_console_guard(
|
|
792
|
+
self._old_input_mode, self._old_output_mode
|
|
793
|
+
)
|
|
794
|
+
except OSError:
|
|
795
|
+
self._console_guard = None
|
|
782
796
|
|
|
783
797
|
def _restore_parent_console(self) -> None:
|
|
784
798
|
kernel32 = self._kernel32
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Out-of-process restoration of shared console modes after owner termination.
|
|
2
|
+
|
|
3
|
+
The helper shares the existing console; it never creates a window. It opens its
|
|
4
|
+
own console handles before acknowledging readiness and waits on an owner process
|
|
5
|
+
handle, so PID reuse cannot cause it to watch a different process.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import threading
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def start_console_guard(input_mode: int, output_mode: int) -> subprocess.Popen:
|
|
18
|
+
child = subprocess.Popen(
|
|
19
|
+
[sys.executable, str(Path(__file__).resolve()), str(os.getpid()),
|
|
20
|
+
str(input_mode), str(output_mode)],
|
|
21
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
22
|
+
)
|
|
23
|
+
ready = threading.Event()
|
|
24
|
+
response = []
|
|
25
|
+
|
|
26
|
+
def read_ready() -> None:
|
|
27
|
+
try:
|
|
28
|
+
response.append(child.stdout.readline())
|
|
29
|
+
finally:
|
|
30
|
+
ready.set()
|
|
31
|
+
|
|
32
|
+
threading.Thread(target=read_ready, daemon=True).start()
|
|
33
|
+
if not ready.wait(5) or response != [b"READY\n"]:
|
|
34
|
+
stop_console_guard(child)
|
|
35
|
+
raise OSError("console recovery helper failed to become ready")
|
|
36
|
+
child.stdout.close()
|
|
37
|
+
return child
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def stop_console_guard(child: subprocess.Popen) -> None:
|
|
41
|
+
if child.poll() is None:
|
|
42
|
+
child.terminate()
|
|
43
|
+
child.wait(timeout=5)
|
|
44
|
+
if child.stdout is not None:
|
|
45
|
+
child.stdout.close()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _watch(owner_pid: int, input_mode: int, output_mode: int) -> int:
|
|
49
|
+
import ctypes
|
|
50
|
+
from ctypes import wintypes
|
|
51
|
+
|
|
52
|
+
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
53
|
+
kernel.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
|
54
|
+
kernel.OpenProcess.restype = wintypes.HANDLE
|
|
55
|
+
kernel.CreateFileW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD,
|
|
56
|
+
wintypes.LPVOID, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE]
|
|
57
|
+
kernel.CreateFileW.restype = wintypes.HANDLE
|
|
58
|
+
kernel.SetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.DWORD]
|
|
59
|
+
kernel.WriteConsoleW.argtypes = [wintypes.HANDLE, wintypes.LPCWSTR, wintypes.DWORD,
|
|
60
|
+
ctypes.POINTER(wintypes.DWORD), wintypes.LPVOID]
|
|
61
|
+
kernel.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
|
|
62
|
+
kernel.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
63
|
+
owner = kernel.OpenProcess(0x00100000, False, owner_pid)
|
|
64
|
+
handles = [owner]
|
|
65
|
+
try:
|
|
66
|
+
if not owner:
|
|
67
|
+
return 2
|
|
68
|
+
console_in = kernel.CreateFileW("CONIN$", 0xC0000000, 3, None, 3, 0, None)
|
|
69
|
+
console_out = kernel.CreateFileW("CONOUT$", 0xC0000000, 3, None, 3, 0, None)
|
|
70
|
+
handles.extend([console_in, console_out])
|
|
71
|
+
invalid = ctypes.c_void_p(-1).value
|
|
72
|
+
if any(h in (None, invalid) for h in handles):
|
|
73
|
+
return 3
|
|
74
|
+
sys.stdout.buffer.write(b"READY\n")
|
|
75
|
+
sys.stdout.buffer.flush()
|
|
76
|
+
if kernel.WaitForSingleObject(owner, 0xFFFFFFFF) != 0:
|
|
77
|
+
return 4
|
|
78
|
+
# The owner is gone; its finally/atexit handlers cannot restore this.
|
|
79
|
+
# Reset emulator modes while VT output is enabled, then restore exact
|
|
80
|
+
# Win32 modes. Do not flush pending user keystrokes from the input queue.
|
|
81
|
+
kernel.SetConsoleMode(console_out, output_mode | 5)
|
|
82
|
+
reset = "".join(f"\x1b[?{mode}l" for mode in
|
|
83
|
+
(9, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1015, 1016, 2004, 9001))
|
|
84
|
+
written = wintypes.DWORD()
|
|
85
|
+
kernel.WriteConsoleW(console_out, reset, len(reset), ctypes.byref(written), None)
|
|
86
|
+
kernel.SetConsoleMode(console_in, input_mode)
|
|
87
|
+
kernel.SetConsoleMode(console_out, output_mode)
|
|
88
|
+
return 0
|
|
89
|
+
finally:
|
|
90
|
+
for handle in handles:
|
|
91
|
+
if handle and handle != ctypes.c_void_p(-1).value:
|
|
92
|
+
kernel.CloseHandle(handle)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
raise SystemExit(_watch(*(int(value) for value in sys.argv[1:])))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
okf_version: "1.0"
|
|
2
|
+
task: Diagnose OpenCode Go Union Alpha 400 and 503 reports after nightly deployment.
|
|
3
|
+
user_observations:
|
|
4
|
+
compatibility: HTTP 400 MissingSessionID from Console Go during Text response.
|
|
5
|
+
codex: HTTP 503 overloaded_error, Console Go Upstream request failed: Endpoint is unavailable, via local router /v1/responses on port 9407.
|
|
6
|
+
official_source: https://opencode.ai/docs/ko/go/ (client requirements: distinct user agent and stable x-opencode-session per conversation; Codex native session header is recognized when forwarded)
|
|
7
|
+
source_evidence:
|
|
8
|
+
compatibility_test: ciel_runtime_support/compatibility_test.py creates provider_headers(provider, pcfg) without inbound headers or session ID before text/tool phases.
|
|
9
|
+
provider_request_access: No-inbound path creates content-type, anthropic-version, user-agent, and provider credentials, but no session header.
|
|
10
|
+
header_forwarding: Inbound end-to-end session headers are preserved by the generic forwarding policy when provided.
|
|
11
|
+
synthetic_capture: Compatibility header names contain no session header; a synthetic x-codex-session-id survives normal openai_responses header projection.
|
|
12
|
+
local_reachability: No listener on 127.0.0.1:9407 from this computer at inspection time, so the user's 503 request was not directly captured.
|
|
13
|
+
confirmed: The current compatibility test fails OpenCode Go's documented session-header requirement; the reported 400 is consistent with this confirmed omission.
|
|
14
|
+
unconfirmed: The 503 response has a different upstream error body. No evidence yet that it shares the MissingSessionID cause; upstream endpoint availability or a request-specific routing failure remain possibilities.
|
|
15
|
+
implementation:
|
|
16
|
+
adapter_contract: ProviderAdapter compatibility_headers defaults to empty.
|
|
17
|
+
go_adapter: OpenCodeGoProviderAdapter supplies one UUID x-opencode-session and Ciel Runtime user-agent for each compatibility-test run.
|
|
18
|
+
test_pipeline: The test creates the provider headers once and reuses them across text, tool-use, and tool-result phases.
|
|
19
|
+
production_proxy: Existing end-to-end forwarding of native Codex/Claude session headers is unchanged.
|
|
20
|
+
verification:
|
|
21
|
+
simulated_full_probe: Three test phases received the same x-opencode-session and completed with Compatibility OK.
|
|
22
|
+
regressions: OpenCode 63 tests and architecture-budget tests passed.
|
|
23
|
+
full_suite: npm test passed; unit 1444 (46 skipped), router 1108, channel 406 (80 skipped), runtime 287 (19 skipped).
|
|
24
|
+
static: Changed Python files pass Ruff; documentation metadata and git diff --check pass.
|
|
25
|
+
live_hosted: Not exercised because no OpenCode Go credential is configured on this computer.
|
|
26
|
+
503: No change claims to repair upstream Endpoint is unavailable; requires separate live request evidence.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
okf_version: "1.0"
|
|
2
|
+
task: "Prevent tool-backed Codex turns from silently ending with an unconfirmed text reply across routed and native Responses paths."
|
|
3
|
+
evidence:
|
|
4
|
+
live_transcript: "YETI-01 E:\\ciel-qb-mcp Codex session emitted task_complete after a progress reply without the promised work tool call."
|
|
5
|
+
route: "Router activity showed provider=opencode-go, model=kimi-k3, endpoint=https://opencode.ai/zen/go/v1/chat/completions."
|
|
6
|
+
code_gap: "Completion check previously required a reasoning block; the observed final replies had no reasoning block. Kimi's bounded retry did not recognize the opencode-go route."
|
|
7
|
+
cross_provider_gap: "Native OpenAI Responses completion gate also required a reasoning output item, so a text-only candidate after function_call_output skipped its independent confirmation path. This source-level condition is provider-independent; the upstream cause of text-only endings remains unverified."
|
|
8
|
+
corroborating_report: "OpenAI Codex GitHub issue #45096 reports a similar custom Responses premature completion; issue #43329 reports premature completion on a stock OpenAI backend. These are independent user reports, not proof of the cause in our sessions."
|
|
9
|
+
changes:
|
|
10
|
+
- "A visible no-tool reply after a tool_result now enters the private completion check regardless of reasoning-block presence."
|
|
11
|
+
- "Kimi K3 model identity enables the existing bounded retry across provider transports."
|
|
12
|
+
- "Plan Mode remains excluded through the existing policy callback."
|
|
13
|
+
- "Native OpenAI Responses gate now checks a text-only candidate when the current input ends with a function/custom tool output, even without a reasoning item."
|
|
14
|
+
verification:
|
|
15
|
+
regression_tests: "151 targeted tests passed: 47 turn recovery, 22 backend item repair, 6 completion gate, 18 reasoning-output recovery, and 58 OpenCode provider. Python compileall and git diff --check passed."
|
|
16
|
+
isolated_full_suite: "The repository's npm test command passed all four isolated groups on 2026-09-17: unit 1,444 (46 skipped), router 1,108, channel 406 (80 skipped), runtime 293 (19 skipped); 3,251 tests total."
|
|
17
|
+
provider_probe: "From YETI-01, authenticated OpenCode Go kimi-k3 Chat Completions with x-opencode-session and required tool choice returned HTTP 200, finish_reason=tool_calls, one ciel_runtime_confirm_completion tool call."
|
|
18
|
+
preliminary_full_suite: "Earlier direct unittest discovery was interrupted after unrelated launch tests opened an interactive Codex resume TUI. The supported isolated npm test subsequently passed."
|
|
19
|
+
limitation: "The updated local runtime has not been installed into the active YETI-01 Codex process; its 187 MB live session was not replayed end-to-end."
|
|
20
|
+
causal_limit: "No captured raw upstream terminal frame from an affected routed or native turn; no conclusion about a recent provider/model regression or Codex CLI change."
|
|
21
|
+
scope: "Local code and tests only; no remote installation or release push."
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
title: OpenCode Zen and Go Union Alpha model card and runtime profile
|
|
2
|
+
sources:
|
|
3
|
+
zen: https://opencode.ai/docs/ko/zen/ (Union Alpha Free endpoint and price)
|
|
4
|
+
go: https://opencode.ai/docs/ko/go/ (Union Alpha Free endpoint and price)
|
|
5
|
+
card: https://models.dev/api.json (opencode and opencode-go union-alpha entries)
|
|
6
|
+
catalog_origin: https://github.com/anomalyco/models.dev/blob/dev/README.md (OpenCode uses this catalog internally)
|
|
7
|
+
verified_metadata:
|
|
8
|
+
model_id: union-alpha
|
|
9
|
+
zen_endpoint: https://opencode.ai/zen/v1/messages
|
|
10
|
+
go_endpoint: https://opencode.ai/zen/go/v1/messages
|
|
11
|
+
protocol: anthropic_messages for both plans
|
|
12
|
+
name: Union Alpha Free
|
|
13
|
+
description: Stealth model built for agentic coding
|
|
14
|
+
context_window_tokens: 262144
|
|
15
|
+
max_output_tokens: 131072
|
|
16
|
+
input_modalities: text, image
|
|
17
|
+
output_modalities: text
|
|
18
|
+
reasoning: true
|
|
19
|
+
tool_call: true
|
|
20
|
+
reasoning_options: [] (no documented effort levels)
|
|
21
|
+
open_weights: false
|
|
22
|
+
catalog_release_date: 2026-09-16
|
|
23
|
+
price: free on both plans at time of verification
|
|
24
|
+
implementation:
|
|
25
|
+
catalog: Exact official model ID and Messages routing for both provider catalogs.
|
|
26
|
+
profile: Selected Union Alpha applies model-specific context, output, and vision support to both plans.
|
|
27
|
+
scope: No inferred effort levels or sampling parameters. Other models retain their prior profiles.
|
|
28
|
+
verification:
|
|
29
|
+
command: python -m unittest discover -s tests -p test_opencode*.py
|
|
30
|
+
result: 60 OpenCode tests passed.
|
|
31
|
+
regression: Both plans expose union-alpha; client operations and runtime-prefixed aliases select Anthropic Messages; other models unchanged.
|
|
32
|
+
runtime_capture: apply_provider_model_profile on current_model=union-alpha yields context_window=262144, max_model_len=262144, max_output_tokens=131072, supports_vision=true for both plans; repeated application is idempotent.
|
|
33
|
+
static: Ruff on changed Python files and git diff --check passed.
|
|
34
|
+
limitations: Real hosted generation not exercised. No credentials, live sessions, installed runtime, git remotes or published packages changed.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
okf_version: "1.0"
|
|
2
|
+
task: "Codebase analysis of ciel-runtime at nightly-0.2.47 (HEAD 6fee3e8), read-only."
|
|
3
|
+
method: "Direct file reads and command output by the main session, plus three read-only exploration agents (router path; launcher/channel/terminal; providers/protocols/tests/CI). docs/*.md claims were not used as evidence; agent claims listed under verified_directly were re-checked in source by the main session."
|
|
4
|
+
changes: "None to source. This journal file is the only file written."
|
|
5
|
+
verified_directly:
|
|
6
|
+
size: "921 tracked files, 883 commits, first commit 2026-06-23 a4736e5, HEAD 2026-09-17 6fee3e8. Tracked .py/.cjs/.js: 688 files, 192,466 physical lines (172,341 non-blank)."
|
|
7
|
+
main_file_budget: "ciel_runtime.py is 4,980 physical lines; architecture_budget.py sets MAIN_FILE_LINE_BUDGET = 4_980 and FINAL_FILE_LINE_BUDGET = 4_999."
|
|
8
|
+
packaging: "package.json: @oneciel-ai/ciel-runtime 0.2.47, bins ciel-runtime/cielrt/ciel-runtimectl/ciel-runtime-stop -> npm-bin/*.js, no dependencies block, engines.node >=18. npm test = compileall + run_test_group.py unit/router/channel/runtime."
|
|
9
|
+
test_runner_env: "scripts/run_test_group.py:75-77 sets CIEL_RUNTIME_CONFIG_DIR, CIEL_RUNTIME_ROUTER_PORT, CIEL_RUNTIME_TEST_ISOLATED=1."
|
|
10
|
+
providers: "Executing the registry: 67 descriptors, 67 adapters, 129 alias-map entries."
|
|
11
|
+
router_port: "runtime_paths.py:80-100 default_router_port: env CIEL_RUNTIME_ROUTER_PORT -> saved web_backend port -> 8799 + (uid % 1000) or 8799 + (sha256(user|HOME)[:8] % 1000); runtime_paths.py:132-139 per-workspace selection and ROUTER_INSTANCE_ID."
|
|
12
|
+
composition_root: "ciel_runtime.py:2983-3013 assembles RouterHttpServices / RouterServerRuntime with ThreadingHTTPServer injected as a port; RouterHandler subclasses RouterHttpHandler. GET channel-MCP slot is wired to `lambda _handler, _path: False` (ciel_runtime.py:2985)."
|
|
13
|
+
upstream_transport: "provider_network.py:117-128 provider_urlopen calls urllib.request.urlopen inside socket_ip_family_policy, which swaps socket.getaddrinfo under a lock (provider_network.py:82-114)."
|
|
14
|
+
legacy_menu: "ciel-runtime-menu.py imports termios/tty at module top; ciel_runtime.py:4166-4186 only runs it when os.name != 'nt' and CIEL_RUNTIME_USE_LEGACY_MENU=1."
|
|
15
|
+
launch_mode_type: "architecture.py:26 LaunchMode = Literal['native','routed','router']; README.md:112-117 lists four modes including 'bridge'."
|
|
16
|
+
channel_delivery: "ciel_runtime.py:3220-3225 normalize_channel_delivery and channel_delivery_mode discard their argument (`del value` / `del cfg`)."
|
|
17
|
+
working_tree: "git diff: channel_terminal_proxy.py +6/-1 (child exit log, BaseException log-and-reraise before existing finally), windows_conpty.py +8 (import, start_console_guard after parent VT output enabled, stop_console_guard after _restore_parent_console). Untracked windows_console_guard.py (97 lines) is imported only by windows_conpty.py:20; no file under tests/ references it (grep)."
|
|
18
|
+
apps_dir: "apps/ is empty and has no tracked files."
|
|
19
|
+
agent_reported_not_rechecked: "Endpoint tables, /v1/messages and /v1/responses call chains, upstream_retry/key-cooldown details, protocol and streaming module roles, tool-guard event handling, observability storage formats, architecture-contract categories, CI workflow contents, launcher/channel/lifecycle details, config migration marker list. Line numbers in those reports can be off by a few lines (observed: run_test_group.py reported as :80-82, actual :75-77)."
|
|
20
|
+
correction: "An earlier in-session figure (172,341 total lines; ciel_runtime.py 4,588) came from PowerShell Measure-Object -Line, which skips blank lines. Physical counts are 192,466 and 4,980."
|
|
21
|
+
stale_memory_found: "Auto-memory ciel-runtime-project.md states `npm test` runs all tests in one process without CIEL_RUNTIME_TEST_ISOLATED. Current package.json:52 runs the four isolated groups. Memory updated."
|
|
22
|
+
not_done: "No tests, lint, router, or CLI were executed. No behavior was exercised; this is a static survey."
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
okf_version: "1.0"
|
|
2
|
+
task: "Interpret screenshot of run_wsg.bat failure without assuming a causal link to Ciel Runtime deployment."
|
|
3
|
+
observed_screenshot:
|
|
4
|
+
command: "PowerShell at D:/ProgramData/Riskonnect/Batch executing ./run_wsg.bat."
|
|
5
|
+
application_stack: "metadataLoad_console.FileSynchronizer.SynchronizeCustomTable and metadataLoad_console.Program.run."
|
|
6
|
+
errors:
|
|
7
|
+
- "[NRTDMS][GetObjectBySID] No object for moniker"
|
|
8
|
+
- "RK write-back for 1 row(s): NotFound=1"
|
|
9
|
+
- "The system cannot find the file specified."
|
|
10
|
+
- "Access is denied for several files under D:/Program Files/iManage/Cloud Workspace Generator, including Analytics.dll and AutoDocPreview.ocx."
|
|
11
|
+
checks:
|
|
12
|
+
local_windows: "D: drive exists, but the shown batch and iManage paths do not exist on this computer."
|
|
13
|
+
yeti_01: "SSH read-only check confirmed D:/ProgramData/Riskonnect/Batch/run_wsg.bat and the generator directory are absent on YETI-01."
|
|
14
|
+
assessment: "The screenshot does not identify the host, the first exception line, or a causal relationship to Ciel Runtime. Root cause remains undetermined."
|
|
15
|
+
scope: "No files or services on the screenshot host were changed."
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
okf_version: "1.0"
|
|
2
|
+
task: "YETI-01 E:\\ciel-qb-mcp: check whether (1) the LLM's closing text being shown twice and (2) 'says it will proceed, then stops' are caused by the ciel-runtime router."
|
|
3
|
+
scope: "Read-only over SSH (100.64.0.10:1004, user YETI-01). No file written, no process touched on YETI-01. Scripts were passed as base64 to python -c. No source change locally."
|
|
4
|
+
target_state:
|
|
5
|
+
router: "127.0.0.1:9407 /health: version 0.2.47-nightly.20260917-050538.6fee3e8, pid 7132, workspace e:\\ciel-qb-mcp, provider github-copilot-oauth, model gpt-5.6-sol, one client pid 5532."
|
|
6
|
+
client: "pid 5532 = ciel_runtime.py cli --continue; child = codex --yolo ... wire_api=responses base_url=http://127.0.0.1:9407/v1 ... resume 01a09e13-3a25-7262-90d6-b19c054e9a15."
|
|
7
|
+
transcript: "C:\\Users\\YETI-01\\.codex\\sessions\\2026\\09\\13\\rollout-2026-09-13T21-00-55-01a09e13-...jsonl (294,947,345 bytes); last write 2026-09-17 07:53:59 local (14:53:59Z)."
|
|
8
|
+
provider_config: "workspaces/51db24129583/config.json providers.github-copilot-oauth: responses_custom_tools_as_functions unset, responses_stream_truncation_retries unset, stream_enabled true."
|
|
9
|
+
log_level: "INFO. Instance dir has router.log(+.1), tool-calls.jsonl; no requests.jsonl / responses.jsonl / router-sse-trace.jsonl."
|
|
10
|
+
observed_stall:
|
|
11
|
+
last_turn: "task_started 14:09:30Z (user: '검증 진행'), task_complete 14:53:59Z, duration_ms 2,669,667, one turn_id 01a0afb3-7a82-7132-ac26-58dbadbdc6a4."
|
|
12
|
+
compactions_inside_turn: "compacted records at 14:10:16, 14:21:34, 14:33:30, 14:43:39, 14:53:55 (window_number 62/63/64 seen for the last three). Each is immediately preceded by an assistant message phase=final_answer ('## Handoff', '## 진행 상태', '## 현재 상태', '### 현재 진행 상태') whose first 200 chars are contained in the compacted.message. Router log for the 14:53:55 one: request_tools=0, request_input_items=379, request_bytes=847187, input_tokens=107040."
|
|
13
|
+
after_compaction: "14:10:25, 14:21:44, 14:33:48, 14:43:45: assistant message phase=commentary ('…이어서 진행하겠습니다…') followed by tool calls; work continued. 14:53:59: assistant message phase=final_answer, 146 chars ('검증을 이어서 진행하겠습니다. …'), no tool call in that response, then task_complete with the same text. Router log for that request: request_tools=14, request_input_items=206, input_tokens=45719."
|
|
14
|
+
router_path_for_these_requests:
|
|
15
|
+
route: "provider Responses passthrough (log event provider_responses_cache, emitted by ProviderResponsesPassthrough). With both config flags unset, forward() writes each upstream chunk to the client unchanged (provider_responses_passthrough.py:449-481, projector None)."
|
|
16
|
+
no_gate_on_this_route: "codex_completion_gate is referenced only from router_http.py (:565 _validate_codex_completion in the Codex backend adapter). recover_preamble_only_turn is called only on the translated route (openai_responses_router.py:234). _handle_provider_responses_route (openai_responses_router.py:417-448) calls neither."
|
|
17
|
+
phase_field: "ciel_runtime_support never assigns phase='final_answer'; it only validates the value (protocols/openai_responses.py:2049-2050, 2886-2888; responses_anthropic_stream.py:629-630)."
|
|
18
|
+
log_histogram: "Whole router.log + router.log.1: no codex_completion_gate_*, no recovery/nudge events. Events present: transcript_delta_delivered 2104, channel_windows_console_deferred 890, provider_responses_cache 813, external_event_sse_disconnected 19, transcript_delta_delivery_failed 12, others <=4."
|
|
19
|
+
observed_duplication_checks:
|
|
20
|
+
transcript_items: "In the last 120 MB of the rollout, assistant response_item texts that occur more than once: two short commentary strings on 2026-09-16 (21:59:01/22:01:08 and 23:14:55/23:17:20), minutes apart, each followed by its own tool call, item ids rs_01a0… . No turn-ending message is recorded twice as a response_item."
|
|
21
|
+
router_stream: "GET /ca/tui/recent (last 200 events): final request tui-7132-1789656835427761900 has 48 output.text.delta events totalling 146 chars = length of the transcript message. No repeated half."
|
|
22
|
+
codex_record_shape: "Each turn end is written by Codex as response_item(message) and then event_msg task_complete.last_agent_message with the same text (hash b76f7ff2 for both at 14:53:59). Each assistant message is also preceded by an event_msg item_completed(AgentMessage)."
|
|
23
|
+
transcript_webhook: "transcript_delta_delivery posts raw JSONL byte ranges. 2104 deliveries, 0 offset rewinds, 2 forward gaps, 12 failures (7 TimeoutError, 5 HTTP 502); a failed batch is re-sent from the same offset."
|
|
24
|
+
not_verified:
|
|
25
|
+
- "What surface the user sees the doubled text on (Codex TUI through ConPTY, Web Chat, or a Walkie transcript consumer). The console screen of pid 5532 cannot be read over SSH."
|
|
26
|
+
- "Whether the Codex TUI rendering through the Windows ConPTY proxy shows the closing message twice. No evidence for or against was collected."
|
|
27
|
+
- "Whether the receiver processed any of the 12 failed transcript batches before the retry."
|
|
28
|
+
- "Raw upstream SSE for the 14:53:57 request is not persisted; that upstream sent phase=final_answer is inferred from the unchanged-bytes passthrough code path plus the transcript, not from a byte capture."
|
|
29
|
+
assessment: "Stall: verified that the turn ended because the first post-compaction response was a no-tool message marked final_answer; on this route the router forwards bytes unchanged and has no completion check, so it neither produced nor prevented it. Duplication: not found in the model output, in the router's observed stream, or in successful transcript deliveries; the only always-at-turn-end identical pair found is Codex's own response_item + task_complete record."
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
okf_version: "1.0"
|
|
2
|
+
task: "Stop Codex turns from ending on an unconfirmed text reply, on every Responses route and provider."
|
|
3
|
+
base_commit: "6fee3e8045c174b064446a1f7955341e7bc0b132 (nightly-0.2.47), working tree only; nothing committed, pushed, or deployed."
|
|
4
|
+
history: "First pass gated only Codex compaction resumes on the provider passthrough route. The user then stated the cut-offs occur broadly regardless of provider or model; the transcript scan below supports that, so the rule was generalised and the compaction-specific predicate removed."
|
|
5
|
+
evidence:
|
|
6
|
+
transcript_scan: "YETI-01 rollout 01a09e13-…, last 160 MB, every task_complete with the item sequence before it, classified by structure. Turns that ended on a progress announcement with no tool call: (S1) after a tool result — opencode-go kimi-k3 04:19:17Z '트레이에 제출합니다.', meta muse-spark 09-16 20:01:31Z '결과 나오는 대로 보고하겠습니다.', 15:46:32Z '…확인하고 이어가…'; (S2) straight after the user's message, no tool call in the turn — opencode-go kimi-k3 03:57:36Z, 03:57:44Z (6 s), 03:57:56Z (5 s) '…확인하겠습니다.'; (S3) first response after Codex mid-turn compaction — github-copilot-oauth gpt-5.6-sol 05:26:33Z and 14:53:59Z. None of these final responses carried a reasoning item."
|
|
7
|
+
routes: "opencode-go kimi-k3 = translated route (collect + recover_preamble_only_turn). meta and github-copilot-oauth = provider Responses passthrough (router log shows only provider_responses_cache for them)."
|
|
8
|
+
gaps: "Passthrough route had no completion check at all. Where a check existed (Codex backend adapter, translated route) it required a reasoning item or a trailing tool result, so S2 and S3 were never checked anywhere and S1 was unchecked on the passthrough route."
|
|
9
|
+
changes:
|
|
10
|
+
- "codex_completion_gate.py: request_allows_completion_check(body) = tools present and tool_choice != 'none'. request_requires_completion_check now = that + parseable completed response + no action + visible text. The reasoning / trailing-tool-result clause is gone."
|
|
11
|
+
- "codex_turn_recovery.py: message_requires_completion_check generalised the same way (tools, tool_choice not none in string or {type:none} form, no tool_use, visible text). The old structural clause survives only as _retry_still_unfinished, which bounds Kimi's retry loop exactly as before."
|
|
12
|
+
- "provider_responses_passthrough.py: non-bridge streaming requests that allow tools are buffered through _forward_buffered_stream and observed; an unconfirmed text-only final triggers one follow-up = completion_check_body -> repair_replayed_response_items -> provider normalize_request, sent with the request headers as built. A completed follow-up with a real action replaces the candidate; confirmation, any failure, or a follow-up naming the private tool keeps the candidate. Log events provider_responses_completion_gate_continued / _kept / _failed. Usage recorded for both upstream responses."
|
|
13
|
+
- "providers/meta.py: Responses tool_choice 'required' and named-function objects are projected to 'auto'."
|
|
14
|
+
- "tests: new tests/test_provider_responses_completion_gate.py (8); test_codex_completion_gate.py (+2, one assertion inverted); test_codex_turn_recovery.py (the 'needs no completion check without prior tool result' test inverted, + tool_choice none test); test_meta_provider.py (+1)."
|
|
15
|
+
defects_found_by_live_probes:
|
|
16
|
+
- "Follow-up headers were rebuilt from request.header_items(); urllib had stamped the first body's Content-length there, Copilot got a truncated body and answered 400 'failed to parse request'. Eight body variants all returned 200 in a bisect, isolating the headers. Now the headers dict as built is passed; the test fake stamps Content-length like urllib."
|
|
17
|
+
- "Meta answered the follow-up with 400: 'only \"auto\" is supported for tool_choice. \"none\", \"required\", and named function choices are not currently supported'. The follow-up bypassed provider request rules and Meta's adapter had no rule for string tool_choice. Fixed by normalising the follow-up and adding the Meta rule. Normalisation was checked idempotent for all nine locally configured adapters that select openai_responses (agy, alims-intl, alitoken, alitoken-individual, codex, github-copilot-oauth, meta, xai, zai-coding-plan)."
|
|
18
|
+
verification:
|
|
19
|
+
live_passthrough_meta: "meta muse-spark-1.3-contributor, single user message + exec_command tool, instruction to announce first (S2 shape). Two POSTs (tool_choice None then 'auto' with exec_command + ciel_runtime_confirm_completion, 3 input items); delivered output reasoning + message + function_call exec_command; log provider_responses_completion_gate_continued."
|
|
20
|
+
live_passthrough_copilot: "github-copilot-oauth gpt-5.6-sol. With tool_choice=none forcing a text-only candidate after a Codex summary message (first pass): follow-up accepted (200), delivered function_call exec_command, log …_continued. With the S2 prompt the model emitted message + function_call in one response, so no check was needed."
|
|
21
|
+
live_translated: "ollama-cloud kimi-k3, S2 prompt: candidate thinking + text, requires_check True, log codex_turn_retry reason=completion_check, delivered text + thinking + tool_use exec_command. ollama-cloud glm-5.1 and deepseek-v4-flash called the tool directly (no check needed). opencode could not be probed: local credential returns 401."
|
|
22
|
+
tests: "Isolated config dir, per-file: 20 files, 720 tests, all OK (architecture_contracts 261 with 42 skipped). compileall clean. ruff clean on all changed files; `ruff check .` reports 2 findings, both in untracked files under docs/journal/2026/09/12."
|
|
23
|
+
not_run: "npm test / full groups were not run because a live router (pid 51576, port 9469) was serving on this machine; its pid was unchanged after every run."
|
|
24
|
+
limitations:
|
|
25
|
+
- "Every Codex turn on a routed provider now costs one extra upstream request when it ends in text (previously only after tool results / reasoning on two of the three routes)."
|
|
26
|
+
- "Passthrough responses to tool-bearing requests are delivered after buffering instead of streaming, as the Codex backend route already did."
|
|
27
|
+
- "Providers other than Meta may also reject tool_choice 'required' on this route (alibaba, xai, zai not probed: no local credentials). A rejected follow-up keeps the candidate and logs provider_responses_completion_gate_failed."
|
|
28
|
+
- "The 09-16 no-reasoning S2 turns were not replayed against opencode-go; that shape is covered by unit tests and by the live Meta probe."
|
|
29
|
+
- "YETI-01 still runs the published 6fee3e8 build; this change is not installed there."
|
|
30
|
+
related: "docs/journal/2026/09/17/diagnostics/yeti-01/duplicate-final-and-stall.okf; visual last-row duplication was set aside at the user's direction (local repro: GulimChe legacy console renders U+00B7 in two cells, so a row Codex measured as exactly 132 cells wraps; Consolas does not)."
|