diff --git a/CHANGELOG.md b/CHANGELOG.md
index 23ecd76..eefa18d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## v2.2.1 (2026-05-20)
+
+- **Fixed compaction orphaning function_call_output items** — root cause of Crof `incomplete` responses
+ - Compaction cut between function_call and its function_call_output, creating dangling tool results
+ - Crof model received orphaned `tool` messages with empty `tool_call_id`, causing confusion and token exhaustion
+ - Compaction now expands tail boundary to include matching function_call/function_call_output pairs
+- **Fixed reasoning control**: `reasoning_effort=none` now always sends both `enable_thinking=false` AND `reasoning_effort=none`
+ - Crof API testing confirmed `reasoning_effort=none` is what actually suppresses reasoning, not `enable_thinking=false`
+- Added upstream debug logging to `~/.cache/codex-proxy/crof-upstream.jsonl`
+
## v2.2.0 (2026-05-20)
- **Added per-provider Reasoning controls in endpoint editor**
diff --git a/codex-launcher_2.2.0_all.deb b/codex-launcher_2.2.0_all.deb
deleted file mode 100644
index 1a10bc3..0000000
Binary files a/codex-launcher_2.2.0_all.deb and /dev/null differ
diff --git a/codex-launcher_2.2.1_all.deb b/codex-launcher_2.2.1_all.deb
new file mode 100644
index 0000000..1bf05c8
Binary files /dev/null and b/codex-launcher_2.2.1_all.deb differ
diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui
index c59ee71..1757f72 100755
--- a/src/codex-launcher-gui
+++ b/src/codex-launcher-gui
@@ -24,6 +24,11 @@ model_catalog_json = ""
"""
CHANGELOG = [
+ ("2.2.1", "2026-05-20", [
+ "Fixed compaction orphaning function_call_output items — root cause of Crof incomplete responses",
+ "Compaction now respects function_call/function_call_output pairs — no more dangling tool results",
+ "Fixed reasoning control: reasoning_effort=none now always sends enable_thinking=false too",
+ ]),
("2.2.0", "2026-05-20", [
"Added per-provider Reasoning On/Off toggle in endpoint editor",
"Added Reasoning Effort level per provider: None, Minimal, Low, Medium, High, Max",
@@ -543,7 +548,7 @@ class LauncherWin(Gtk.Window):
# header row
hdr = Gtk.Box(spacing=8)
vbox.pack_start(hdr, False, False, 0)
- lbl = Gtk.Label(label="Codex Launcher v2.2.0")
+ lbl = Gtk.Label(label="Codex Launcher v2.2.1")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog")
diff --git a/src/translate-proxy.py b/src/translate-proxy.py
index bfcd59b..e3883ea 100755
--- a/src/translate-proxy.py
+++ b/src/translate-proxy.py
@@ -238,8 +238,16 @@ def _compact_input(input_data):
break
head = input_data[:head_end]
- tail = input_data[-_COMPACT_KEEP_RECENT:]
- body = input_data[head_end:-_COMPACT_KEEP_RECENT]
+ tail_start = len(input_data) - _COMPACT_KEEP_RECENT
+ while tail_start > head_end:
+ if input_data[tail_start].get("type") == "function_call_output":
+ tail_start -= 1
+ elif input_data[tail_start].get("type") == "message" and input_data[tail_start].get("role") == "assistant":
+ tail_start -= 1
+ else:
+ break
+ tail = input_data[tail_start:]
+ body = input_data[head_end:tail_start]
if not body:
return head + tail
@@ -891,9 +899,11 @@ class Handler(http.server.BaseHTTPRequestHandler):
if body.get("tool_choice"):
chat_body["tool_choice"] = body["tool_choice"]
chat_body["stream"] = stream
- if not REASONING_ENABLED:
+ if not REASONING_ENABLED or REASONING_EFFORT == "none":
chat_body["enable_thinking"] = False
- chat_body["reasoning_effort"] = REASONING_EFFORT if REASONING_ENABLED else "none"
+ chat_body["reasoning_effort"] = "none"
+ else:
+ chat_body["reasoning_effort"] = REASONING_EFFORT
target = upstream_target(TARGET_URL, "/chat/completions")
fwd = forwarded_headers(self.headers, {
@@ -901,6 +911,16 @@ class Handler(http.server.BaseHTTPRequestHandler):
"Authorization": f"Bearer {API_KEY}",
}, browser_ua=True)
print(f"[translate-proxy] POST {target} model={model} stream={stream} ua={fwd.get('User-Agent','')[:50]}", file=sys.stderr)
+ _crof_debug_path = os.path.join(_LOG_DIR, "crof-upstream.jsonl")
+ with open(_crof_debug_path, "a") as _cdf:
+ _cdf.write(json.dumps({
+ "ts": _ts, "model": model, "max_tokens": chat_body.get("max_tokens"),
+ "reasoning_enabled": REASONING_ENABLED, "reasoning_effort": chat_body.get("reasoning_effort"),
+ "enable_thinking": chat_body.get("enable_thinking", "NOT_SENT"),
+ "n_messages": len(chat_body.get("messages", [])),
+ "has_tools": bool(chat_body.get("tools")),
+ "messages_summary": [{"role": m.get("role"), "tc": len(m.get("tool_calls", [])), "content_len": len(str(m.get("content", "")))[:6], "tool_call_id": m.get("tool_call_id")} for m in chat_body.get("messages", [])],
+ }) + "\n")
req = urllib.request.Request(
target,
data=json.dumps(chat_body).encode(),