fix(codex-bridge): close the gaps an independent review found
Server CI and Windows package / verify (push) Successful in 3m23s
Server CI and Windows package / windows-package (push) Successful in 7m9s

- One decision per real turn: a turn starts with a user message after the
  previous final answer; tool steps, messages typed mid-turn, a lone "."
  and compaction keep the combo. Works without prompt_cache_key too.
- The virtual Otomatik name never reaches the gateway (stage off or error
  -> last choice or targets.zor); prefilter.target may not be it.
- finishCheck: at most once per turn; a broken, empty or refused retry
  delivers the already approved first answer.
- Layer window filter: only trusted windows, never the last layer.
- Correction channel only on always-pass targets while checks are on;
  Sesli-Hizli-Zeki stays always-pass by default; a >256 KB SSE frame
  passes unfiltered instead of breaking the stream.
- Turn-start questions wait in the local queue no longer than their
  timeout; decision-turn memory under a lock with refreshed order.
- Dify fetch refuses redirects (the app key never moves host); the
  rules file write retries a Windows sharing violation.
- An unreadable settings file no longer breaks every request.
- TypeScript accepts only https:// URLs the bridge accepts;
  finishCheck.timeoutSeconds (unused) removed.

Tests: 310 bridge tests, 36 electron tests; side-bridge live run for the
turn, prefilter (incl. decision timeout) and finish-check paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
yilsemandClaude Opus 5.5 committed 2026-10-14 08:14:38 +03:00
1 parent 7c4ef18df0
commit a7ba300448
10 files changed
+353 -91

No files matched your search

+114 -29
View File
@@ -245,6 +245,7 @@ def load_ide_catalog(ag=True):
for h in set(oto["targets"].values())),
"arac": True, "kaynak": "", "claude": False}
model_map[slug] = (IDE_ROUTE, oto["model"], None)
OTO_ADI[0] = oto["model"]
n += 1
else:
ROUTES.pop(IDE_ROUTE, None)
@@ -263,68 +264,115 @@ def _oto_ayari():
return None
return s["autoRoute"] if s["enabled"] and s["autoRoute"]["enabled"] else None
# Tur basi kararlari: sohbet -> (tur anahtari, gelen slug, secilen slug). Ayni turun arac adimlari ayni karari kullanir.
# Tur basi kararlari: sohbet -> (tur anahtari, gelen slug, secilen slug). Ayni turun adimlari ayni karari kullanir.
ON_KARAR = {}
ON_KARAR_SINIR = 500
BITIS_TURLARI = {} # finishCheck kullanilan turlar (turda bir kez)
KARAR_KILIDI = threading.Lock()
OTO_ADI = ["Otomatik"] # katalogdaki sanal modelin son adi (karar asamasi hata verirse disari cikmasin)
# Acilista pencere verisi icin aga cikilmaz (IDE koprunun dinlemeye baslamasini sinirli sure bekler);
# ilk tam yenileme asagidaki dongunun basinda, arka planda yapilir.
load_ide_catalog(ag=False)
def tur_bilgisi(req, sohbet):
"""(sohbet koku, tur anahtari, tur basindaki istek, yeni tur mu). Tur, calisan bir turun icine dusmeyen
kullanici mesajiyla baslar. Arac adimlari, tur ortasinda yazilan mesaj, tek "." ve ozet istegi ayni turdadir;
yeni tur yalniz son oge yeni bir kullanici istegi oldugunda baslar."""
girdi = req.get("input") or []
kok = sohbet or "anon:" + hashlib.sha256(json.dumps(girdi[:1], ensure_ascii=False, sort_keys=True,
default=str).encode("utf-8", "replace")).hexdigest()
bas, istek = -1, ""
for i in range(len(girdi) - 1, -1, -1):
metin = jev_denetim.istek_metni(girdi[i])
if not metin or metin == ".":
continue
if not istek:
bas, istek = i, metin # tur basi bulunamazsa en son gercek istek
if not model_gecis.tur_ortasi(req, i):
bas, istek = i, metin
break
son = model_gecis.son_sira(girdi)
yeni = son >= 0 and son == bas and not model_gecis.tur_ortasi(req, son)
tur = hashlib.sha256(f"{kok}\0{bas}\0{istek}".encode("utf-8", "replace")).hexdigest()
return kok, tur, istek, yeni
def karar_asamasi(n, req, sohbet, slug):
"""14 Ekim 2026 karar asamalari: "Otomatik" modelin combo secimi (autoRoute) ve normal modelin reddedecegi
tahmin edilen isi dogrudan ayardaki katmana verme (prefilter). Karar tur basinda bir kez verilir; ayni turun
arac adimlari ayni karari kullanir (her adimda model degisirse saglayicilar arasi gecmis bozulur). Kapali ayar,
bos istek ya da karar modeli hatasi bugunku yolu degistirmez (Otomatik icin guclu hedef secilir)."""
tahmin edilen isi dogrudan ayardaki katmana verme (prefilter). Karar yalniz yeni turda verilir; ayni turun
adimlari ayni karari kullanir (tur ortasinda model degisirse saglayicilar arasi gecmis bozulur). Kapali ayar,
bos istek ya da karar modeli hatasi bugunku yolu degistirmez. Sanal model adi hicbir yolda gecide gitmez."""
if not slug.startswith(IDE_PREFIX):
return slug
settings = jev_settings.read()[0]
a, p = settings["autoRoute"], settings["prefilter"]
sanal = slug == IDE_PREFIX + a["model"]
kok, tur, istek, yeni = tur_bilgisi(req, sohbet)
with KARAR_KILIDI:
kayit = ON_KARAR.get(kok)
if sanal and not (settings["enabled"] and a["enabled"]):
# Kapatilmis Otomatik: son karar ya da guclu hedef; sanal ad gecide gitmez.
secilen = kayit[2] if kayit and kayit[1] == slug else IDE_PREFIX + a["targets"]["zor"]
log(f"[{n} {slug}] Otomatik kapali; bu adim {secilen} modeline gidiyor")
req["model"] = secilen
return secilen
if not settings["enabled"]:
return slug
a, p = settings["autoRoute"], settings["prefilter"]
oto = a["enabled"] and slug == IDE_PREFIX + a["model"]
hedef = slug[len(IDE_PREFIX):]
ret = p["enabled"] and (oto or not (jev_denetim.muaf_mi(hedef) or hedef == p["target"]
or hedef in jev_denetim.sonrakiler(p["target"])))
if not oto and not ret:
ret = p["enabled"] and (sanal or not (jev_denetim.muaf_mi(hedef) or hedef == p["target"]
or hedef in jev_denetim.sonrakiler(p["target"])))
if not sanal and not ret:
return slug
istek, sira = jev_denetim.son_istek_sirasi(req)
# Tur = son kullanici mesaji (metni ve girdideki yeri); ayni turun arac adimlarinda degismez.
tur = hashlib.sha256(f"{sohbet}\0{sira}\0{istek}".encode("utf-8", "replace")).hexdigest()
kayit = ON_KARAR.get(sohbet) if sohbet else None
if kayit and kayit[0] == tur and kayit[1] == slug:
if kayit and kayit[1] == slug and (kayit[0] == tur or not yeni):
secilen = kayit[2]
else:
if istek:
t0 = time.time()
sonuc = jev_denetim.on_karar(istek, oto, ret, jev_denetim.anahtar())
sonuc = jev_denetim.on_karar(istek, sanal, ret, jev_denetim.anahtar())
puan = sonuc["puan"]
log(f"[{n} {slug}] tur basi karari ({time.time() - t0:.1f} sn): "
+ (", ".join(f"{k}={v:.2f}" for k, v in puan.items()) if puan else "karar modeline sorulamadi"))
else:
sonuc = {"hedef": a["targets"]["zor"] if oto else None, "ret": None}
if oto:
sonuc = {"hedef": a["targets"]["zor"] if sanal else None, "ret": None}
if sanal:
hedef = sonuc["hedef"]
secilen = IDE_PREFIX + hedef
if (p["enabled"] and sonuc["ret"] and not jev_denetim.muaf_mi(hedef) and hedef != p["target"]
and hedef not in jev_denetim.sonrakiler(p["target"])):
log(f"[{n} {slug}] ret on kontrolu: normal model reddedebilir, adim {p['target']} katmanindan basliyor")
secilen = IDE_PREFIX + p["target"]
if sohbet:
ON_KARAR[sohbet] = (tur, slug, secilen)
req["model"] = secilen
with KARAR_KILIDI:
ON_KARAR.pop(kok, None) # yeniden ekleme sirayi tazeler: aktif sohbet en son silinir
ON_KARAR[kok] = (tur, slug, secilen)
while len(ON_KARAR) > ON_KARAR_SINIR:
ON_KARAR.pop(next(iter(ON_KARAR)))
ON_KARAR.pop(next(iter(ON_KARAR)), None)
if secilen != slug:
log(f"[{n} {slug}] karar: bu tur {secilen} modeline gidiyor")
req["model"] = secilen
return secilen
def bitis_kullan(tur):
"""finishCheck turda en fazla bir kez: ilk cagri True (ve isaretler), sonrakiler False."""
with KARAR_KILIDI:
if tur in BITIS_TURLARI:
return False
BITIS_TURLARI[tur] = time.time()
while len(BITIS_TURLARI) > ON_KARAR_SINIR:
BITIS_TURLARI.pop(next(iter(BITIS_TURLARI)), None)
return True
def sigan_katmanlar(sira, req, tag):
"""Istegi sigdiramayacak katman zincirden cikar (bos baglam hatasiyla bosa dusmesin)."""
"""Istegi sigdiramayacak katman zincirden cikar (bos baglam hatasiyla bosa dusmesin). Yalniz penceresi
guvenilir (pencere.bekci_calisir) katman atlanir; zincirin son halkasi hic atlanmaz."""
tahmin, sonuc = None, []
for ad in sira:
p = (IDE_BILGI.get(IDE_PREFIX + ad) or {}).get("pencere")
if p:
for i, ad in enumerate(sira):
bilgi = IDE_BILGI.get(IDE_PREFIX + ad) or {}
p = bilgi.get("pencere")
if p and i < len(sira) - 1 and pencere.bekci_calisir(bilgi.get("kaynak"), p):
if tahmin is None:
tahmin = pencere.tahmini_token(req, 2.0)
if tahmin > int(p * 0.90):
@@ -1059,6 +1107,7 @@ class H(http.server.BaseHTTPRequestHandler):
self._akis_basligi(resp, resp.status)
kalan, son_yazi, son_govde = list(sira), time.time(), b""
devam_kullanildi = False # finishCheck: son cevap ayni modele en fazla bir kez geri verilir
yedek = None # geri verilen istek basarisiz olursa teslim edilecek, Jev'den gecmis ilk cevap
def w(b):
if b:
@@ -1129,6 +1178,7 @@ class H(http.server.BaseHTTPRequestHandler):
yeni_devam = None
log(f"{tag} bitis denetimi: devam istegi acilamadi, cevap oldugu gibi geciyor ({type(e).__name__}: {e})")
if yeni_devam is not None:
yedek = (parcalar, rewriter, tag)
resp, rewriter, tag = yeni_devam
log(f"{tag} bitis denetimi: is bitmeden bitti denmis, ayni modele bir kez geri verildi")
continue
@@ -1138,6 +1188,11 @@ class H(http.server.BaseHTTPRequestHandler):
gecmedi = True
log(f"{tag} katman denetimi: bos adim (yazi da komut da yok), siradakine geciliyor: "
f"{b''.join(parcalar)[-200:]!r}")
if gecmedi and yedek is not None:
# Geri verilen istek koptu, bos dondu ya da gecmedi: Jev'den gecmis ilk cevap teslim edilir.
parcalar, rewriter, tag = yedek
gecmedi = False
log(f"{tag} bitis denetimi: geri verilen istek kullanilamadi, ilk cevap teslim ediliyor")
yeni = None
while gecmedi and kalan and yeni is None:
ad = kalan.pop(0)
@@ -1670,6 +1725,12 @@ class H(http.server.BaseHTTPRequestHandler):
slug = karar_asamasi(n, req, sohbet, slug)
except Exception as e:
log(f"[{n} {slug}] karar asamasi atlandi: {type(e).__name__}: {e}")
try:
if slug == IDE_PREFIX + OTO_ADI[0]:
# Sanal ad gecide gitmez: varsayilan guclu hedef.
slug = req["model"] = IDE_PREFIX + jev_settings.DEFAULTS["autoRoute"]["targets"]["zor"]
except Exception:
pass
mapping = MODEL_MAP.get(slug)
if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req) and model_gecis.tur_ortasi(req, model_gecis.son_sira(req["input"])):
# Calisan turun icine dusen nokta secimi tasimaz (olcum 10 Ekim 2026) ve cevaplanirsa isi yarida
@@ -1862,14 +1923,19 @@ class H(http.server.BaseHTTPRequestHandler):
# secili model baska bir rotada olsa bile. Gecit yoksa denetim yapilmaz. Ozet isteginde ve aracsiz
# (rewriter yok) yolda kapalidir.
jev_rota = ROUTES.get(ide_route)
jev_etkin = jev_denetim.etkin_mi() and not (ozet_tetik or rewriter is None or jev_rota is None or not jev_denetim.anahtar())
jev_sira = jev_denetim.sonrakiler(target) if jev_etkin else []
try:
jev_etkin = jev_denetim.etkin_mi() and not (ozet_tetik or rewriter is None or jev_rota is None or not jev_denetim.anahtar())
jev_sira = jev_denetim.sonrakiler(target) if jev_etkin else []
jev_istek = jev_denetim.son_istek(req) if jev_etkin else ""
except Exception as e:
# Okunamayan ayar istegi dusurmez: bu adim denetimsiz gecer.
log(f"{tag} katman denetimi ayari okunamadi, denetimsiz devam: {type(e).__name__}: {e}")
jev_etkin, jev_sira, jev_istek = False, [], ""
if jev_sira:
try:
jev_sira = sigan_katmanlar(jev_sira, req, tag)
except Exception as e:
log(f"{tag} katman pencere kontrolu atlandi: {type(e).__name__}: {e}")
jev_istek = jev_denetim.son_istek(req) if jev_etkin else ""
def jev_ac(ad):
katman_req = json.loads(data)
@@ -1883,9 +1949,12 @@ class H(http.server.BaseHTTPRequestHandler):
f"[{n} {slug}=>{ad}]")
def jev_devam(ad, onceki):
"""finishCheck: ayni hedefe, erken biten cevap ve not eklenmis istek. Yalniz AITURK gecidindeki hedefler."""
"""finishCheck: ayni hedefe, erken biten cevap ve not eklenmis istek. Yalniz AITURK gecidindeki hedefler.
Turda en fazla bir kez (bitis_kullan)."""
if ad == target and route_name != ide_route:
raise ValueError("hedef AITURK gecidinde degil")
if not bitis_kullan(bitis_turu):
raise ValueError("bu turda zaten geri verildi")
devam_req = json.loads(data)
devam_req["model"] = ad
devam_req.setdefault("input", []).extend([
@@ -1900,7 +1969,12 @@ class H(http.server.BaseHTTPRequestHandler):
return (yanit, SseRewriter(custom, hedef=ad, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren, tool_names=tool_names),
f"[{n} {slug}=>{ad} devam]")
try:
bitis_turu = tur_bilgisi(req, sohbet)[1]
jev_bitis = jev_devam if jev_etkin and jev_settings.read()[0]["finishCheck"]["enabled"] else None
if jev_bitis is not None:
with KARAR_KILIDI:
if bitis_turu in BITIS_TURLARI:
jev_bitis = None # bu turda bir kez geri verildi
except Exception:
jev_bitis = None
# A 9Router combo (Fusion-*, no provider prefix) runs a whole panel upstream. Once it may
@@ -1994,13 +2068,24 @@ class WatchResponse:
def __init__(self, response, accept):
self.response, self.filter = response, MetadataFilter(accept)
self.done = False
self.gecis = False # suzgec tasti (cok buyuk cerceve): kalan akis oldugu gibi gecer
def __getattr__(self, name):
return getattr(self.response, name)
def read1(self, size):
if self.gecis:
return self.response.read1(size)
while not self.done:
raw = self.response.read1(size)
if raw:
data = self.filter.feed(raw)
try:
data = self.filter.feed(raw)
except ValueError:
# 256 KB'i asan tek cerceve: akisi dusurmek yerine suzgecsiz devam (yan kanal cercevesi
# bu boyda olmaz). Tamponda yan kanal varsa disari cikmaz.
data, self.filter.frames.buf = self.filter.frames.buf, b''
self.gecis = True
log("izleme suzgeci: cok buyuk SSE cercevesi, kalan akis suzgecsiz geciyor")
return b'' if b'bekci.watch' in data else data
if data: return data
else:
self.done = True