Measured with the unmodified Codex binary on 10 Oct 2026: - real context windows per model (NanoGPT list, local table, combo = its smallest member, capped at the summarizer's window) so Codex compacts by itself when switching to a smaller model; an overflow guard trims oversized old parts and reports what still does not fit - classic tools instead of code mode for external models: GLM 0/3 -> 3/3, Qwen 0/3 -> 2/3 on simple tool tasks, Claude/cx/Gemini unchanged - phase (commentary / final_answer) on external assistant messages - an answer cut by the client is carried to the next turn once - images stay for vision-capable targets - summary brake stops tight loops, not frequent compaction - permanent gateway errors and tool-less targets fail at once with a readable message instead of five reconnects - a lone "." (used to apply a model change in voice) is answered locally - the bridge listens within seconds; the IDE waits 45 s for it Release: AITURK IDE 1.0.0-beta.26. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
314 lines
15 KiB
Python
314 lines
15 KiB
Python
"""Sikistirma (ozet) bagdastiricisi (6 Ekim 2026, kart a-grubu-bakim-20261006-0417).
|
|
|
|
Codex 0.160.0 "remote compaction v2" kullanir: girdinin sonuna {"type": "compaction_trigger"} koyar ve yanitta tam bir
|
|
{"type": "compaction", "encrypted_content": ...} ogesi bekler. Gercek olcum (raporlar/sesli-kopru-kanit-20261006/sik1, sik2):
|
|
* 9Router modelleri bu ogeyi uretemez -> "remote compaction v2 expected exactly one compaction output item, got 0",
|
|
tur hata verir (kucuk modele geciste ve 9Router modelinde esik asilinca).
|
|
* OpenAI'nin urettigi sifreli ozet 9Router modelince okunamaz -> kararlar/gerekceler kaybolur.
|
|
|
|
Bu modul:
|
|
1. 9Router'a giden sikistirma isteginde ozeti duz metin olarak ayni (ya da bridge.json ozet_kopru.model ile secilen)
|
|
modele yazdirir ve Codex'in bekledigi "compaction" ogesine sarar (encrypted_content = ISARET + base64(metin)).
|
|
2. Koprunun urettigi ozet ogelerini her giden istekte duz metin kullanici mesajina acar (hangi saglayiciya giderse).
|
|
3. OpenAI'nin sifreli ozeti 9Router modeline gidecekse, OpenAI'ye bir kez "bu ozeti duz metne dok" diye sorar,
|
|
sonucu diske onbellekler ve onu gonderir; olmazsa durust bir not koyar (sessiz kayip yok).
|
|
OpenAI -> OpenAI sikistirmasi oldugu gibi (yerlesik) kalir.
|
|
Kapatmak: bridge.json "ozet_kopru": {"enabled": false}
|
|
"""
|
|
import base64, hashlib, json, os, re
|
|
|
|
ISARET = "ASTRABRIDGE-OZET-1:"
|
|
OZET_BASLIK = "<onceki_konusma_ozeti>"
|
|
OZET_BITIS = "</onceki_konusma_ozeti>"
|
|
|
|
OZET_ISTEMI = """BAGLAM KONTROL NOKTASI (bu mesaj kullanicidan degil, kopru altyapisindan): Bu konusma baglam sinirina yaklastigi icin ozetlenip baska bir modele devredilecek. Simdi arac CAGIRMA; yalniz asagidaki ozeti yaz. Ozeti okuyacak model bu konusmayi goremeyecek, yalniz senin yazdigini gorecek; bu yuzden kisa tutmak icin bilgi atma. Bu ozet istegini kullanicinin istegi gibi ozetleme.
|
|
|
|
Kurallar:
|
|
- Kullanicinin ve konusmanin kullandigi etiket, kimlik ve adlari (ornek: "KARAR-1", kart/is numaralari, dosya adlari, model adlari) HARFI HARFINE koru ve her bilgiyi kendi etiketiyle yaz.
|
|
- Sayilari, tarihleri, yollari, komutlari ve onemli ozgun cumleleri degistirmeden aktar.
|
|
- Uydurma; emin olmadigin seyi "dogrulanmadi" diye isaretle. Sifre, anahtar ve gizli degerleri yazma.
|
|
|
|
Su basliklarla, madde madde ve somut yaz:
|
|
1. KULLANICININ ISTEGI VE HEDEF: kullanicinin kendi sozleriyle ne istedigi, kabul olcutleri, kisitlar.
|
|
2. KARARLAR VE GEREKCELERI: verilen her karar ve neden verildigi (etiketleriyle); reddedilen secenekler ve nedeni.
|
|
3. YAPILANLAR VE ONEMLI SONUCLAR: degisen dosyalar, calistirilan komutlar ve gercek ciktilari, test/olcum sonuclari, dogrulanan ve dogrulanamayan seyler.
|
|
4. ACIK ISLER VE SIRADAKI ADIM: bitmemis isler, bekleyen sorular/onaylar, bir sonraki somut adim.
|
|
5. KULLANICI TERCIHLERI VE UYARILAR: kalici talimatlar, yapilmamasi istenenler, bilinen tuzaklar ve hatalar.
|
|
6. GEREKLI BASVURULAR: yollar, adresler, kart/is numaralari, model ve ayar adlari."""
|
|
|
|
COZ_ISTEMI = ("Yukaridaki sikistirilmis konusma ozetinin icerigini, bu konusmayi hic gormemis baska bir modelin isi "
|
|
"surdurebilmesi icin eksiksiz ve ayrintili DUZ METIN olarak yaz: kullanicinin istegi, kararlar ve gerekceleri, "
|
|
"yapilanlar ve onemli sonuclar, acik isler ve siradaki adim, kullanici tercihleri, gerekli yollar/kimlikler. "
|
|
"Arac cagirma, yorum ekleme, uydurma. Sifre ve anahtar yazma.")
|
|
|
|
OKUNAMADI_NOTU = ("(Kopru notu: Bu noktadan onceki konusma, onceki modelin sifreli ozetinde duruyor ve bu modele "
|
|
"aktarilamadi. Onceki kararlari bilmiyorsan uydurma; kullaniciya sor.)")
|
|
|
|
|
|
KISA_MESAJI = ("AstraBridge: Sohbet ozeti cok kisa cikti (%d karakter, en az %d bekleniyordu) ve gecmisin yerine "
|
|
"konmadi; mesajlariniz sohbette duruyor. Tekrar deneyin ya da daha guclu bir model secin.")
|
|
|
|
|
|
def en_az_karakter(girdi_bayt, ayar=None):
|
|
"""Bu buyuklukteki bir konusmanin ozeti en az kac karakter olmali.
|
|
Yalniz bozuk/bos sayilacak kadar kisa ozeti yakalar; ozetin yeterliligini boyuttan cikarmaz.
|
|
Olcum (10 Ekim 2026, 646.955 baytlik gunluk + bes olgu): guclu modelin dogru ozeti 1500-1700
|
|
karakterdi; boyuta oranli yuksek esik (2587) bu dogru ozeti reddedip sohbeti kilitledi.
|
|
Hafiza kaybinin asil onlemi ozeti guclu modele yazdirmaktir (bridge.py _ozet_openai)."""
|
|
ayar = ayar or {}
|
|
tavan = int(ayar.get("en_az_tavan", 300))
|
|
oran = max(1, int(ayar.get("en_az_oran", 2000))) # her 2000 bayt girdi icin 1 karakter
|
|
return max(0, min(tavan, int(girdi_bayt) // oran))
|
|
|
|
|
|
def tetik_mi(req):
|
|
"""Istek bir sikistirma istegi mi (girdinin son ogesi compaction_trigger)?"""
|
|
ogeler = req.get("input")
|
|
return (isinstance(ogeler, list) and bool(ogeler) and isinstance(ogeler[-1], dict)
|
|
and ogeler[-1].get("type") == "compaction_trigger")
|
|
|
|
|
|
def ozet_ogesi(metin, kimlik_tohumu=""):
|
|
"""Duz metin ozeti Codex'in bekledigi compaction ogesine sarar."""
|
|
kimlik = "cmp_astrabridge_" + hashlib.sha256((kimlik_tohumu + metin).encode("utf-8")).hexdigest()[:32]
|
|
return {"type": "compaction", "id": kimlik,
|
|
"encrypted_content": ISARET + base64.b64encode(metin.encode("utf-8")).decode("ascii")}
|
|
|
|
|
|
def ozet_metni(oge):
|
|
"""Koprunun urettigi compaction ogesinden duz metni cikarir; kopru ogesi degilse None."""
|
|
icerik = oge.get("encrypted_content") if isinstance(oge, dict) else None
|
|
if not isinstance(icerik, str) or not icerik.startswith(ISARET):
|
|
return None
|
|
try:
|
|
return base64.b64decode(icerik[len(ISARET):]).decode("utf-8")
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _ozet_mesaji(metin):
|
|
return {"type": "message", "role": "user",
|
|
"content": [{"type": "input_text", "text": OZET_BASLIK + "\n" + metin.strip() + "\n" + OZET_BITIS}]}
|
|
|
|
|
|
def ozetleri_ac(req, yabanci_hedef=False, coz=None):
|
|
"""Giden istekteki compaction ogelerini hedefin okuyabilecegi bicime getirir (yerinde).
|
|
- Koprunun urettigi ozet -> duz metin kullanici mesaji (her hedefte).
|
|
- Baska saglayicinin (OpenAI) sifreli ozeti: hedef OpenAI ise dokunulmaz; hedef yabanciysa (9Router) coz(oge)
|
|
ile duz metne cevrilir, olmazsa OKUNAMADI_NOTU konur.
|
|
Doner: {'acilan': n, 'cozulen': n, 'okunamayan': n}"""
|
|
bilgi = {"acilan": 0, "cozulen": 0, "okunamayan": 0}
|
|
ogeler = req.get("input")
|
|
if not isinstance(ogeler, list):
|
|
return bilgi
|
|
yeni = []
|
|
for oge in ogeler:
|
|
if isinstance(oge, dict) and oge.get("type") == "compaction":
|
|
metin = ozet_metni(oge)
|
|
if metin is not None:
|
|
yeni.append(_ozet_mesaji(metin)); bilgi["acilan"] += 1
|
|
continue
|
|
if yabanci_hedef:
|
|
cozulmus = None
|
|
if coz is not None:
|
|
try:
|
|
cozulmus = coz(oge)
|
|
except Exception:
|
|
cozulmus = None
|
|
if cozulmus:
|
|
yeni.append(_ozet_mesaji(cozulmus)); bilgi["cozulen"] += 1
|
|
else:
|
|
yeni.append({"type": "message", "role": "user",
|
|
"content": [{"type": "input_text", "text": OKUNAMADI_NOTU}]}); bilgi["okunamayan"] += 1
|
|
continue
|
|
yeni.append(oge)
|
|
if bilgi["acilan"] or bilgi["cozulen"] or bilgi["okunamayan"]:
|
|
req["input"] = yeni
|
|
return bilgi
|
|
|
|
|
|
def ozet_istegi_hazirla(req, istem=None):
|
|
"""Sikistirma istegini (sonunda compaction_trigger) duz metin ozet istegine cevirir (yerinde)."""
|
|
ogeler = [o for o in req.get("input") or [] if not (isinstance(o, dict) and o.get("type") == "compaction_trigger")]
|
|
ogeler.append({"type": "message", "role": "user", "content": [{"type": "input_text", "text": istem or OZET_ISTEMI}]})
|
|
req["input"] = ogeler
|
|
req["tool_choice"] = "none"
|
|
req["stream"] = True
|
|
return req
|
|
|
|
|
|
def _olay(d):
|
|
return b"event: " + d["type"].encode() + b"\ndata: " + json.dumps(d, ensure_ascii=False).encode("utf-8") + b"\n\n"
|
|
|
|
|
|
class OzetAkisi:
|
|
"""Ozet isteginin SSE yanitini Codex'in bekledigi tek 'compaction' ogesine cevirir.
|
|
ic: once calisan cevirici (SseRewriter). Metin gelmezse compaction ogesi uretmez (Codex hatayi gosterir)."""
|
|
|
|
def __init__(self, ic=None, tohum="", en_az=0):
|
|
self.ic = ic
|
|
self.tohum = tohum
|
|
self.en_az = en_az # bundan kisa ozet kabul edilmez (bkz. en_az_karakter)
|
|
self.kisa = False
|
|
self.buf = b""
|
|
self.parcalar = {} # item_id -> delta metni
|
|
self.bitenler = [] # tamamlanan mesaj metinleri
|
|
self.metin = None # sonuc (gunluk/test icin)
|
|
self.bitti = False
|
|
|
|
def _topla(self):
|
|
metin = "\n\n".join(t for t in self.bitenler if t.strip())
|
|
if not metin.strip():
|
|
metin = "\n\n".join(t for t in self.parcalar.values() if t.strip())
|
|
return metin.strip()
|
|
|
|
def _blok(self, blok):
|
|
veri = [s[5:].strip() for s in re.split(rb"\r?\n", blok) if s.startswith(b"data:")]
|
|
if not veri:
|
|
return b""
|
|
try:
|
|
d = json.loads(b"\n".join(veri))
|
|
except Exception:
|
|
return b""
|
|
if not isinstance(d, dict):
|
|
return b""
|
|
t = d.get("type", "")
|
|
if t == "response.created":
|
|
return _olay(d)
|
|
if t == "response.output_text.delta":
|
|
self.parcalar[d.get("item_id")] = self.parcalar.get(d.get("item_id"), "") + (d.get("delta") or "")
|
|
return b": ozet\n\n" # baglantiyi canli tutar; Codex yorum satirini yok sayar
|
|
if t == "response.output_item.done":
|
|
oge = d.get("item") or {}
|
|
if oge.get("type") == "message":
|
|
self.bitenler.append("".join(p.get("text", "") for p in oge.get("content") or []
|
|
if isinstance(p, dict) and p.get("type") == "output_text"))
|
|
return b": ozet\n\n"
|
|
if t == "response.completed":
|
|
self.metin = self._topla()
|
|
self.bitti = True
|
|
if not self.metin:
|
|
return _olay(d) # ozet yok: Codex "0 compaction ogesi" hatasini gosterir
|
|
if len(self.metin) < self.en_az:
|
|
# Anormal kisa ozet gecmisin yerine konmaz: Codex hatayi gosterir, sohbet oldugu gibi kalir.
|
|
self.kisa = True
|
|
yanit = dict(d.get("response") or {})
|
|
yanit["status"] = "failed"; yanit["output"] = []
|
|
yanit["error"] = {"code": "astrabridge_ozet_kisa", "message": KISA_MESAJI % (len(self.metin), self.en_az)}
|
|
return _olay({"type": "response.failed", "response": yanit})
|
|
oge = ozet_ogesi(self.metin, self.tohum)
|
|
yanit = dict(d.get("response") or {})
|
|
yanit["output"] = [oge]
|
|
return (_olay({"type": "response.output_item.done", "output_index": 0, "item": oge})
|
|
+ _olay({"type": "response.completed", "response": yanit}))
|
|
if t in ("response.failed", "response.incomplete", "error"):
|
|
return _olay(d)
|
|
return b""
|
|
|
|
def sonuc_notu(self):
|
|
"""Gunluk satiri icin sonuc (icerik yazilmaz)."""
|
|
if not self.metin:
|
|
return "ozet URETILEMEDI"
|
|
if self.kisa:
|
|
return f"ozet REDDEDILDI: {len(self.metin)} karakter, en az {self.en_az} bekleniyordu; gecmis korunuyor"
|
|
return f"ozet uretildi, {len(self.metin)} karakter"
|
|
|
|
def feed(self, chunk):
|
|
if self.ic:
|
|
chunk = self.ic.feed(chunk)
|
|
self.buf += chunk
|
|
out = b""
|
|
while True:
|
|
m = re.search(rb"\r?\n\r?\n", self.buf)
|
|
if not m:
|
|
break
|
|
blok, self.buf = self.buf[:m.start()], self.buf[m.end():]
|
|
out += self._blok(blok)
|
|
return out
|
|
|
|
def flush(self):
|
|
if self.ic:
|
|
self.buf += self.ic.flush()
|
|
self.buf = b""
|
|
return b""
|
|
|
|
|
|
# ---- OpenAI'nin sifreli ozetini duz metne cevirme (onbellekli) ----
|
|
def onbellek_yolu(klasor, oge):
|
|
kimlik = str(oge.get("id") or hashlib.sha256(str(oge.get("encrypted_content")).encode()).hexdigest()[:32])
|
|
return os.path.join(klasor, re.sub(r"[^A-Za-z0-9_.-]", "_", kimlik)[:80] + ".txt")
|
|
|
|
|
|
def onbellekten(klasor, oge):
|
|
try:
|
|
with open(onbellek_yolu(klasor, oge), encoding="utf-8") as f:
|
|
return f.read().strip() or None
|
|
except OSError:
|
|
return None
|
|
|
|
|
|
def onbellege(klasor, oge, metin):
|
|
os.makedirs(klasor, exist_ok=True)
|
|
yol = onbellek_yolu(klasor, oge)
|
|
with open(yol + ".tmp", "w", encoding="utf-8") as f:
|
|
f.write(metin)
|
|
os.replace(yol + ".tmp", yol)
|
|
|
|
|
|
def coz_istegi(oge, model, talimat="Sen bir konusma ozeti aktaricisisin."):
|
|
"""Sifreli ozeti duz metne dokturmek icin OpenAI'ye gidecek istek govdesi."""
|
|
temiz = {k: v for k, v in oge.items() if k in ("type", "id", "encrypted_content")}
|
|
return {"model": model, "instructions": talimat, "store": False, "stream": True, "tools": [],
|
|
"tool_choice": "none", "parallel_tool_calls": False,
|
|
"reasoning": {"effort": "low", "context": "all_turns"}, # chatgpt.com arka ucu bunu sart kosuyor
|
|
"include": ["reasoning.encrypted_content"],
|
|
"input": [temiz, {"type": "message", "role": "user", "content": [{"type": "input_text", "text": COZ_ISTEMI}]}]}
|
|
|
|
|
|
def sse_metni(govde):
|
|
"""SSE govdesinden (bayt) asistan metnini toplar."""
|
|
a = OzetAkisi()
|
|
a.feed(govde if govde.endswith(b"\n\n") else govde + b"\n\n")
|
|
return a.metin if a.bitti else a._topla()
|
|
|
|
|
|
# ---- Dongu freni ----
|
|
# Ozetten sonra da baglam hedefin esigini asiyorsa (Codex son kullanici mesajlarini aynen tuttugu icin olabilir) Codex
|
|
# ayni turda tekrar tekrar sikistirma ister; her biri basarili oldugu icin dongu kendiliginden bitmez (olcum: sik6).
|
|
DONGU_MESAJI = ("AstraBridge: Bu sohbetin baglami, ozetlendikten sonra da secili modelin sinirina sigmiyor "
|
|
"(kisa surede %d ozet denendi). Dongu durduruldu. Daha buyuk baglam pencereli bir model secin ya da "
|
|
"yeni sohbet acin; mesajlariniz sohbette duruyor.")
|
|
|
|
|
|
class DonguFreni:
|
|
"""Ozet dongusunu durdurur, mesru sik sikistirmayi durdurmaz (10 Ekim 2026: kucuk esikli sohbette
|
|
10 dakikadaki 4. ozet reddedilip tur dusuyordu). Iki ayri sinir:
|
|
- art_arda: arada normal (ozet olmayan) istek gecmeden ust uste gelen ozet sayisi; gercek dongu budur.
|
|
- sinir: pencere_sn icindeki toplam ozet sayisi; her ozetin arasina normal istek giren yavas donguyu keser."""
|
|
|
|
def __init__(self, sinir=8, pencere_sn=600, art_arda=3):
|
|
self.sinir = sinir
|
|
self.pencere = pencere_sn
|
|
self.art_arda = art_arda
|
|
self.kayit = {}
|
|
self.ust_uste = {}
|
|
|
|
def normal(self, anahtar):
|
|
"""Bu sohbette ozet olmayan bir istek gecti: art arda sayaci sifirlanir."""
|
|
self.ust_uste.pop(anahtar, None)
|
|
|
|
def izin(self, anahtar, simdi):
|
|
"""Bu ozet istegine izin var mi? Varsa kaydeder."""
|
|
zamanlar = [t for t in self.kayit.get(anahtar, []) if simdi - t < self.pencere]
|
|
if len(zamanlar) >= self.sinir or self.ust_uste.get(anahtar, 0) >= self.art_arda:
|
|
self.kayit[anahtar] = zamanlar
|
|
return False
|
|
zamanlar.append(simdi)
|
|
self.kayit[anahtar] = zamanlar
|
|
self.ust_uste[anahtar] = self.ust_uste.get(anahtar, 0) + 1
|
|
if len(self.kayit) > 500: # bellek sismesin
|
|
for k in sorted(self.kayit, key=lambda k: self.kayit[k][-1])[:250]:
|
|
self.kayit.pop(k, None)
|
|
self.ust_uste.pop(k, None)
|
|
return True
|