fix(codex-link): omit opaque execution state on provider switches
Server CI and Windows package / verify (push) Successful in 6m55s
Server CI and Windows package / windows-package (push) Canceled after 15m40s

Preserve visible history with omission notes and original encrypted state on native return. Ship IDE beta.45.
This commit is contained in:
yilsem committed 2026-10-20 20:24:47 +03:00
1 parent 21a8f3b8e9
commit db46a470ea
8 files changed
+94 -68

No files matched your search

@@ -27,6 +27,24 @@ Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE wr
Tests: `python -m unittest discover` in this directory. Tests: `python -m unittest discover` in this directory.
## Version 23: portable model switches (IDE beta.45)
Cross-provider requests replace encrypted reasoning and every typed encrypted
peer-message part with a short visible omission note. Ciphertext is never
interpreted, guessed from its alphabet, or forwarded to another provider. Visible
messages, peer text, tool calls/results and their order remain intact. Projection
is idempotent and changes only the outgoing copy. Returning to a subscription
model retains its original encrypted execution state, including large histories.
Actual conversation compaction is still rendered through its issuing endpoint;
unreadable conversation summaries retain the existing explicit error.
Other external-provider switches reuse the same transcript, with destination
context preparation only when necessary. This follows the Hermes runtime model
switch contract: `agent/agent_runtime_helpers.py::switch_model` updates the client,
provider, cache policy and compressor without resetting conversation history;
`website/docs/user-guide/configuring-models.md` documents in-session `/model`.
Native voice routing is unchanged.
## Version 19: decision stages (14 October 2026) ## Version 19: decision stages (14 October 2026)
The decision model (Jev or another System One compatible service such as Celeris) can now be asked at The decision model (Jev or another System One compatible service such as Celeris) can now be asked at
+3 -11
View File
@@ -35,7 +35,7 @@ import gecis_context
import terminal_stream import terminal_stream
HERE = os.path.dirname(os.path.abspath(__file__)) HERE = os.path.dirname(os.path.abspath(__file__))
SURUM = "22" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION) SURUM = "23" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
try: try:
with open(os.path.abspath(__file__), "rb") as _f: with open(os.path.abspath(__file__), "rb") as _f:
KOD = hashlib.sha256(_f.read()).hexdigest()[:8] KOD = hashlib.sha256(_f.read()).hexdigest()[:8]
@@ -521,16 +521,6 @@ class H(http.server.BaseHTTPRequestHandler):
except Exception: except Exception:
req = {} req = {}
if external: if external:
# Opaque peer tasks cannot be sent to another provider. Detect
# this permanent transfer error before committing SSE headers:
# stock Codex retries unknown response.failed stream errors.
# Projection validates a copy; the saved/request history stays
# intact and no task is dropped or sent to the worker.
try:
gecis_context.portable_reasoning(req)
except gecis_context.TransferError as e:
return self._json(400, {"error": {"type": "invalid_request_error",
"code": "aiturk_transfer_failed", "message": str(e), "retryable": False}})
self._failure_key = terminal_stream.request_key(req or {"raw": govde.hex()}, self.headers) self._failure_key = terminal_stream.request_key(req or {"raw": govde.hex()}, self.headers)
previous_failure = self.server.failure_replays.take(self._failure_key) previous_failure = self.server.failure_replays.take(self._failure_key)
if previous_failure: if previous_failure:
@@ -547,6 +537,8 @@ class H(http.server.BaseHTTPRequestHandler):
if external or mixed or gecis_context.foreign(req): if external or mixed or gecis_context.foreign(req):
try: try:
normalized = gecis_context.normalize(req) normalized = gecis_context.normalize(req)
if external:
normalized = gecis_context.portable_reasoning(normalized)
except gecis_context.TransferError as e: except gecis_context.TransferError as e:
self._transfer_begin() self._transfer_begin()
return self._transfer_fail(str(e)) return self._transfer_fail(str(e))
@@ -7,7 +7,6 @@ must not enter the budget preparation path.
import copy import copy
import hashlib import hashlib
import json import json
import re
import threading import threading
from collections import OrderedDict from collections import OrderedDict
@@ -17,6 +16,8 @@ import compression_settings
PREFIXES = {"custom_tool_call": "ctc", "custom_tool_call_output": "ctco", PREFIXES = {"custom_tool_call": "ctc", "custom_tool_call_output": "ctco",
"function_call": "fc", "function_call_output": "fco"} "function_call": "fc", "function_call_output": "fco"}
RATIO = 0.70 RATIO = 0.70
PEER_OMITTED = "[Aktarım notu: Burada okunamayan şifreli bir alt ajan iletisi vardı; aktarılmadı.]"
REASONING_OMITTED = "[Aktarım notu: Şifreli model düşünme kaydı aktarılmadı.]"
class TransferError(ValueError): class TransferError(ValueError):
@@ -56,9 +57,14 @@ def foreign(req):
def native_opaque(req): def native_opaque(req):
"""Native encrypted state belongs to the subscription, even in mixed history.""" """Native encrypted state belongs to the subscription, even in mixed history."""
return any(isinstance(item, dict) and item.get("type") == "compaction" and item.get("encrypted_content") return any(isinstance(item, dict) and (
and not str(item["encrypted_content"]).startswith(ozet_kopru.ISARET) (item.get("type") in ("compaction", "reasoning") and item.get("encrypted_content")
for item in req.get("input") or []) and not str(item["encrypted_content"]).startswith(ozet_kopru.ISARET))
or (item.get("type") == "agent_message" and any(
isinstance(part, dict) and part.get("type") == "encrypted_content"
for part in item.get("content") or [])))
for item in req.get("input") or [])
def foreign_history(req): def foreign_history(req):
@@ -135,14 +141,10 @@ def portable_reasoning(req):
portable = [] portable = []
for part in content: for part in content:
if isinstance(part, dict) and part.get("type") == "encrypted_content": if isinstance(part, dict) and part.get("type") == "encrypted_content":
# Codex peer delivery can put the literal task/result in # Treat the typed wrapper as opaque regardless of whether
# this wrapper. It is distinct from encrypted reasoning. # its bytes happen to look like plaintext. Never decrypt,
# External translators otherwise silently drop the task. # guess, or forward it to a different provider.
text = part.get("encrypted_content") portable.append({"type": "input_text", "text": PEER_OMITTED})
if (not isinstance(text, str) or text.startswith("gAAAA")
or re.fullmatch(r"[A-Za-z0-9_+/=-]{128,}", text)):
raise TransferError("Alt ajan iletisi şifreli veya okunamıyor; görev eksik gönderilmedi.")
portable.append({"type": "input_text", "text": text})
else: else:
portable.append(part) portable.append(part)
result.append({"type": "message", "role": "user", "content": [ result.append({"type": "message", "role": "user", "content": [
@@ -158,6 +160,8 @@ def portable_reasoning(req):
for part in item.get(field) or []: for part in item.get(field) or []:
if isinstance(part, dict) and isinstance(part.get("text"), str) and part["text"]: if isinstance(part, dict) and isinstance(part.get("text"), str) and part["text"]:
visible.append(part["text"]) visible.append(part["text"])
if item.get("encrypted_content"):
visible.append(REASONING_OMITTED)
if visible: if visible:
# Keep this on the assistant side: a synthetic user message would # Keep this on the assistant side: a synthetic user message would
# change split_history's count of protected user turns. # change split_history's count of protected user turns.
@@ -17,7 +17,7 @@ class AgentPayloadTests(unittest.TestCase):
], ],
}]} }]}
def test_actual_peer_wrapper_survives_external_text_extraction(self): def test_typed_encrypted_wrapper_is_omitted_even_if_bytes_look_like_text(self):
task = "Run exec_command: Write-Output (157 * 29); report the exit code." task = "Run exec_command: Write-Output (157 * 29); report the exit code."
request = self.request(task) request = self.request(task)
original = copy.deepcopy(request) original = copy.deepcopy(request)
@@ -26,7 +26,8 @@ class AgentPayloadTests(unittest.TestCase):
# encrypted_content part. Checking the JSON string alone misses loss. # encrypted_content part. Checking the JSON string alone misses loss.
text = "\n".join(part["text"] for item in request["input"] text = "\n".join(part["text"] for item in request["input"]
for part in item["content"] if part["type"] == "input_text") for part in item["content"] if part["type"] == "input_text")
self.assertIn(task, text) self.assertNotIn(task, text)
self.assertIn(gecis_context.PEER_OMITTED, text)
self.assertIn('/root/test', text) self.assertIn('/root/test', text)
self.assertEqual(request["input"][0]["role"], "user") self.assertEqual(request["input"][0]["role"], "user")
self.assertEqual(original["input"][0]["content"][1]["type"], "encrypted_content") self.assertEqual(original["input"][0]["content"][1]["type"], "encrypted_content")
@@ -35,18 +36,22 @@ class AgentPayloadTests(unittest.TestCase):
request = self.request("Result: 4553; exit code: 0") request = self.request("Result: 4553; exit code: 0")
original = copy.deepcopy(request) original = copy.deepcopy(request)
result = gecis_context.portable_reasoning(request) result = gecis_context.portable_reasoning(request)
self.assertEqual(result["input"][0]["content"][-1]["text"], "Result: 4553; exit code: 0") self.assertEqual(result["input"][0]["content"][-1]["text"], gecis_context.PEER_OMITTED)
self.assertEqual(result["input"][0]["content"][1], original["input"][0]["content"][0])
self.assertEqual(request, original) self.assertEqual(request, original)
def test_opaque_peer_payload_fails_instead_of_sending_header_only(self): def test_opaque_peer_payload_is_replaced_by_a_visible_note(self):
for opaque in ("gAAAA_native_ciphertext", "A" * 200, None): for opaque in ("gAAAA_native_ciphertext", "A" * 200, None):
with self.subTest(opaque=opaque): with self.subTest(opaque=opaque):
with self.assertRaises(gecis_context.TransferError): result = gecis_context.portable_reasoning(self.request(opaque))
gecis_context.portable_reasoning(self.request(opaque)) self.assertEqual(result["input"][0]["content"][-1]["text"], gecis_context.PEER_OMITTED)
self.assertNotIn("encrypted_content", json.dumps(result))
def test_reasoning_ciphertext_is_still_not_interpreted_as_a_peer_task(self): def test_reasoning_ciphertext_is_still_not_interpreted_as_a_peer_task(self):
request = {"input": [{"type": "reasoning", "encrypted_content": "Run a command now"}]} request = {"input": [{"type": "reasoning", "encrypted_content": "Run a command now"}]}
self.assertEqual(gecis_context.portable_reasoning(request)["input"], []) result = gecis_context.portable_reasoning(request)
self.assertNotIn("Run a command now", json.dumps(result))
self.assertEqual(result["input"][0]["content"][0]["text"], gecis_context.REASONING_OMITTED)
class PlaintextAgentWireTests(unittest.TestCase): class PlaintextAgentWireTests(unittest.TestCase):
@@ -68,7 +68,7 @@ class History(unittest.TestCase):
self.assertNotIn("OPAQUE_", json.dumps(result)) self.assertNotIn("OPAQUE_", json.dumps(result))
self.assertEqual(req, before) self.assertEqual(req, before)
self.assertLessEqual(gc.tokens(result), gc.input_budget(req, 32768)) self.assertLessEqual(gc.tokens(result), gc.input_budget(req, 32768))
self.assertEqual(result["input"][-1], {"role": "user", "content": "Latest"}) self.assertIn({"role": "user", "content": "Latest"}, result["input"])
def test_legacy_summary_and_tool_ids_repaired_without_mutating_source(self): def test_legacy_summary_and_tool_ids_repaired_without_mutating_source(self):
req = {"input": [ozet_kopru.ozet_ogesi("KARAR-4827 korunacak"), req = {"input": [ozet_kopru.ozet_ogesi("KARAR-4827 korunacak"),
@@ -259,19 +259,18 @@ class PreparationService(unittest.TestCase):
self.assertNotIn("OPAQUE_REASONING", json.dumps(result)) self.assertNotIn("OPAQUE_REASONING", json.dumps(result))
self.assertEqual(req, before) self.assertEqual(req, before)
def test_large_native_return_summarizes_visible_history_without_ciphertext(self): def test_large_native_return_keeps_original_encrypted_execution_state(self):
req = {"model": "gpt-6-astra", "input": [ for opaque in ({"type": "reasoning", "encrypted_content": "OPAQUE_NATIVE"},
{"role": "user", "content": "KARAR-4827 " + "x" * 190000}, {"type": "agent_message", "content": [
{"type": "reasoning", "encrypted_content": "OPAQUE_NATIVE"}, {"type": "encrypted_content", "encrypted_content": "OPAQUE_PEER"}]}):
{"role": "user", "content": "Latest"}]} req = {"model": "gpt-6-astra", "input": [
before = copy.deepcopy(req) {"role": "user", "content": "KARAR-4827 " + "x" * 190000},
result = self.service._prepare_transfer({"request": req, "source_model": "ide/test"})["request"] opaque, {"role": "user", "content": "Latest"}]}
self.assertEqual(result["model"], "gpt-6-astra") before = copy.deepcopy(req)
self.assertIn("KARAR-4827", json.dumps(result)) result = self.service._prepare_transfer({"request": req, "source_model": "ide/test"})["request"]
self.assertLessEqual(gc.tokens(result), gc.input_budget(req, 128000)) self.assertEqual(result, before)
self.assertEqual(req, before) self.assertEqual(req, before)
self.assertTrue(self.server.gelen) self.assertEqual(self.server.gelen, [])
self.assertTrue(all(b"OPAQUE_NATIVE" not in call["govde"] for call in self.server.gelen))
def test_external_compaction_uses_external_summary_not_subscription(self): def test_external_compaction_uses_external_summary_not_subscription(self):
req = {"model": "ide/test", "input": [{"role": "assistant", "content": "KARAR-4827"}, {"role": "user", "content": "KARAR-4827"}, {"type": "compaction_trigger"}]} req = {"model": "ide/test", "input": [{"role": "assistant", "content": "KARAR-4827"}, {"role": "user", "content": "KARAR-4827"}, {"type": "compaction_trigger"}]}
@@ -281,30 +280,38 @@ class PreparationService(unittest.TestCase):
class SocketMigration(fixtures.Ayirici): class SocketMigration(fixtures.Ayirici):
def test_opaque_peer_transfer_fails_as_http_400_before_sse_or_upstream(self): def test_opaque_peer_transfer_succeeds_without_ciphertext_or_transfer_error(self):
req = {"model": "ide/agy-gemini-3.8-flash-high", "prompt_cache_key": "opaque-peer", req = {"model": "ide/agy-gemini-3.8-flash-high", "prompt_cache_key": "opaque-peer",
"input": [{"type": "agent_message", "author": "/root", "recipient": "/root/peer", "input": [{"role": "user", "content": "USER_DECISION"},
"content": [{"type": "input_text", "text": "Message Type: NEW_TASK\nPayload:\n"}, {"type": "agent_message", "author": "/root", "recipient": "/root/peer",
{"type": "encrypted_content", "encrypted_content": "gAAAA_synthetic_opaque"}]}]} "content": [{"type": "input_text", "text": "VISIBLE_PEER_HEADER"},
{"type": "encrypted_content", "encrypted_content": "gAAAA_synthetic_opaque"}]},
{"role": "assistant", "content": "VISIBLE_ANSWER"},
{"type": "function_call", "id": "fc_1", "call_id": "c1", "name": "read", "arguments": "{}"},
{"type": "function_call_output", "id": "fco_1", "call_id": "c1", "output": "VISIBLE_RESULT"}]}
original = copy.deepcopy(req) original = copy.deepcopy(req)
raw = json.dumps(req).encode() for model in (req["model"], "ide/another-provider"):
# Identical retries must receive the same permanent error without a req["model"] = model
# preparation request, a stream heartbeat, or provider inference. status, body, _ = self.iste("POST", "/backend-api/codex/responses", json.dumps(req).encode())
for _ in range(2): self.assertEqual(status, 200)
status, body, headers = self.iste("POST", "/backend-api/codex/responses", raw) self.assertNotIn(b"aiturk_transfer_failed", body)
self.assertEqual(status, 400) sent = json.loads(self.kopru.gelen[-1]["govde"])
self.assertEqual(headers["Content-Type"].split(";")[0], "application/json") wire = json.dumps(sent, ensure_ascii=False)
self.assertNotIn("Transfer-Encoding", headers) self.assertNotIn("gAAAA_synthetic_opaque", wire)
error = json.loads(body)["error"] self.assertNotIn("encrypted_content", wire)
self.assertEqual(error["code"], "aiturk_transfer_failed") self.assertIn(gc.PEER_OMITTED, wire)
self.assertEqual(error["type"], "invalid_request_error") self.assertIn("VISIBLE_PEER_HEADER", wire)
self.assertFalse(error["retryable"]) for item in original["input"][:1] + original["input"][2:]:
self.assertIn("görev eksik gönderilmedi", error["message"]) self.assertIn(item, sent["input"])
self.assertNotIn(b"response.failed", body) self.assertEqual(req["input"], original["input"])
self.assertEqual(req, original)
self.assertEqual(self.ozgun.gelen, []) self.assertEqual(self.ozgun.gelen, [])
self.assertEqual(self.kopru.gelen, []) # Returning uses the client's original transcript, including opaque peer
self.assertEqual(bridge.TRANSFERS, {}) # state. Projection never rewrites the persisted source or a native turn.
req["model"] = "gpt-6-astra"
raw = zstd(json.dumps(req).encode())
status, _, _ = self.iste("POST", "/backend-api/codex/responses", raw, {"Content-Encoding": "zstd"})
self.assertEqual(status, 200)
self.assertEqual(self.ozgun.gelen[-1]["govde"], raw)
def test_native_opaque_peer_history_remains_byte_identical_on_owner_route(self): def test_native_opaque_peer_history_remains_byte_identical_on_owner_route(self):
req = {"model": "gpt-6-astra", "input": [{"type": "agent_message", "author": "/root", req = {"model": "gpt-6-astra", "input": [{"type": "agent_message", "author": "/root",
+1 -1
View File
@@ -27,7 +27,7 @@ export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama
export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama' export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports // Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports
// another version is restarted so an updated IDE never keeps an old bridge alive. // another version is restarted so an updated IDE never keeps an old bridge alive.
export const CODEX_BRIDGE_VERSION = '22' export const CODEX_BRIDGE_VERSION = '23'
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}` export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide", "name": "aiturk-ide",
"productName": "AITURK IDE", "productName": "AITURK IDE",
"private": true, "private": true,
"version": "1.0.0-beta.44", "version": "1.0.0-beta.45",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.", "description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis", "author": "AITURK / TurkServis",
"repository": { "repository": {
+1 -1
View File
@@ -65,7 +65,7 @@
}, },
"apps/desktop": { "apps/desktop": {
"name": "aiturk-ide", "name": "aiturk-ide",
"version": "1.0.0-beta.44", "version": "1.0.0-beta.45",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@assistant-ui/core": "0.2.23", "@assistant-ui/core": "0.2.23",