diff --git a/apps/desktop/assets/codex-bridge/README.md b/apps/desktop/assets/codex-bridge/README.md index 65da265..8e2b590 100644 --- a/apps/desktop/assets/codex-bridge/README.md +++ b/apps/desktop/assets/codex-bridge/README.md @@ -27,6 +27,24 @@ Run: `python bridge.py --home --hermes-home `. The IDE wr 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) The decision model (Jev or another System One compatible service such as Celeris) can now be asked at diff --git a/apps/desktop/assets/codex-bridge/bridge.py b/apps/desktop/assets/codex-bridge/bridge.py index d6ed135..e722bb3 100644 --- a/apps/desktop/assets/codex-bridge/bridge.py +++ b/apps/desktop/assets/codex-bridge/bridge.py @@ -35,7 +35,7 @@ import gecis_context import terminal_stream 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: with open(os.path.abspath(__file__), "rb") as _f: KOD = hashlib.sha256(_f.read()).hexdigest()[:8] @@ -521,16 +521,6 @@ class H(http.server.BaseHTTPRequestHandler): except Exception: req = {} 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) previous_failure = self.server.failure_replays.take(self._failure_key) if previous_failure: @@ -547,6 +537,8 @@ class H(http.server.BaseHTTPRequestHandler): if external or mixed or gecis_context.foreign(req): try: normalized = gecis_context.normalize(req) + if external: + normalized = gecis_context.portable_reasoning(normalized) except gecis_context.TransferError as e: self._transfer_begin() return self._transfer_fail(str(e)) diff --git a/apps/desktop/assets/codex-bridge/gecis_context.py b/apps/desktop/assets/codex-bridge/gecis_context.py index 1543409..7081ce8 100644 --- a/apps/desktop/assets/codex-bridge/gecis_context.py +++ b/apps/desktop/assets/codex-bridge/gecis_context.py @@ -7,7 +7,6 @@ must not enter the budget preparation path. import copy import hashlib import json -import re import threading from collections import OrderedDict @@ -17,6 +16,8 @@ import compression_settings PREFIXES = {"custom_tool_call": "ctc", "custom_tool_call_output": "ctco", "function_call": "fc", "function_call_output": "fco"} 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): @@ -56,9 +57,14 @@ def foreign(req): def native_opaque(req): """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") - and not str(item["encrypted_content"]).startswith(ozet_kopru.ISARET) - for item in req.get("input") or []) + return any(isinstance(item, dict) and ( + (item.get("type") in ("compaction", "reasoning") and item.get("encrypted_content") + 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): @@ -135,14 +141,10 @@ def portable_reasoning(req): portable = [] for part in content: if isinstance(part, dict) and part.get("type") == "encrypted_content": - # Codex peer delivery can put the literal task/result in - # this wrapper. It is distinct from encrypted reasoning. - # External translators otherwise silently drop the task. - text = part.get("encrypted_content") - 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}) + # Treat the typed wrapper as opaque regardless of whether + # its bytes happen to look like plaintext. Never decrypt, + # guess, or forward it to a different provider. + portable.append({"type": "input_text", "text": PEER_OMITTED}) else: portable.append(part) result.append({"type": "message", "role": "user", "content": [ @@ -158,6 +160,8 @@ def portable_reasoning(req): for part in item.get(field) or []: if isinstance(part, dict) and isinstance(part.get("text"), str) and part["text"]: visible.append(part["text"]) + if item.get("encrypted_content"): + visible.append(REASONING_OMITTED) if visible: # Keep this on the assistant side: a synthetic user message would # change split_history's count of protected user turns. diff --git a/apps/desktop/assets/codex-bridge/test_agent_payload.py b/apps/desktop/assets/codex-bridge/test_agent_payload.py index 6ca8b3c..4041431 100644 --- a/apps/desktop/assets/codex-bridge/test_agent_payload.py +++ b/apps/desktop/assets/codex-bridge/test_agent_payload.py @@ -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." request = self.request(task) original = copy.deepcopy(request) @@ -26,7 +26,8 @@ class AgentPayloadTests(unittest.TestCase): # encrypted_content part. Checking the JSON string alone misses loss. text = "\n".join(part["text"] for item in request["input"] 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.assertEqual(request["input"][0]["role"], "user") 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") original = copy.deepcopy(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) - 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): with self.subTest(opaque=opaque): - with self.assertRaises(gecis_context.TransferError): - gecis_context.portable_reasoning(self.request(opaque)) + result = 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): 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): diff --git a/apps/desktop/assets/codex-bridge/test_gecis_context.py b/apps/desktop/assets/codex-bridge/test_gecis_context.py index e8e9d17..3047c6e 100644 --- a/apps/desktop/assets/codex-bridge/test_gecis_context.py +++ b/apps/desktop/assets/codex-bridge/test_gecis_context.py @@ -68,7 +68,7 @@ class History(unittest.TestCase): self.assertNotIn("OPAQUE_", json.dumps(result)) self.assertEqual(req, before) 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): 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.assertEqual(req, before) - def test_large_native_return_summarizes_visible_history_without_ciphertext(self): - req = {"model": "gpt-6-astra", "input": [ - {"role": "user", "content": "KARAR-4827 " + "x" * 190000}, - {"type": "reasoning", "encrypted_content": "OPAQUE_NATIVE"}, - {"role": "user", "content": "Latest"}]} - before = copy.deepcopy(req) - result = self.service._prepare_transfer({"request": req, "source_model": "ide/test"})["request"] - self.assertEqual(result["model"], "gpt-6-astra") - self.assertIn("KARAR-4827", json.dumps(result)) - self.assertLessEqual(gc.tokens(result), gc.input_budget(req, 128000)) - self.assertEqual(req, before) - self.assertTrue(self.server.gelen) - self.assertTrue(all(b"OPAQUE_NATIVE" not in call["govde"] for call in self.server.gelen)) + def test_large_native_return_keeps_original_encrypted_execution_state(self): + for opaque in ({"type": "reasoning", "encrypted_content": "OPAQUE_NATIVE"}, + {"type": "agent_message", "content": [ + {"type": "encrypted_content", "encrypted_content": "OPAQUE_PEER"}]}): + req = {"model": "gpt-6-astra", "input": [ + {"role": "user", "content": "KARAR-4827 " + "x" * 190000}, + opaque, {"role": "user", "content": "Latest"}]} + before = copy.deepcopy(req) + result = self.service._prepare_transfer({"request": req, "source_model": "ide/test"})["request"] + self.assertEqual(result, before) + self.assertEqual(req, before) + self.assertEqual(self.server.gelen, []) 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"}]} @@ -281,30 +280,38 @@ class PreparationService(unittest.TestCase): 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", - "input": [{"type": "agent_message", "author": "/root", "recipient": "/root/peer", - "content": [{"type": "input_text", "text": "Message Type: NEW_TASK\nPayload:\n"}, - {"type": "encrypted_content", "encrypted_content": "gAAAA_synthetic_opaque"}]}]} + "input": [{"role": "user", "content": "USER_DECISION"}, + {"type": "agent_message", "author": "/root", "recipient": "/root/peer", + "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) - raw = json.dumps(req).encode() - # Identical retries must receive the same permanent error without a - # preparation request, a stream heartbeat, or provider inference. - for _ in range(2): - status, body, headers = self.iste("POST", "/backend-api/codex/responses", raw) - self.assertEqual(status, 400) - self.assertEqual(headers["Content-Type"].split(";")[0], "application/json") - self.assertNotIn("Transfer-Encoding", headers) - error = json.loads(body)["error"] - self.assertEqual(error["code"], "aiturk_transfer_failed") - self.assertEqual(error["type"], "invalid_request_error") - self.assertFalse(error["retryable"]) - self.assertIn("görev eksik gönderilmedi", error["message"]) - self.assertNotIn(b"response.failed", body) - self.assertEqual(req, original) + for model in (req["model"], "ide/another-provider"): + req["model"] = model + status, body, _ = self.iste("POST", "/backend-api/codex/responses", json.dumps(req).encode()) + self.assertEqual(status, 200) + self.assertNotIn(b"aiturk_transfer_failed", body) + sent = json.loads(self.kopru.gelen[-1]["govde"]) + wire = json.dumps(sent, ensure_ascii=False) + self.assertNotIn("gAAAA_synthetic_opaque", wire) + self.assertNotIn("encrypted_content", wire) + self.assertIn(gc.PEER_OMITTED, wire) + self.assertIn("VISIBLE_PEER_HEADER", wire) + for item in original["input"][:1] + original["input"][2:]: + self.assertIn(item, sent["input"]) + self.assertEqual(req["input"], original["input"]) self.assertEqual(self.ozgun.gelen, []) - self.assertEqual(self.kopru.gelen, []) - self.assertEqual(bridge.TRANSFERS, {}) + # Returning uses the client's original transcript, including opaque peer + # 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): req = {"model": "gpt-6-astra", "input": [{"type": "agent_message", "author": "/root", diff --git a/apps/desktop/electron/codex-link-config.ts b/apps/desktop/electron/codex-link-config.ts index 51155c6..ea8f72d 100644 --- a/apps/desktop/electron/codex-link-config.ts +++ b/apps/desktop/electron/codex-link-config.ts @@ -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' // 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. -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_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' diff --git a/apps/desktop/package.json b/apps/desktop/package.json index cc4fb41..dc666af 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -2,7 +2,7 @@ "name": "aiturk-ide", "productName": "AITURK IDE", "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.", "author": "AITURK / TurkServis", "repository": { diff --git a/package-lock.json b/package-lock.json index 82162de..000f557 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ }, "apps/desktop": { "name": "aiturk-ide", - "version": "1.0.0-beta.44", + "version": "1.0.0-beta.45", "license": "MIT", "dependencies": { "@assistant-ui/core": "0.2.23",