Compare commits

..
Author SHA1 Message Date
yilsemandClaude Opus 5.5 b347abe1f6 release(ide): ship beta.46 with original subscription WebSocket
Server CI and Windows package / verify (push) Successful in 5m42s
Server CI and Windows package / windows-package (push) Successful in 9m56s
Bridge version 25 so the installed IDE replaces the running splitter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeNgWNzPjzUKQuS32RUYXx
2026-10-20 22:38:51 +03:00
yilsemandClaude Opus 5.5 be14cf2923 fix(codex-link): tunnel subscription WebSocket to chatgpt.com unchanged
Server CI and Windows package / windows-package (push) Successful in 10m51s
Server CI and Windows package / verify (push) Successful in 6m28s
Codex Baglama rejected every Responses WebSocket with 426, so Codex's own
subscription models fell back to HTTP and re-sent the whole conversation on
every step. The splitter now tunnels the WebSocket of Codex's own models to
chatgpt.com byte for byte (model known from x-codex-routing-hint). Added
models, remembered transfers and transfer history in a message still get
426 / a close frame and stay on the existing HTTP path, so added-model and
transition handling is unchanged. bridge.json "abonelik_websocket": false
restores the old refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeNgWNzPjzUKQuS32RUYXx
2026-10-20 22:17:57 +03:00
yilsem 1f13b86e5f fix(context): preserve summary details and unify IDE release branches
Server CI and Windows package / verify (push) Successful in 3m45s
Server CI and Windows package / windows-package (push) Successful in 9m49s
Use the complete target budget, reject omitted concrete references, rebuild deficient checkpoints, and run both branches through Linux CI with an exact-branch parity gate. IDE beta.45, bridge24.
2026-10-20 21:02:45 +03:00
yilsem 7a479e46be merge: bring Windows IDE model transitions into aiturk/main
ci / approval-gate-windows (push) Canceled after 0s
Preserve main workflow, existing approval fixes, and both histories.
2026-10-20 20:37:13 +03:00
yilsem db46a470ea 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.
2026-10-20 20:24:47 +03:00
yilsem 21a8f3b8e9 ci: preserve IDE workflow line endings
Server CI and Windows package / verify (push) Successful in 7m18s
Server CI and Windows package / windows-package (push) Successful in 18m51s
2026-10-20 17:45:33 +03:00
yilsem fb62f31be3 ci: cancel superseded user-facing IDE workflow runs
Server CI and Windows package / verify (push) Canceled after 21s
Server CI and Windows package / windows-package (push) Canceled after 0s
2026-10-20 17:45:08 +03:00
yilsem 1bf80e06a3 ci: cancel superseded workflow runs with concurrency
ci / approval-gate-windows (push) Canceled after 0s
2026-10-20 17:06:05 +03:00
yilsemandClaude Opus 5.5 69e2f98e18 ci(gitea): run the approval-gate tests on the Windows runner
This fork had no CI on Gitea (Actions was off; the upstream .github
workflows do not run here). Add one job that runs the protected
instruction-file gate tests and the file-write safety tests on the
Windows runner. Seven upstream tests that cannot pass on Windows (POSIX
paths, symlink privilege, POSIX stat) are deselected by name and listed
in the job log.

Kart: turkservis-20261007-1421-2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwSpve9HuXMy7dHf4RDMzX
2026-10-07 18:12:11 +03:00
yilsemandClaude Opus 5.5 0d8c521a3c fix(approval): fail closed at once when no human can see the approval prompt
A one-shot run (hermes chat --query-file ... -Q, stdin closed, no window)
still installs the prompt_toolkit approval callback but never starts the
prompt UI. The protected instruction-file gate (AGENTS.md / CLAUDE.md /
SOUL.md ...) took the registered callback for a reachable human and waited
out approvals.timeout on a panel painted nowhere. With the profile value of
300000 s the stage job hung for good.

- Gate: in a one-shot run, or when only an automatic approver
  (subagent_auto_approve) is attached, return BLOCKED / APPROVAL REQUIRED
  immediately instead of prompting. Human consent is still required every
  time; yolo and approvals.single_query_mode still do not open this gate.
- One-shot exit prints an "approval_required: {json}" line on stderr
  (session_id, tool_call_id, request_id, targets, reason, applied=false)
  so the launcher can tell "blocked, needs a human" from "still working".
- execute_code: an approval that arrives after the calling script stopped
  waiting (300 s reply timeout, script run timeout, retired kernel cell)
  is not applied.
- Same invisible wait removed from the computer_use and memory-write
  prompts in a one-shot run. The one-shot marker is no longer inherited by
  child processes.
- BLOCKED text no longer breaks on a target path containing braces.

Long-running tools are untouched: no general tool timeout was shortened.

Kart: turkservis-20261007-1421-2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwSpve9HuXMy7dHf4RDMzX
2026-10-07 17:16:43 +03:00
13 changed files with 631 additions and 78 deletions

No files matched your search

+14
View File
@@ -5,6 +5,10 @@ on:
branches: [aiturk/main, fix/ide-windows-vcruntime] branches: [aiturk/main, fix/ide-windows-vcruntime]
workflow_dispatch: workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: permissions:
contents: read contents: read
@@ -19,6 +23,16 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
github-server-url: https://gitea.turkservis.online github-server-url: https://gitea.turkservis.online
- name: Require main and Windows release to share the exact source
run: |
set -eu
main_sha="$(git ls-remote origin refs/heads/aiturk/main | cut -f1)"
release_sha="$(git ls-remote origin refs/heads/fix/ide-windows-vcruntime | cut -f1)"
if [ "$main_sha" != "$GITHUB_SHA" ] || [ "$release_sha" != "$GITHUB_SHA" ]; then
echo "Both IDE branches must be updated together to the same merged commit."
echo "Use one atomic push; refusing to publish a divergent branch."
exit 1
fi
- name: Exact source and Linux test environment - name: Exact source and Linux test environment
run: | run: |
set -eu set -eu
@@ -27,6 +27,44 @@ 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 25: subscription WebSocket stays original (IDE beta.46)
User decision (20 October 2026): "Codex orjinal aynen kalacak, sadece ek combo ve model ekleyeceğiz."
Codex opens a Responses WebSocket for its own models. The splitter used to refuse it with 426, so
subscription chats fell back to HTTP and re-sent the whole conversation on every step. Now:
- Codex's own model (`x-codex-routing-hint: model=...`): the WebSocket is tunnelled to chatgpt.com byte for byte.
- Added model, remembered transfer, or transfer history inside a message: 426 / close frame; that chat stays
on the existing HTTP path for the life of the Codex process, so added-model and transition handling is unchanged.
- `bridge.json` `"abonelik_websocket": false` restores the old refusal.
## Version 24: portable model switches and summary coverage (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.
The compression target is a maximum, not a minimum length. All of the available
target budget is offered to the summarizer; compression level controls prose
density and no longer multiplies that budget down a second time. Numbered
decision identifiers, file references and numeric settings must survive the
summary before it is accepted or cached. An incomplete provider summary tries
the configured fallback; a deficient old checkpoint is ignored and rebuilt.
These literal checks detect concrete omissions; they do not prove semantic
equivalence of every sentence.
## 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
+225 -17
View File
@@ -11,6 +11,8 @@ Codex'te tek adres ayari (`openai_base_url`) oldugu icin butun istekler buraya g
(ide/..., 9r/..., model_map) o surece HIC gitmez. (ide/..., 9r/..., model_map) o surece HIC gitmez.
* Model listesi -> kopru ayaktaysa ek modelleri eklenmis liste; degilse chatgpt.com'un listesi. * Model listesi -> kopru ayaktaysa ek modelleri eklenmis liste; degilse chatgpt.com'un listesi.
* Geri kalan her sey -> chatgpt.com'a, oldugu gibi. * Geri kalan her sey -> chatgpt.com'a, oldugu gibi.
* Surekli baglanti (WebSocket) -> Codex'in kendi modeliyse chatgpt.com'a bayt bayt tunel (Codex'in ozgun hizli yolu).
Ek model ya da gecis gecmisi gorulurse o sohbet 426 ile HTTP'ye birakilir; ek model/gecis mantigi HTTP'de kalir.
Karisik gecmiste yerel aktarim hazirligi inference'dan ayridir. Abonelik Karisik gecmiste yerel aktarim hazirligi inference'dan ayridir. Abonelik
inference'i her zaman CHATGPT'ye; ek model inference'i KOPRU'ye gider. inference'i her zaman CHATGPT'ye; ek model inference'i KOPRU'ye gider.
@@ -21,6 +23,11 @@ Yerel denetim: GET /aiturk/codex-baglama/status, POST .../stop, POST .../refresh
""" """
import gzip import gzip
import hashlib import hashlib
import socket
import ssl
import struct
import urllib.parse
import zlib
import http.client import http.client
import http.server import http.server
import json import json
@@ -35,7 +42,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 = "25" # 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]
@@ -160,6 +167,88 @@ def model_oku(govde, kodlama):
return None return None
# --- Surekli baglanti (WebSocket) ---------------------------------------------------------------------------
# Kullanici karari (20 Ekim 2026): "Codex orjinal aynen kalacakti, sadece ek combo ve model ekleyecektik."
# Codex abonelik modelinde chatgpt.com'a WebSocket acar; reddedilirse HTTP'ye duser ve her adimda butun sohbeti
# yeniden gonderir. Abonelik sohbeti bu yuzden chatgpt.com'a dokunulmadan tunellenir. Ek model ya da gecis
# gecmisi olan sohbet ilk anda 426 alir ve HTTP'de kalir (bugunku ek model/gecis davranisi aynen).
WS_HTTP = {} # session-id -> zaman: bu Codex surecinde HTTP'de kalacak sohbetler
ws_kilit = threading.Lock()
def ws_ipucu_model(baslik):
"""Codex'in el sikismadaki model ipucu (x-codex-routing-hint: model=gpt-6.1-sol)."""
for parca in (baslik or "").replace(",", ";").split(";"):
k, _, v = parca.partition("=")
if k.strip().lower() == "model":
return v.strip()
return ""
def ws_http_isaretle(sid):
if not sid:
return
with ws_kilit:
WS_HTTP[sid] = time.time()
while len(WS_HTTP) > 4096:
del WS_HTTP[next(iter(WS_HTTP))]
def ws_cerceve_oku(oku):
"""Tek cerceve: (ham_bayt, fin, rsv1, opcode, acik_yuk) ya da baglanti bittiyse None."""
bas = oku(2)
if len(bas) < 2:
return None
b0, b1 = bas[0], bas[1]
n, ek = b1 & 0x7F, b""
if n == 126:
ek = oku(2)
if len(ek) < 2:
return None
n = struct.unpack(">H", ek)[0]
elif n == 127:
ek = oku(8)
if len(ek) < 8:
return None
n = struct.unpack(">Q", ek)[0]
maske = oku(4) if b1 & 0x80 else b""
if b1 & 0x80 and len(maske) < 4:
return None
yuk = oku(n) if n else b""
if len(yuk) < n:
return None
ham = bas + ek + maske + yuk
if maske and n:
m = (maske * (n // 4 + 1))[:n]
yuk = (int.from_bytes(yuk, "big") ^ int.from_bytes(m, "big")).to_bytes(n, "big")
return ham, bool(b0 & 0x80), bool(b0 & 0x40), b0 & 0x0F, yuk
def ws_kapat_cercevesi(kod=1011, neden="AITURK: HTTP yoluna geciliyor"):
yuk = struct.pack(">H", kod) + neden.encode("utf-8")[:120]
return bytes([0x88, len(yuk)]) + yuk
def ws_mesaj_engeli(veri, hesap=""):
"""Bu mesaj chatgpt.com'a tunelden gidemez mi? Neden metni ya da None (gidebilir)."""
try:
m = json.loads(veri)
except (ValueError, UnicodeDecodeError):
return None
if not isinstance(m, dict) or m.get("type") != "response.create":
return None
if ek_model_mu(m.get("model")):
return "ek model"
if gecis_context.foreign(m) or gecis_context.foreign_history(m):
return "gecis gecmisi"
thread = str(m.get("prompt_cache_key") or "")
if thread:
scope = hashlib.sha256((str(hesap or "") + "|" + thread).encode()).hexdigest()
if ek_model_mu(TRANSFERS.get(scope) or ""):
return "gecis gecmisi"
return None
ISTEK_ATLA = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection"} ISTEK_ATLA = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection"}
YANIT_ATLA = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection", "content-encoding"} YANIT_ATLA = {"host", "content-length", "accept-encoding", "transfer-encoding", "connection", "content-encoding"}
@@ -491,6 +580,138 @@ class H(http.server.BaseHTTPRequestHandler):
return None, True return None, True
return result["value"], started return result["value"], started
def _ws_ret(self, n, neden):
"""426: Codex bekletmeden HTTP yoluna duser (ek model ve gecisler bu yolda islenir)."""
log(f"[{n} WS] HTTP yoluna birakildi: {neden}")
self.send_response(426)
self.send_header("Content-Length", "0")
self.send_header("Connection", "close")
self.end_headers()
self.close_connection = True
def _ws(self, n):
model = ws_ipucu_model(self.headers.get("x-codex-routing-hint"))
sid = self.headers.get("session-id") or self.headers.get("thread-id") or ""
hesap = self.headers.get("chatgpt-account-id") or ""
thread = self.headers.get("thread-id") or sid
scope = hashlib.sha256((str(hesap) + "|" + thread).encode()).hexdigest() if thread else ""
if CFG.get("abonelik_websocket") is False:
return self._ws_ret(n, "ayarla kapali")
if not self.path.split("?", 1)[0].rstrip("/").endswith("/responses"):
return self._ws_ret(n, "yol")
if not model:
return self._ws_ret(n, "model ipucu yok")
if ek_model_mu(model):
return self._ws_ret(n, f"ek model {model}")
with ws_kilit:
http_de = sid in WS_HTTP
if http_de:
return self._ws_ret(n, "sohbet HTTP yolunda")
if scope and ek_model_mu(TRANSFERS.get(scope) or ""):
return self._ws_ret(n, "gecis gecmisi")
u = urllib.parse.urlsplit(CHATGPT)
guvenli = u.scheme == "https"
try:
ust = socket.create_connection((u.hostname, u.port or (443 if guvenli else 80)), timeout=20)
if guvenli:
ust = ssl.create_default_context().wrap_socket(ust, server_hostname=u.hostname)
satirlar = [f"GET {self.path} HTTP/1.1", f"Host: {u.netloc}"]
satirlar += [f"{k}: {v}" for k, v in self.headers.items() if k.lower() != "host"]
ust.sendall(("\r\n".join(satirlar) + "\r\n\r\n").encode("latin-1"))
cevap = b""
while b"\r\n\r\n" not in cevap:
parca = ust.recv(65536)
if not parca or len(cevap) > 65536:
raise OSError("el sikisma cevabi eksik")
cevap += parca
bas, _, artik = cevap.partition(b"\r\n\r\n")
durum = int(bas.split(b" ", 2)[1])
except (OSError, ValueError, IndexError) as e:
return self._ws_ret(n, f"chatgpt.com acilamadi: {type(e).__name__}: {e}")
if durum != 101:
ust.close()
return self._ws_ret(n, f"chatgpt.com {durum}")
uzanti = ""
for satir in bas.split(b"\r\n")[1:]:
k, _, v = satir.decode("latin-1").partition(":")
if k.strip().lower() == "sec-websocket-extensions":
uzanti = v.strip().lower()
sikistirma = "permessage-deflate" in uzanti
baglamsiz = "client_no_context_takeover" in uzanti
self.close_connection = True
yaz_kilit = threading.Lock()
bitti = threading.Event()
ust.settimeout(None)
self.connection.settimeout(None)
def istemciye(veri):
with yaz_kilit:
self.wfile.write(veri)
self.wfile.flush()
def kapat():
bitti.set()
for soket in (ust, self.connection):
try:
soket.shutdown(socket.SHUT_RDWR)
except OSError:
pass
istemciye(bas + b"\r\n\r\n" + artik)
log(f"[{n} WS {model}] chatgpt.com'a dogrudan tunel acildi")
def asagi():
try:
while True:
parca = ust.recv(65536)
if not parca:
break
istemciye(parca)
except OSError:
pass
finally:
kapat()
threading.Thread(target=asagi, daemon=True).start()
acici = zlib.decompressobj(-zlib.MAX_WBITS) if sikistirma else None
ham_parcalar, yukler, sikisik, mesaj = [], [], False, 0
try:
while not bitti.is_set():
c = ws_cerceve_oku(self.rfile.read)
if c is None:
break
ham, fin, rsv1, op, yuk = c
if op >= 8:
ust.sendall(ham)
continue
if op in (1, 2):
ham_parcalar, yukler, sikisik = [ham], [yuk], rsv1
else:
ham_parcalar.append(ham)
yukler.append(yuk)
if not fin:
continue
veri = b"".join(yukler)
if sikisik and acici is not None:
if baglamsiz:
acici = zlib.decompressobj(-zlib.MAX_WBITS)
veri = acici.decompress(veri + b"\x00\x00\xff\xff")
neden = ws_mesaj_engeli(veri, hesap)
if neden:
ws_http_isaretle(sid)
log(f"[{n} WS {model}] {neden}: sohbet HTTP yoluna aktarildi ({mesaj} mesaj tunelden gitti)")
istemciye(ws_kapat_cercevesi())
break
ust.sendall(b"".join(ham_parcalar))
ham_parcalar, yukler = [], []
mesaj += 1
except (OSError, zlib.error) as e:
log(f"[{n} WS {model}] baglanti hatasi: {type(e).__name__}: {e}")
finally:
kapat()
ust.close()
log(f"[{n} WS {model}] kapandi, {mesaj} mesaj")
def _isle(self): def _isle(self):
self._native_return_scope = None self._native_return_scope = None
self._failure_key = None self._failure_key = None
@@ -498,12 +719,7 @@ class H(http.server.BaseHTTPRequestHandler):
sira[0] += 1 sira[0] += 1
n = sira[0] n = sira[0]
if "websocket" in (self.headers.get("Upgrade") or "").lower(): if "websocket" in (self.headers.get("Upgrade") or "").lower():
# Bugunku calisan durum: surekli acik baglanti reddedilir, Codex olagan yola duser. return self._ws(n)
self.send_response(426)
self.send_header("Content-Length", "0")
self.send_header("Connection", "close")
self.end_headers()
return
govde = self._oku() govde = self._oku()
yol = self.path yol = self.path
if yol.startswith("/aiturk/codex-baglama/"): if yol.startswith("/aiturk/codex-baglama/"):
@@ -521,16 +737,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 +753,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))
@@ -142,6 +142,7 @@ def call(handler, module, slug, source, budget, settings, deadline, depth=0):
text = ozet_kopru.sse_metni(bytes(raw)) text = ozet_kopru.sse_metni(bytes(raw))
if not text or len(text.strip()) < 80: if not text or len(text.strip()) < 80:
raise gc.TransferError('Sıkıştırıcı tamamlanmış bir özet döndürmedi') raise gc.TransferError('Sıkıştırıcı tamamlanmış bir özet döndürmedi')
gc.require_summary_coverage(chunk, text)
results.append(text) results.append(text)
combined = '\n\n'.join(results) combined = '\n\n'.join(results)
if not combined: if not combined:
@@ -211,6 +212,8 @@ def prepare(handler, module, payload, checkpoints):
protected, old, recent = gc.split_history(req.get('input') or [], settings['keepTurns']) protected, old, recent = gc.split_history(req.get('input') or [], settings['keepTurns'])
checkpoints.begin(scope, old, settings) checkpoints.begin(scope, old, settings)
saved = checkpoints.get(scope, old, settings) if old else None saved = checkpoints.get(scope, old, settings) if old else None
if saved and gc.summary_missing(old[:saved[0]], saved[1]):
saved = None
effective = dict(req, input=protected + [ozet_kopru._ozet_mesaji(saved[1])] + old[saved[0]:] + recent) if saved else req effective = dict(req, input=protected + [ozet_kopru._ozet_mesaji(saved[1])] + old[saved[0]:] + recent) if saved else req
if not compact and gc.tokens(effective) <= trigger: if not compact and gc.tokens(effective) <= trigger:
return {'request': effective, 'window': window} return {'request': effective, 'window': window}
@@ -12,11 +12,52 @@ import threading
from collections import OrderedDict from collections import OrderedDict
import ozet_kopru import ozet_kopru
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ı.]"
def summary_missing(source, summary):
"""Check literal decisions, file paths and numeric settings, never secrets.
This is a loss detector for concrete references, not a semantic proof of
every sentence. Missing references must not enter a durable checkpoint.
"""
if isinstance(source, str):
try:
source = json.loads(source)
except ValueError:
pass
def strings(value):
if isinstance(value, str):
yield value
elif isinstance(value, dict):
for child in value.values():
yield from strings(child)
elif isinstance(value, list):
for child in value:
yield from strings(child)
text = '\n'.join(strings(source)).replace('\\', '/')
target = summary.replace('\\', '/').replace('`', '')
identifiers = set(re.findall(r'\b[A-Z][A-Z0-9]{0,23}(?:-[A-Z][A-Z0-9]{0,23}){0,3}-\d[A-Za-z0-9_-]{0,63}\b', text))
identifiers = {x for x in identifiers if not re.match(r'(?:SECRET|TOKEN|APIKEY|PASSWORD|OTP)-', x)}
paths = set(re.findall(r'(?<![A-Za-z0-9_.-])(?:[A-Za-z]:/(?:[A-Za-z0-9_.-]{1,160}/){0,24}|(?:[A-Za-z0-9_.-]{1,160}/){1,24})[A-Za-z0-9_.-]{1,160}\.(?:json|py|tsx?|jsx?|md|txt|ya?ml|toml|ini|sql|csv|log)\b', text)) if '/' in text else set()
assignments = set(re.findall(r'\b([A-Za-z_][A-Za-z0-9_]{0,63})\s*=\s*(\d{1,16}|true|false)\b', text))
assignments = {(key, value) for key, value in assignments
if not re.search(r'key|token|secret|password|sifre|anahtar|otp', key, re.I)}
missing = sum(value not in target for value in identifiers | paths)
missing += sum(not re.search(re.escape(key) + r'\s*[=:]\s*' + re.escape(value) + r'\b', target)
for key, value in assignments)
return missing
def require_summary_coverage(source, summary):
missing = summary_missing(source, summary)
if missing:
raise TransferError(f'Özet {missing} karar, dosya yolu veya sayısal ayarı korumadı; eksik özet kabul edilmedi.')
class TransferError(ValueError): class TransferError(ValueError):
@@ -56,9 +97,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 +181,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 +200,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.
@@ -214,6 +258,8 @@ class Preparer:
protected, old, recent = split_history(req.get("input") or [], keep) protected, old, recent = split_history(req.get("input") or [], keep)
original_old = old original_old = old
saved = checkpoints.get(scope, old, settings) if checkpoints and old else None saved = checkpoints.get(scope, old, settings) if checkpoints and old else None
if saved and summary_missing(old[:saved[0]], saved[1]):
saved = None
if saved: if saved:
old = [ozet_kopru._ozet_mesaji(saved[1])] + old[saved[0]:] old = [ozet_kopru._ozet_mesaji(saved[1])] + old[saved[0]:]
req = dict(req, input=protected + old + recent) req = dict(req, input=protected + old + recent)
@@ -233,8 +279,10 @@ class Preparer:
if any(isinstance(x, dict) and x.get("encrypted_content") for x in old): if any(isinstance(x, dict) and x.get("encrypted_content") for x in old):
raise TransferError("Şifreli geçmiş önce sahibi tarafından okunabilir özete çevrilmeli.") raise TransferError("Şifreli geçmiş önce sahibi tarafından okunabilir özete çevrilmeli.")
source = encoded(old) source = encoded(old)
factor = compression_settings.LEVELS[settings['level']] if settings else .5 # The configured target already bounds the resulting context. Applying
budget = max(512, int(available * factor)) # the level's ratio again denied the summarizer most of that budget.
# Level controls writing style; the whole available budget stays usable.
budget = max(512, available)
key = hashlib.sha256((scope + str(budget) + encoded(settings) + source).encode()).hexdigest() key = hashlib.sha256((scope + str(budget) + encoded(settings) + source).encode()).hexdigest()
with self.lock: with self.lock:
summary = self.cache.get(key) summary = self.cache.get(key)
@@ -242,6 +290,7 @@ class Preparer:
summary = summarize(source, budget) summary = summarize(source, budget)
if not isinstance(summary, str) or len(summary.strip()) < 80: if not isinstance(summary, str) or len(summary.strip()) < 80:
raise TransferError("AITURK özeti tamamlanamadı; eski geçmiş korundu.") raise TransferError("AITURK özeti tamamlanamadı; eski geçmiş korundu.")
require_summary_coverage(old, summary)
candidate = dict(req, input=protected + [ozet_kopru._ozet_mesaji(summary)] + recent) candidate = dict(req, input=protected + [ozet_kopru._ozet_mesaji(summary)] + recent)
if tokens(candidate) > limit: if tokens(candidate) > limit:
raise TransferError("Hazırlanan özet hedef bütçesine sığmadı; eski geçmiş korundu.") raise TransferError("Hazırlanan özet hedef bütçesine sığmadı; eski geçmiş korundu.")
@@ -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):
@@ -6,10 +6,13 @@ import hashlib
import http.client import http.client
import http.server import http.server
import json import json
import socket
import struct
import tempfile import tempfile
import threading import threading
import time import time
import unittest import unittest
import zlib
import bridge import bridge
@@ -57,6 +60,70 @@ def zstd(veri):
return zstandard.ZstdCompressor().compress(veri) return zstandard.ZstdCompressor().compress(veri)
def ws_cerceve(veri, op=1, rsv1=False, maske=b"\x01\x02\x03\x04"):
"""Istemci (Codex) cercevesi: maskeli."""
b0 = 0x80 | (0x40 if rsv1 else 0) | op
n = len(veri)
bas = bytes([b0, 0x80 | n]) if n < 126 else bytes([b0, 0x80 | 126]) + struct.pack(">H", n) if n < 65536 \
else bytes([b0, 0x80 | 127]) + struct.pack(">Q", n)
return bas + maske + bytes(c ^ maske[i % 4] for i, c in enumerate(veri))
def sikistir(sik, veri):
return (sik.compress(veri) + sik.flush(zlib.Z_SYNC_FLUSH))[:-4]
class SahteWs:
"""Sahte chatgpt.com WebSocket ucu: el sikismayi ve gelen ham cerceveleri kaydeder, her mesaja ozetle cevap verir."""
def __init__(self, durum=101, uzanti=""):
self.durum, self.uzanti, self.ham, self.baslik, self.baglanti = durum, uzanti, [], {}, 0
self.dinle = socket.socket()
self.dinle.bind(("127.0.0.1", 0))
self.dinle.listen(8)
self.port = self.dinle.getsockname()[1]
threading.Thread(target=self._kabul, daemon=True).start()
def _kabul(self):
while True:
try:
s, _ = self.dinle.accept()
except OSError:
return
self.baglanti += 1
threading.Thread(target=self._bag, args=(s,), daemon=True).start()
def _bag(self, s):
f = s.makefile("rb")
satirlar = []
while True:
satir = f.readline().decode("latin-1").rstrip("\r\n")
if not satir:
break
satirlar.append(satir)
for satir in satirlar[1:]:
k, _, v = satir.partition(":")
self.baslik[k.strip().lower()] = v.strip()
if self.durum != 101:
s.sendall(b"HTTP/1.1 %d Hayir\r\nContent-Length: 0\r\n\r\n" % self.durum)
s.close()
return
cevap = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
if self.uzanti:
cevap += f"Sec-WebSocket-Extensions: {self.uzanti}\r\n"
s.sendall((cevap + "\r\n").encode())
ac = zlib.decompressobj(-zlib.MAX_WBITS)
while True:
c = bridge.ws_cerceve_oku(f.read)
if c is None or c[3] == 8:
break
self.ham.append(c[0])
veri = ac.decompress(c[4] + b"\x00\x00\xff\xff") if c[2] else c[4]
yuk = b"yanit:" + hashlib.sha256(veri).hexdigest().encode()
s.sendall(bytes([0x81, len(yuk)]) + yuk)
s.close()
class Ayirici(unittest.TestCase): class Ayirici(unittest.TestCase):
def setUp(self): def setUp(self):
self.ozgun = sunucu(duz(200, b'event: response.completed\ndata: {"ok":1}\n\n', "text/event-stream")) self.ozgun = sunucu(duz(200, b'event: response.completed\ndata: {"ok":1}\n\n', "text/event-stream"))
@@ -72,6 +139,7 @@ class Ayirici(unittest.TestCase):
self.transfer_old = bridge.TRANSFER_PATH, dict(bridge.TRANSFERS) self.transfer_old = bridge.TRANSFER_PATH, dict(bridge.TRANSFERS)
bridge.TRANSFER_PATH = self.gecici.name + "/transfers.json" bridge.TRANSFER_PATH = self.gecici.name + "/transfers.json"
bridge.TRANSFERS.clear() bridge.TRANSFERS.clear()
bridge.WS_HTTP.clear()
bridge.CHATGPT = f"http://127.0.0.1:{self.ozgun.server_address[1]}" bridge.CHATGPT = f"http://127.0.0.1:{self.ozgun.server_address[1]}"
bridge.KOPRU = f"http://127.0.0.1:{self.kopru.server_address[1]}" bridge.KOPRU = f"http://127.0.0.1:{self.kopru.server_address[1]}"
bridge.LOGDIR = self.gecici.name bridge.LOGDIR = self.gecici.name
@@ -209,11 +277,107 @@ class Ayirici(unittest.TestCase):
d = json.loads(self.iste("GET", "/aiturk/codex-baglama/status")[1]) d = json.loads(self.iste("GET", "/aiturk/codex-baglama/status")[1])
self.assertEqual((d["surum"], d["ide_model"], d["kopru"], d["ayirici"]), (bridge.SURUM, 147, "calisiyor", True)) self.assertEqual((d["surum"], d["ide_model"], d["kopru"], d["ayirici"]), (bridge.SURUM, 147, "calisiyor", True))
def test_a_persistent_connection_is_refused_the_way_it_is_today(self): def test_a_persistent_connection_without_a_model_hint_is_refused_the_way_it_is_today(self):
durum, _, _ = self.iste("GET", "/backend-api/codex/responses", None, {"Upgrade": "websocket", "Connection": "Upgrade"}) durum, _, _ = self.iste("GET", "/backend-api/codex/responses", None, {"Upgrade": "websocket", "Connection": "Upgrade"})
self.assertEqual(durum, 426) self.assertEqual(durum, 426)
self.assertEqual(self.ozgun.gelen + self.kopru.gelen, []) self.assertEqual(self.ozgun.gelen + self.kopru.gelen, [])
# --- Surekli baglanti: abonelik modeli chatgpt.com'a tunellenir, ek model/gecis HTTP'de kalir ---------------
def ws_ust(self, durum=101, uzanti=""):
ust = SahteWs(durum, uzanti)
bridge.CHATGPT = f"http://127.0.0.1:{ust.port}"
return ust
def ws_ac(self, model="gpt-6.1-sol", sid="sohbet-1", uzanti=True):
k = socket.create_connection(("127.0.0.1", self.on.server_address[1]), timeout=10)
baslik = ["GET /backend-api/codex/responses HTTP/1.1", "Host: 127.0.0.1", "Connection: Upgrade",
"Upgrade: websocket", "Sec-WebSocket-Version: 13", "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",
"authorization: Bearer sahte", "chatgpt-account-id: hesap", f"session-id: {sid}", f"thread-id: {sid}",
"openai-beta: responses_websockets=2026-02-06"]
if model:
baslik.append(f"x-codex-routing-hint: model={model}")
if uzanti:
baslik.append("sec-websocket-extensions: permessage-deflate; client_max_window_bits")
k.sendall(("\r\n".join(baslik) + "\r\n\r\n").encode())
cevap = b""
while b"\r\n\r\n" not in cevap:
parca = k.recv(4096)
if not parca:
break
cevap += parca
return k, int(cevap.split(b" ", 2)[1]), cevap.partition(b"\r\n\r\n")[2]
def test_codex_own_model_websocket_is_tunnelled_to_chatgpt_byte_for_byte(self):
ust = self.ws_ust()
k, durum, _ = self.ws_ac()
self.assertEqual(durum, 101)
mesaj = json.dumps({"type": "response.create", "model": "gpt-6.1-sol", "input": [{"type": "message", "role": "user",
"content": [{"type": "input_text", "text": "selam " * 5000}]}]}).encode()
cerceve = ws_cerceve(mesaj)
k.sendall(cerceve)
cevap = bridge.ws_cerceve_oku(k.makefile("rb").read)
self.assertEqual(cevap[4], b"yanit:" + hashlib.sha256(mesaj).hexdigest().encode())
self.assertEqual(ust.ham[0], cerceve) # Codex'in cercevesi aynen gitti
self.assertEqual(ust.baslik["authorization"], "Bearer sahte")
self.assertEqual(ust.baslik["openai-beta"], "responses_websockets=2026-02-06")
self.assertEqual(self.kopru.gelen, [])
k.close()
def test_added_model_websocket_falls_back_to_http_at_once(self):
ust = self.ws_ust()
k, durum, _ = self.ws_ac(model="ide/claude-claude-haiku-5-5")
self.assertEqual(durum, 426)
self.assertEqual(ust.baglanti, 0)
k.close()
def test_switching_to_an_added_model_on_an_open_tunnel_moves_the_chat_to_http(self):
ust = self.ws_ust()
k, durum, _ = self.ws_ac(sid="sohbet-gecis")
self.assertEqual(durum, 101)
k.sendall(ws_cerceve(json.dumps({"type": "response.create", "model": "ide/agy-gemini-3.8-flash-low", "input": []}).encode()))
kapanis = bridge.ws_cerceve_oku(k.makefile("rb").read)
self.assertEqual(kapanis[3], 8) # kapanis cercevesi
self.assertEqual(ust.ham, []) # ek model istegi chatgpt.com'a gitmedi
k.close()
k2, durum2, _ = self.ws_ac(sid="sohbet-gecis")
self.assertEqual(durum2, 426) # ayni sohbet artik HTTP yolunda
k2.close()
def test_compressed_message_with_transfer_history_is_detected(self):
ust = self.ws_ust(uzanti="permessage-deflate")
k, durum, _ = self.ws_ac(sid="sohbet-sikisik")
self.assertEqual(durum, 101)
sik = zlib.compressobj(wbits=-zlib.MAX_WBITS)
ilk = json.dumps({"type": "response.create", "model": "gpt-6.1-sol", "input": [{"type": "message", "role": "user",
"content": [{"type": "input_text", "text": "ilk"}]}]}).encode()
k.sendall(ws_cerceve(sikistir(sik, ilk), rsv1=True))
okur = k.makefile("rb").read
self.assertEqual(bridge.ws_cerceve_oku(okur)[3], 1)
ikinci = json.dumps({"type": "response.create", "model": "gpt-6.1-sol",
"input": [{"type": "message", "role": "assistant", "id": "chatcmpl-abc", "content": []}]}).encode()
k.sendall(ws_cerceve(sikistir(sik, ikinci), rsv1=True))
self.assertEqual(bridge.ws_cerceve_oku(okur)[3], 8)
self.assertEqual(len(ust.ham), 1)
k.close()
def test_chat_with_a_remembered_transfer_stays_on_http(self):
self.ws_ust()
scope = hashlib.sha256(b"hesap|sohbet-eski").hexdigest()
bridge.TRANSFERS[scope] = "ide/Jail-Omni"
k, durum, _ = self.ws_ac(sid="sohbet-eski")
self.assertEqual(durum, 426)
k.close()
def test_chatgpt_refusing_the_tunnel_or_setting_off_falls_back_to_http(self):
ust = self.ws_ust(durum=401)
k, durum, _ = self.ws_ac(sid="sohbet-red")
self.assertEqual((durum, ust.baglanti), (426, 1))
k.close()
bridge.CFG["abonelik_websocket"] = False
k, durum, _ = self.ws_ac(sid="sohbet-kapali")
self.assertEqual((durum, ust.baglanti), (426, 1))
k.close()
def test_which_side_a_model_is_on(self): def test_which_side_a_model_is_on(self):
for slug, ek in (("ide/Opus-5", True), ("9r/ag/gemini", True), ("gpt-6-astra", False), ("gpt-6.1-sol", False), for slug, ek in (("ide/Opus-5", True), ("9r/ag/gemini", True), ("gpt-6-astra", False), ("gpt-6.1-sol", False),
("", False), (None, False), ("idea", False)): ("", False), (None, False), ("idea", False)):
@@ -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",
@@ -0,0 +1,64 @@
import copy
import tempfile
import unittest
import compression_settings as cs
import gecis_context as gc
class SummaryCoverage(unittest.TestCase):
def test_long_noise_is_not_misclassified_as_one_giant_identifier(self):
gc.require_summary_coverage('A' * 60000 + 'KARAR-4827' + 'B' * 60000,
'KARAR-4827: original decision preserved.')
def test_partial_decision_ledger_is_rejected_and_full_ledger_is_accepted(self):
records = [f'DEC-{i:04d}: src/feature_{i:04d}/config.json limit_{i:04d}={30000+i} TEST-QA-{i:04d} NO-DELETE-{i:04d}'
for i in range(200)]
source = [{'role': 'user', 'content': '\n'.join(records)}]
with self.assertRaises(gc.TransferError):
gc.require_summary_coverage(source, '\n'.join(records[:95]))
gc.require_summary_coverage(source, '\n'.join(records))
def test_assignment_values_stay_associated_with_their_keys(self):
source = [{'content': 'limit_a=123 limit_b=456 src/a.py DEC-1'}]
self.assertGreater(gc.summary_missing(source, 'limit_a=456 limit_b=123 src/a.py DEC-1'), 0)
gc.require_summary_coverage(source, 'DEC-1: `src/a.py`; limit_a: 123; limit_b: 456')
def test_secret_named_assignments_are_not_required_in_a_summary(self):
gc.require_summary_coverage([{'content': 'api_key=123456 password=7890 otp=1234'}], 'Secrets omitted.')
def test_target_budget_is_not_multiplied_by_compression_level_again(self):
settings = dict(cs.DEFAULTS, keepTurns=1, triggerPercent=60, targetPercent=25, level='fazla')
req = {'input': [{'role': 'user', 'content': 'DEC-1 ' + 'x' * 60000},
{'role': 'user', 'content': 'Keep this recent turn.'}]}
observed = []
def summarize(source, budget):
observed.append(budget)
return 'DEC-1: All accepted decisions, paths, settings and outstanding work are retained. ' * 2
prepared = gc.Preparer().prepare(req, 32768, summarize, settings=settings, automatic=True)
protected, _, recent = gc.split_history(req['input'], 1)
available = gc.input_budget(req, 32768, settings, True) - gc.tokens(dict(req, input=protected + recent)) - 256
self.assertEqual(observed, [available])
self.assertLessEqual(gc.tokens(prepared), 32768 * .25)
def test_incomplete_old_checkpoint_is_rebuilt_without_overwriting_source(self):
settings = dict(cs.DEFAULTS, keepTurns=1)
req = {'input': [{'role': 'user', 'content': 'DEC-1 src/a.py limit_a=123 ' + 'x' * 60000},
{'role': 'user', 'content': 'Latest'}]}
original = copy.deepcopy(req)
with tempfile.TemporaryDirectory() as home:
store = cs.Checkpoints(home)
old = gc.split_history(req['input'], 1)[1]
store.save('scope', old, settings, 'Incomplete old summary without the required details. ' * 3)
calls = []
def summarize(source, budget):
calls.append(1)
return 'DEC-1 src/a.py limit_a=123. Accepted decision and all relevant constraints retained. ' * 2
result = gc.Preparer().prepare(req, 32768, summarize, 'scope', settings=settings, checkpoints=store)
self.assertEqual(calls, [1])
self.assertEqual(req, original)
gc.require_summary_coverage(old, gc.encoded(result))
if __name__ == '__main__':
unittest.main()
+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 = '25'
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.46",
"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": {
@@ -63,7 +63,8 @@ export function CompressionSettingsView() {
{draft.budgetMode === 'percent' ? numeric('transferPercent', t('Hedef model bağlamı (%)', 'Target model context (%)'), 10, 90) : numeric('maxTokens', t('Maksimum giriş tokenı', 'Maximum input tokens'), 1024, 2000000)} {draft.budgetMode === 'percent' ? numeric('transferPercent', t('Hedef model bağlamı (%)', 'Target model context (%)'), 10, 90) : numeric('maxTokens', t('Maksimum giriş tokenı', 'Maximum input tokens'), 1024, 2000000)}
<ListRow title={t('Otomatik sıkıştırma', 'Automatic compression')} action={<Switch aria-label={t('Otomatik sıkıştırma', 'Automatic compression')} disabled={busy} checked={draft.autoEnabled} onCheckedChange={v => change('autoEnabled', v)} />} /> <ListRow title={t('Otomatik sıkıştırma', 'Automatic compression')} action={<Switch aria-label={t('Otomatik sıkıştırma', 'Automatic compression')} disabled={busy} checked={draft.autoEnabled} onCheckedChange={v => change('autoEnabled', v)} />} />
{numeric('triggerPercent', t('Başlama eşiği (%)', 'Trigger threshold (%)'), 20, 90)} {numeric('triggerPercent', t('Başlama eşiği (%)', 'Trigger threshold (%)'), 20, 90)}
{numeric('targetPercent', t('Sıkıştırma sonrası hedef (%)', 'Target after compression (%)'), 10, 80)} {numeric('targetPercent', t('Sıkıştırma sonrası üst sınır (%)', 'Maximum after compression (%)'), 10, 80)}
<p className="text-sm text-(--ui-text-secondary)">{t('Bu bir üst sınırdır; tekrarlar azaldığında özet daha küçük olabilir. Sıkıştırma seviyesi anlatım yoğunluğunu belirler, bu sınırı ayrıca küçültmez.', 'This is an upper limit; removing repetition can produce a smaller summary. Compression level controls writing density without shrinking this budget again.')}</p>
{numeric('timeoutSeconds', t('Sıkıştırıcı bekleme süresi (saniye)', 'Summarizer timeout (seconds)'), 1, 3600)} {numeric('timeoutSeconds', t('Sıkıştırıcı bekleme süresi (saniye)', 'Summarizer timeout (seconds)'), 1, 3600)}
<Button disabled={busy || draft.targetPercent >= draft.triggerPercent} onClick={async () => { <Button disabled={busy || draft.targetPercent >= draft.triggerPercent} onClick={async () => {
setBusy(true); setMessage(''); setFailed(false) setBusy(true); setMessage(''); setFailed(false)
+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.46",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@assistant-ui/core": "0.2.23", "@assistant-ui/core": "0.2.23",