From 91ecad8e5c9e3c7b4c9856c07e04f36e81e1fd54 Mon Sep 17 00:00:00 2001
From: yilsem
Date: Tue, 13 Oct 2026 03:36:42 +0300
Subject: [PATCH] Preserve native Codex history and add configurable
cross-model compression
Separate encrypted reasoning from actual compaction, stop migration after native return, and retain full history and recent exchanges. Add typed settings, Gemini fallback, verified provider windows, bounded summaries and durable concurrent checkpoints.
Validation: 245 bridge tests plus focused concurrency tests; 71 settings/connection tests; 66 supplementary tests; TypeScript checks; renderer and Electron bundles; real primary/native/fallback summary probes and 700K input acceptance.
---
.gitea/workflows/ci.yml | 2 +-
README.AITURK.md | 16 ++
apps/desktop/assets/codex-bridge/bridge.py | 36 ++-
.../codex-bridge/compression-defaults.json | 15 ++
.../assets/codex-bridge/compression_engine.py | 227 ++++++++++++++++++
.../codex-bridge/compression_settings.py | 125 ++++++++++
.../assets/codex-bridge/gecis_context.py | 73 +++++-
apps/desktop/assets/codex-bridge/kopru.py | 74 +-----
.../desktop/assets/codex-bridge/ozet_kopru.py | 10 +
apps/desktop/assets/codex-bridge/pencere.py | 16 +-
.../codex-bridge/test_compression_settings.py | 170 +++++++++++++
.../assets/codex-bridge/test_gecis_context.py | 141 ++++++++++-
.../assets/codex-bridge/test_pencere_phase.py | 9 +-
.../electron/compression-settings-types.ts | 21 ++
.../electron/compression-settings.test.ts | 31 +++
apps/desktop/electron/compression-settings.ts | 48 ++++
apps/desktop/electron/main.ts | 26 ++
apps/desktop/electron/mobile-live.test.ts | 5 +-
apps/desktop/electron/preload.ts | 2 +
apps/desktop/package.json | 6 +-
.../src/app/settings/codex-link-settings.tsx | 2 +
.../settings/compression-settings.test.tsx | 37 +++
.../src/app/settings/compression-settings.tsx | 79 ++++++
.../src/app/starmap/share-controls.test.tsx | 2 +-
apps/desktop/src/global.d.ts | 3 +
apps/desktop/tsconfig.electron.json | 1 +
package-lock.json | 59 +++--
27 files changed, 1119 insertions(+), 117 deletions(-)
create mode 100644 apps/desktop/assets/codex-bridge/compression-defaults.json
create mode 100644 apps/desktop/assets/codex-bridge/compression_engine.py
create mode 100644 apps/desktop/assets/codex-bridge/compression_settings.py
create mode 100644 apps/desktop/assets/codex-bridge/test_compression_settings.py
create mode 100644 apps/desktop/electron/compression-settings-types.ts
create mode 100644 apps/desktop/electron/compression-settings.test.ts
create mode 100644 apps/desktop/electron/compression-settings.ts
create mode 100644 apps/desktop/src/app/settings/compression-settings.test.tsx
create mode 100644 apps/desktop/src/app/settings/compression-settings.tsx
diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
index 27a521f..299d2b0 100644
--- a/.gitea/workflows/ci.yml
+++ b/.gitea/workflows/ci.yml
@@ -66,7 +66,7 @@ jobs:
- name: Native dependency staging regression tests
run: npx vitest run --root apps/desktop scripts/stage-native-deps.test.mjs --maxWorkers=2
- name: Codex connection settings regression tests
- run: npx vitest run --root apps/desktop electron/codex-link.test.ts electron/codex-link-config.test.ts electron/staged-transition.test.ts src/app/settings/codex-link-settings.test.tsx src/app/settings/staged-transition-settings.test.tsx --maxWorkers=2
+ run: npx vitest run --root apps/desktop electron/codex-link.test.ts electron/codex-link-config.test.ts electron/staged-transition.test.ts electron/compression-settings.test.ts src/app/settings/codex-link-settings.test.tsx src/app/settings/staged-transition-settings.test.tsx src/app/settings/compression-settings.test.tsx --maxWorkers=2
- name: Build and package Windows x64
run: |
set -eu
diff --git a/README.AITURK.md b/README.AITURK.md
index 381be75..070c470 100644
--- a/README.AITURK.md
+++ b/README.AITURK.md
@@ -18,6 +18,22 @@ TurkServis'in Hermes Agent tabanlı masaüstü geliştirme ortamı.
## Veri ve güncellemeler
+### Codex bağlam ve sıkıştırma
+
+Codex Bağlama ayarları, ek model içeren geçişlerde sıkıştırıcı/yedek model,
+ayrıntı seviyesi, korunacak mesajlaşmalar, yüzde/token bütçesi ve otomatik eşik
+seçimini sunar. Varsayılanlar: Gemini 3.8 Flash High, yedek Gemini 3 Flash,
+10 mesajlaşma, geçişte %70, otomatik tetiklemede %70 ve sonrasında %50.
+Kapasite bağlantıdan gelen model bilgisiyle hesaplanır; 272K genel tavan yoktur.
+Girdi hesabı talimatlar ve araçları da içeren temkinli bir token tahminidir.
+
+Abonelik modellerinin kendi aralarındaki istekler ve GPT-Live ses yolu özgün
+Codex hattında kalır. Şifreli reasoning yalnız ek model için hazırlanan kopyada
+görünür metnine indirgenir; gerçek şifreli konuşma özeti sahibi tarafından açılır.
+Sohbet kimliği ve özgün konuşma kaydı değiştirilmez. Doğrulanmış özet sürümleri
+köprü veri dizinindeki `compression-checkpoints.sqlite3` içinde saklanır.
+Son mesajlaşmalar bütçeye sığmıyorsa sayı kendiliğinden azaltılmaz.
+
Windows ajan verisi `%LOCALAPPDATA%/TurkServis/AITURK-IDE/agent` altında tutulur.
`AITURK_IDE_HOME` açıkça başka bir konum seçmek için kullanılabilir.
Mevcut Hermes kurulumu ve `HERMES_HOME` dizini otomatik olarak devralınmaz.
diff --git a/apps/desktop/assets/codex-bridge/bridge.py b/apps/desktop/assets/codex-bridge/bridge.py
index 394cc19..6a21d99 100644
--- a/apps/desktop/assets/codex-bridge/bridge.py
+++ b/apps/desktop/assets/codex-bridge/bridge.py
@@ -356,6 +356,8 @@ class H(http.server.BaseHTTPRequestHandler):
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
toplam = 0
+ event_tail = b""
+ completed = False
try:
while True:
try:
@@ -368,6 +370,16 @@ class H(http.server.BaseHTTPRequestHandler):
if not parca:
break
toplam += len(parca)
+ if getattr(self, "_native_return_scope", None):
+ event_tail += parca
+ while b"\n" in event_tail:
+ line, event_tail = event_tail.split(b"\n", 1)
+ if line.startswith(b"data:"):
+ try:
+ event = json.loads(line[5:])
+ completed = completed or event.get("type") == "response.completed"
+ except (ValueError, AttributeError):
+ pass
self.wfile.write(b"%x\r\n" % len(parca) + parca + b"\r\n")
self.wfile.flush()
self.wfile.write(b"0\r\n\r\n")
@@ -378,6 +390,8 @@ class H(http.server.BaseHTTPRequestHandler):
return True
if not sessiz:
log(f"{tag} bitti status={durum} {toplam} bayt")
+ if completed and getattr(self, "_native_return_scope", None):
+ remember_transfer(*self._native_return_scope)
return True
def _transfer_begin(self):
@@ -438,6 +452,7 @@ class H(http.server.BaseHTTPRequestHandler):
return result["value"], started
def _isle(self):
+ self._native_return_scope = None
with kilit:
sira[0] += 1
n = sira[0]
@@ -467,9 +482,9 @@ class H(http.server.BaseHTTPRequestHandler):
thread = str(req.get("prompt_cache_key") or "")
scope = hashlib.sha256((str(self.headers.get("chatgpt-account-id") or "") + "|" + thread).encode()).hexdigest()
source = TRANSFERS.get(scope) if thread else None
- mixed = bool(source) or gecis_context.foreign(req)
- if external and thread:
- remember_transfer(scope, model)
+ # A completed return to the subscription ends migration. Historical
+ # foreign IDs must not keep later native turns in the preparer.
+ mixed = ek_model_mu(source or "") or (source is None and gecis_context.foreign(req))
if external or mixed:
try:
normalized = gecis_context.normalize(req)
@@ -481,13 +496,14 @@ class H(http.server.BaseHTTPRequestHandler):
prepared, started = self._prepare(normalized, source)
if prepared is None:
return
- if "compaction" in prepared:
+ if "compaction" in prepared or "compaction_items" in prepared:
if not started:
self._transfer_begin()
- item = prepared["compaction"]
- for event in ({"type": "response.output_item.done", "output_index": 0, "item": item},
- {"type": "response.completed", "response": {"id": "resp_aiturk_compact", "object": "response",
- "status": "completed", "output": [item]}}):
+ items = prepared.get("compaction_items") or [prepared["compaction"]]
+ events = [{"type": "response.output_item.done", "output_index": index, "item": item} for index, item in enumerate(items)]
+ events.append({"type": "response.completed", "response": {"id": "resp_aiturk_compact", "object": "response",
+ "status": "completed", "output": items}})
+ for event in events:
self._transfer_chunk(b"event: " + event["type"].encode() + b"\ndata: " + json.dumps(event, ensure_ascii=False).encode() + b"\n\n")
self.wfile.write(b"0\r\n\r\n")
self.wfile.flush()
@@ -497,8 +513,12 @@ class H(http.server.BaseHTTPRequestHandler):
started = False
changed = normalized != req
body = json.dumps(normalized, ensure_ascii=False).encode() if changed else govde
+ if external and thread:
+ remember_transfer(scope, model)
+ self._native_return_scope = (scope, model) if thread and not external else None
return self._ilet(n, KOPRU if external else CHATGPT, yol, body,
f"{'KOPRU' if external else 'OZGUN'} {model}", rewritten=changed, started=started)
+ self._native_return_scope = (scope, model) if thread else None
return self._ilet(n, CHATGPT, yol, govde, f"OZGUN {model or '?'}")
if self.command == "GET" and "/models" in yol:
# Ek modeller listede gorunsun diye liste koprüden istenir; kopru cevap vermezse chatgpt.com'un listesi.
diff --git a/apps/desktop/assets/codex-bridge/compression-defaults.json b/apps/desktop/assets/codex-bridge/compression-defaults.json
new file mode 100644
index 0000000..758dc40
--- /dev/null
+++ b/apps/desktop/assets/codex-bridge/compression-defaults.json
@@ -0,0 +1,15 @@
+{
+ "schemaVersion": 1,
+ "primaryModel": "ide/agy-gemini-3.8-flash-high",
+ "fallbackModel": "ide/agy-gemini-3-flash",
+ "currentModelFallback": true,
+ "level": "orta",
+ "keepTurns": 10,
+ "budgetMode": "percent",
+ "transferPercent": 70,
+ "maxTokens": 64000,
+ "autoEnabled": true,
+ "triggerPercent": 70,
+ "targetPercent": 50,
+ "timeoutSeconds": 3600
+}
diff --git a/apps/desktop/assets/codex-bridge/compression_engine.py b/apps/desktop/assets/codex-bridge/compression_engine.py
new file mode 100644
index 0000000..cb69cf6
--- /dev/null
+++ b/apps/desktop/assets/codex-bridge/compression_engine.py
@@ -0,0 +1,227 @@
+"""Tool-free summarization with validated completion, fallback and checkpoints."""
+import copy
+import json
+import time
+import urllib.request
+
+import compression_settings as settings_store
+import gecis_context as gc
+import ozet_kopru
+
+
+def text_fragments(item, budget):
+ """Split text without dropping metadata or non-text content blocks."""
+ if isinstance(item, str):
+ text, wrap = item, lambda value: value
+ elif isinstance(item, dict) and not item.get('call_id'):
+ content = item.get('content')
+ if isinstance(content, list):
+ for part in content:
+ if isinstance(part, dict) and isinstance(part.get('text'), str):
+ wrapper = dict(item, content=[dict(part, text='')])
+ size = max(1, (budget - gc.tokens([wrapper]) - 128) // 2)
+ for start in range(0, max(1, len(part['text'])), size):
+ fragment = dict(item, content=[dict(part, text=part['text'][start:start+size])])
+ if gc.tokens([fragment]) > budget:
+ raise gc.TransferError('Mesaj parçası sıkıştırıcıya sığmadı')
+ yield fragment
+ else:
+ fragment = dict(item, content=[part])
+ if gc.tokens([fragment]) > budget:
+ raise gc.TransferError('Metin dışı içerik sıkıştırıcı kapasitesine sığmıyor; geçmiş korundu.')
+ yield fragment
+ return
+ if not isinstance(content, str):
+ raise gc.TransferError('Bölünemeyen kayıt sıkıştırıcı kapasitesine sığmıyor; geçmiş korundu.')
+ text, wrap = content, lambda value: dict(item, content=value)
+ else:
+ raise gc.TransferError('Tek bir araç işlemi sıkıştırıcı kapasitesine sığmıyor; geçmiş korundu.')
+ # At most budget characters per piece is conservative for UTF-8 text.
+ size = max(1, (budget - gc.tokens([wrap('')]) - 128) // 2)
+ for start in range(0, max(1, len(text)), size):
+ fragment = wrap(text[start:start+size])
+ if gc.tokens([fragment]) > budget:
+ raise gc.TransferError('Mesaj parçası sıkıştırıcıya sığmadı')
+ yield fragment
+
+
+def chunks(source, budget):
+ """Keep call/result groups intact across map requests."""
+ try:
+ items = json.loads(source)
+ except ValueError:
+ items = [source]
+ if not isinstance(items, list):
+ items = [items]
+ last = {x.get('call_id'): i for i, x in enumerate(items) if isinstance(x, dict) and x.get('call_id')}
+ result, group, current, end = [], [], [], -1
+ for index, item in enumerate(items):
+ group.append(item)
+ if isinstance(item, dict) and item.get('call_id'):
+ end = max(end, last[item['call_id']])
+ if index < end:
+ continue
+ if gc.tokens(group) > budget:
+ # Text-only messages may be mapped in pieces. Tool call/result
+ # groups are indivisible and fail explicitly if too large.
+ if len(group) != 1 or (isinstance(item, dict) and item.get('call_id')):
+ raise gc.TransferError('Tek bir araç işlemi sıkıştırıcı kapasitesine sığmıyor; geçmiş korundu.')
+ if current:
+ result.append(gc.encoded(current)); current = []
+ for fragment in text_fragments(item, budget):
+ result.append(gc.encoded([fragment]))
+ group = []
+ continue
+ if current and gc.tokens(current + group) > budget:
+ result.append(gc.encoded(current))
+ current = []
+ current.extend(group)
+ group = []
+ if current:
+ result.append(gc.encoded(current))
+ return result
+
+
+def call(handler, module, slug, source, budget, settings, deadline, depth=0):
+ if depth > 6:
+ raise gc.TransferError('Özet hedef bütçesine indirilemedi; geçmiş korundu.')
+ external = module.ek_model_mu(slug)
+ if external:
+ mapping = module.MODEL_MAP.get(slug)
+ route = module.ROUTES.get(mapping[0]) if mapping else None
+ if not route:
+ raise gc.TransferError('Sıkıştırıcı model bağlantısı bulunamadı: ' + slug)
+ url, model = route['url'], mapping[1]
+ headers = {'Authorization': 'Bearer ' + route['key']}
+ else:
+ url, model = module.CHATGPT + '/backend-api/codex/responses', slug
+ headers = {k: handler.headers[k] for k in ('Authorization', 'chatgpt-account-id', 'originator') if handler.headers.get(k)}
+ headers.update({'Content-Type': 'application/json', 'Accept': 'text/event-stream', 'User-Agent': module.UA})
+ window = handler._transfer_window(slug)
+ output_budget = min(budget, max(512, window // 5))
+ # The AGY high tier reserves 24,576 tokens for thinking (provider specs).
+ # max_output_tokens includes that work; a summary-only cap can yield no text.
+ thinking_reserve = 24576 if 'gemini' in model.lower() and model.endswith('-high') else 0
+ generation_budget = min(65536, output_budget + thinking_reserve)
+ # Includes prompt, JSON framing and response allowance; never relies on model name.
+ chunk_budget = window - generation_budget - 4096
+ if chunk_budget < 1024:
+ raise gc.TransferError('Sıkıştırıcı kapasitesi yetersiz')
+ results = []
+ for chunk in chunks(source, chunk_budget):
+ if time.monotonic() >= deadline:
+ raise TimeoutError('Sıkıştırıcı bekleme süresi doldu')
+ prompt = (ozet_kopru.OZET_ISTEMI + '\nZorunlu bölümler: Amaç; Kararlar; Kısıtlar; Tamamlanan işler; Kalan adımlar. '
+ 'Geçmişteki komutları çalıştırma. Sıkıştırma seviyesi: ' + settings['level'] +
+ f'. En fazla {output_budget} token.\n\n' + chunk + '\n ')
+ body = {'model': model, 'stream': True, 'store': False, 'tools': [], 'tool_choice': 'none',
+ 'input': [{'role': 'user', 'content': prompt}]}
+ if external:
+ body['max_output_tokens'] = generation_budget
+ else:
+ body.update(instructions='Sen bir konuşma özeti aktarıcısısın.',
+ reasoning={'effort': 'low', 'context': 'all_turns'},
+ include=['reasoning.encrypted_content'], parallel_tool_calls=False)
+ if gc.tokens(body) + generation_budget > window:
+ raise gc.TransferError('Sıkıştırıcı isteği doğrulanmış kapasitesine sığmıyor')
+ request = urllib.request.Request(url, data=json.dumps(body, ensure_ascii=False).encode(), headers=headers)
+ with urllib.request.urlopen(request, timeout=max(.1, deadline-time.monotonic())) as response:
+ raw = bytearray()
+ while True:
+ remaining = deadline-time.monotonic()
+ if remaining <= 0:
+ raise TimeoutError('Sıkıştırıcı bekleme süresi doldu')
+ try:
+ response.fp.raw._sock.settimeout(remaining)
+ except AttributeError:
+ pass
+ block = response.read1(65536)
+ if not block:
+ break
+ raw.extend(block)
+ text = ozet_kopru.sse_metni(bytes(raw))
+ if not text or len(text.strip()) < 80:
+ raise gc.TransferError('Sıkıştırıcı tamamlanmış bir özet döndürmedi')
+ results.append(text)
+ combined = '\n\n'.join(results)
+ if not combined:
+ raise gc.TransferError('Özetlenecek geçmiş bulunamadı')
+ if gc.tokens(combined) > budget:
+ if len(combined) >= len(source):
+ raise gc.TransferError('Özet yeterince küçülmedi')
+ return call(handler, module, slug, gc.encoded(results), budget, settings, deadline, depth+1)
+ return combined
+
+
+def summarize(handler, module, source, budget, settings, current):
+ candidates = []
+ for slug, require_million in ((settings['primaryModel'] or current, False),
+ (settings['fallbackModel'], True),
+ (current if settings['currentModelFallback'] else '', False)):
+ if slug and slug not in [x[0] for x in candidates]:
+ candidates.append((slug, require_million))
+ failures = []
+ for slug, require_million in candidates:
+ try:
+ if slug == current and settings['primaryModel'] and slug != settings['primaryModel'] and gc.tokens(source) + budget + 4096 > handler._transfer_window(slug):
+ raise gc.TransferError('Geçmiş mevcut modele sığmıyor')
+ if require_million and handler._transfer_window(slug) < 1000000:
+ raise gc.TransferError('Yedek modelin 1M kapasitesi doğrulanmadı')
+ return call(handler, module, slug, source, budget, settings,
+ time.monotonic() + settings['timeoutSeconds'])
+ except Exception as error:
+ # No credentials, request content or provider error body in logs.
+ failures.append(slug + ': ' + type(error).__name__)
+ raise gc.TransferError('Sıkıştırıcılar yanıt veremedi; geçmiş korundu. ' + '; '.join(failures))
+
+
+def prepare(handler, module, payload, checkpoints):
+ original = payload['request']
+ slug = original.get('model', '')
+ external = module.ek_model_mu(slug)
+ source_model = payload.get('source_model') or ''
+ # Defense in depth: an accidental worker call cannot alter native-only history.
+ if not external and not module.ek_model_mu(source_model) and not gc.foreign(original):
+ return {'request': copy.deepcopy(original), 'window': handler._transfer_window(slug)}
+ settings = settings_store.read(module.HOME)
+ actual = (module.yonlendirilen(str(original.get('prompt_cache_key') or ''), slug) or slug) if external else slug
+ window = handler._transfer_window(actual)
+ req = gc.portable_reasoning(gc.normalize(original)) if external else gc.normalize(original)
+ compact = external and ozet_kopru.tetik_mi(req)
+ if compact:
+ req['input'] = [x for x in req.get('input', []) if not isinstance(x, dict) or x.get('type') != 'compaction_trigger']
+ opaque = any(isinstance(x, dict) and x.get('type') == 'compaction' for x in req.get('input') or [])
+ if opaque and (external or gc.tokens(req) > gc.input_budget(req, window, settings)):
+ stats = ozet_kopru.ozetleri_ac(req, yabanci_hedef=True,
+ coz=lambda item: handler._openai_ozet_coz(0, item, module.CFG.get('ozet_kopru') or {}))
+ if stats['okunamayan']:
+ raise gc.TransferError('Önceki şifreli özet okunamadı; geçmiş korundu.')
+ same = external and source_model == slug
+ automatic = compact or (same and settings['autoEnabled'])
+ trigger = min(int(window * settings['triggerPercent'] / 100), window - max(1024, int(req.get('max_output_tokens') or 0))) if automatic else gc.input_budget(req, window, settings)
+ scope = settings_store.digest([handler.headers.get('chatgpt-account-id', ''), req.get('prompt_cache_key')]) if req.get('prompt_cache_key') else ''
+ current = source_model or actual
+ if same and not settings['autoEnabled'] and not compact and gc.tokens(req) <= window - max(1024, int(req.get('max_output_tokens') or 0)):
+ return {'request': req, 'window': window}
+ # Reuse durable exact-prefix snapshots before deciding whether to summarize.
+ protected, old, recent = gc.split_history(req.get('input') or [], settings['keepTurns'])
+ checkpoints.begin(scope, old, settings)
+ saved = checkpoints.get(scope, old, settings) if old else None
+ 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:
+ return {'request': effective, 'window': window}
+ key = settings_store.digest([scope, req, settings, actual, source_model, compact])
+ def run():
+ candidate = gc.PREPARER.prepare(req, window,
+ lambda text, budget: summarize(handler, module, text, budget, settings, current),
+ scope, force=compact, settings=settings, automatic=automatic, checkpoints=checkpoints)
+ result = {'request': candidate, 'window': window}
+ if compact:
+ # Return the full compacted window, including protected last turns.
+ items = copy.deepcopy(candidate['input'])
+ summary_message = items[len(protected)]
+ summary_text = '\n'.join(x.get('text', '') for x in summary_message.get('content', []))
+ items[len(protected)] = ozet_kopru.ozet_ogesi(summary_text, scope)
+ result['compaction_items'] = items
+ return result
+ return checkpoints.coalesce(key, run)
diff --git a/apps/desktop/assets/codex-bridge/compression_settings.py b/apps/desktop/assets/codex-bridge/compression_settings.py
new file mode 100644
index 0000000..69b0aab
--- /dev/null
+++ b/apps/desktop/assets/codex-bridge/compression_settings.py
@@ -0,0 +1,125 @@
+"""Validated snapshots and durable, source-bound context projections.
+
+Only prepared copies live here. Codex owns the transcript and its Chat ID.
+"""
+import copy
+import hashlib
+import json
+import os
+import sqlite3
+import threading
+from pathlib import Path
+from contextlib import contextmanager
+
+DEFAULTS = json.loads(Path(__file__).with_name('compression-defaults.json').read_text(encoding='utf-8'))
+LEVELS = {'mini': .85, 'az': .65, 'orta': .45, 'fazla': .3, 'cok-fazla': .15}
+
+
+def validate(value):
+ if not isinstance(value, dict) or set(value) != set(DEFAULTS):
+ raise ValueError('Geçersiz sıkıştırma ayarları')
+ v = copy.deepcopy(value)
+ if v['schemaVersion'] != 1 or v['level'] not in LEVELS or v['budgetMode'] not in ('percent', 'tokens'):
+ raise ValueError('Geçersiz sıkıştırma sürümü veya seviyesi')
+ for field in ('primaryModel', 'fallbackModel'):
+ if not isinstance(v[field], str) or len(v[field]) > 256:
+ raise ValueError('Geçersiz sıkıştırıcı model')
+ for field in ('currentModelFallback', 'autoEnabled'):
+ if type(v[field]) is not bool:
+ raise ValueError('Geçersiz sıkıştırma seçeneği')
+ for field, low, high in [('keepTurns', 1, 100), ('transferPercent', 10, 90),
+ ('triggerPercent', 20, 90), ('targetPercent', 10, 80),
+ ('maxTokens', 1024, 2000000), ('timeoutSeconds', 1, 3600)]:
+ if type(v[field]) is not int or not low <= v[field] <= high:
+ raise ValueError('Geçersiz sıkıştırma sınırı: ' + field)
+ if v['targetPercent'] >= v['triggerPercent']:
+ raise ValueError('Sıkıştırma hedefi başlama eşiğinden küçük olmalı')
+ return v
+
+
+def read(home):
+ try:
+ return validate(json.loads((Path(home) / 'compression-settings.json').read_text(encoding='utf-8')))
+ except FileNotFoundError:
+ return copy.deepcopy(DEFAULTS)
+
+
+def digest(value):
+ return hashlib.sha256(json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
+
+
+class Checkpoints:
+ def __init__(self, home):
+ self.path = Path(home) / 'compression-checkpoints.sqlite3'
+ self.lock = threading.RLock()
+ self.inflight = {}
+
+ @contextmanager
+ def connect(self):
+ self.path.parent.mkdir(parents=True, exist_ok=True)
+ db = sqlite3.connect(self.path, timeout=30)
+ db.execute('CREATE TABLE IF NOT EXISTS versions (id INTEGER PRIMARY KEY, scope TEXT, source_hash TEXT, source_count INTEGER, settings_hash TEXT, summary TEXT, created TEXT DEFAULT CURRENT_TIMESTAMP)')
+ db.execute('CREATE INDEX IF NOT EXISTS versions_scope ON versions(scope, id)')
+ db.execute('CREATE TABLE IF NOT EXISTS active (scope TEXT PRIMARY KEY, version INTEGER)')
+ db.execute('CREATE TABLE IF NOT EXISTS latest (scope TEXT PRIMARY KEY, request_hash TEXT)')
+ try:
+ with db:
+ yield db
+ finally:
+ db.close()
+
+ def get(self, scope, items, settings):
+ if not scope:
+ return None
+ with self.lock, self.connect() as db:
+ row = db.execute('SELECT v.source_hash,v.source_count,v.summary FROM versions v JOIN active a ON a.version=v.id WHERE a.scope=? AND v.settings_hash=?', (scope, digest(settings))).fetchone()
+ if row and len(items) >= row[1] and digest(items[:row[1]]) == row[0]:
+ return row[1], row[2]
+ return None
+
+ def save(self, scope, items, settings, summary):
+ if not scope:
+ return
+ with self.lock, self.connect() as db:
+ cursor = db.execute('INSERT INTO versions(scope,source_hash,source_count,settings_hash,summary) VALUES(?,?,?,?,?)', (scope, digest(items), len(items), digest(settings), summary))
+ # A late summary of an earlier prefix cannot replace a newer one.
+ old = db.execute('SELECT v.source_count FROM versions v JOIN active a ON a.version=v.id WHERE a.scope=?', (scope,)).fetchone()
+ latest = db.execute('SELECT request_hash FROM latest WHERE scope=?', (scope,)).fetchone()
+ current = not latest or latest[0] == digest([items, settings])
+ if current and (not old or old[0] <= len(items)):
+ db.execute('INSERT OR REPLACE INTO active(scope,version) VALUES(?,?)', (scope, cursor.lastrowid))
+
+ def begin(self, scope, items, settings):
+ if scope:
+ with self.lock, self.connect() as db:
+ db.execute('INSERT OR REPLACE INTO latest(scope,request_hash) VALUES(?,?)',
+ (scope, digest([items, settings])))
+
+ def restore(self, scope, version):
+ with self.lock, self.connect() as db:
+ if not db.execute('SELECT id FROM versions WHERE id=? AND scope=?', (version, scope)).fetchone():
+ raise ValueError('Sıkıştırma sürümü bu sohbete ait değil')
+ db.execute('INSERT OR REPLACE INTO active(scope,version) VALUES(?,?)', (scope, version))
+
+ def coalesce(self, key, run):
+ with self.lock:
+ entry = self.inflight.get(key)
+ owner = entry is None
+ if owner:
+ entry = {'event': threading.Event()}
+ self.inflight[key] = entry
+ if not owner:
+ entry['event'].wait()
+ if 'error' in entry:
+ raise entry['error']
+ return copy.deepcopy(entry['result'])
+ try:
+ entry['result'] = run()
+ return copy.deepcopy(entry['result'])
+ except Exception as error:
+ entry['error'] = error
+ raise
+ finally:
+ with self.lock:
+ entry['event'].set()
+ self.inflight.pop(key, None)
diff --git a/apps/desktop/assets/codex-bridge/gecis_context.py b/apps/desktop/assets/codex-bridge/gecis_context.py
index b8d215f..c5eed2f 100644
--- a/apps/desktop/assets/codex-bridge/gecis_context.py
+++ b/apps/desktop/assets/codex-bridge/gecis_context.py
@@ -11,9 +11,10 @@ import threading
from collections import OrderedDict
import ozet_kopru
+import compression_settings
PREFIXES = {"custom_tool_call": "ctc", "custom_tool_call_output": "ctco",
- "function_call": "fc", "function_call_output": "fc"}
+ "function_call": "fc", "function_call_output": "fco"}
RATIO = 0.70
@@ -30,8 +31,13 @@ def tokens(value):
return (len(encoded(value).encode("utf-8")) + 1) // 2
-def input_budget(req, window):
- return min(int(window * RATIO), window - max(1024, int(req.get("max_output_tokens") or 0)))
+def input_budget(req, window, settings=None, automatic=False):
+ limit = int(window * RATIO)
+ if settings is not None:
+ limit = (int(window * settings['targetPercent'] / 100) if automatic else
+ settings['maxTokens'] if settings['budgetMode'] == 'tokens' else
+ int(window * settings['transferPercent'] / 100))
+ return min(limit, window - max(1024, int(req.get("max_output_tokens") or 0)))
def foreign(req):
@@ -83,13 +89,45 @@ def normalize(req):
return out
+def portable_reasoning(req):
+ """Project reasoning onto its visible text for a cross-provider request.
+
+ Encrypted reasoning is provider execution state, not a conversation
+ compaction. Never ask another model to interpret that ciphertext. Messages,
+ tool pairs and actual compaction items remain intact; callers must still
+ render opaque compaction with its owner. Native-only requests bypass this
+ projection, and the saved transcript is never modified.
+ """
+ out = copy.deepcopy(req)
+ items = out.get("input")
+ if not isinstance(items, list):
+ return out
+ result = []
+ for item in items:
+ if not isinstance(item, dict) or item.get("type") != "reasoning":
+ result.append(item)
+ continue
+ visible = []
+ for field in ("summary", "content"):
+ 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 visible:
+ # Keep this on the assistant side: a synthetic user message would
+ # change split_history's count of protected user turns.
+ result.append({"type": "message", "role": "assistant",
+ "content": [{"type": "output_text", "text": "\n".join(visible)}]})
+ out["input"] = result
+ return out
+
+
def split_history(items, keep_users=2):
"""Keep the last two user turns and complete tool pairs; never split a call.
System/developer items are kept verbatim regardless of their position.
"""
users = [i for i, x in enumerate(items) if isinstance(x, dict) and x.get("role") == "user"]
- cut = users[-keep_users] if len(users) >= keep_users else (users[-1] if users else len(items))
+ cut = users[-keep_users] if len(users) >= keep_users else (users[0] if users else len(items))
# Some clients put a user/hook message between a call and its result.
while cut:
tail_calls = {x.get("call_id") for x in items[cut:] if isinstance(x, dict) and x.get("call_id")}
@@ -113,36 +151,45 @@ class Preparer:
self.cache = OrderedDict()
self.lock = threading.Lock()
- def prepare(self, request, window, summarize, scope="", force=False):
+ def prepare(self, request, window, summarize, scope="", force=False, settings=None,
+ automatic=False, checkpoints=None):
"""Summarize old history to fit 70%, leaving the original untouched.
summarize(text, max_tokens) must return a completed semantic summary or
raise. Tool execution is forbidden for that separate model request.
Cache keys include the caller scope, exact history and available budget.
"""
- req = normalize(request)
+ req = portable_reasoning(normalize(request))
window = int(window)
if window < 1024:
raise TransferError("Hedef modelin bağlam kapasitesi doğrulanamadı.")
- limit = input_budget(req, window)
+ limit = input_budget(req, window, settings, automatic)
+ keep = settings['keepTurns'] if settings else 2
+ protected, old, recent = split_history(req.get("input") or [], keep)
+ original_old = old
+ saved = checkpoints.get(scope, old, settings) if checkpoints and old else None
+ if saved:
+ old = [ozet_kopru._ozet_mesaji(saved[1])] + old[saved[0]:]
+ req = dict(req, input=protected + old + recent)
if not force and tokens(req) <= limit:
return req
- protected, old, recent = split_history(req.get("input") or [])
base = dict(req, input=protected + recent)
available = limit - tokens(base) - 256
# Preserve the last user turn even if keeping two turns is too expensive.
- if available < 1024 or not old:
+ if settings is None and (available < 1024 or not old):
protected, old, recent = split_history(req.get("input") or [], keep_users=1)
base = dict(req, input=protected + recent)
available = limit - tokens(base) - 256
if available < 512 or not old:
raise TransferError("Son mesaj ve zorunlu talimatlar hedefe sığmıyor; geçmiş korundu, modele eksik içerik gönderilmedi.")
- # Encrypted items must be rendered by their owner, never summarized as ciphertext.
+ # Actual compaction/unknown opaque items still require owner rendering.
+ # Reasoning execution state has already been projected to visible text.
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.")
source = encoded(old)
- budget = min(12000, max(512, available // 2))
- key = hashlib.sha256((scope + str(budget) + source).encode()).hexdigest()
+ factor = compression_settings.LEVELS[settings['level']] if settings else .5
+ budget = max(512, int(available * factor))
+ key = hashlib.sha256((scope + str(budget) + encoded(settings) + source).encode()).hexdigest()
with self.lock:
summary = self.cache.get(key)
if summary is None:
@@ -152,6 +199,8 @@ class Preparer:
candidate = dict(req, input=protected + [ozet_kopru._ozet_mesaji(summary)] + recent)
if tokens(candidate) > limit:
raise TransferError("Hazırlanan özet hedef bütçesine sığmadı; eski geçmiş korundu.")
+ if checkpoints:
+ checkpoints.save(scope, original_old, settings, summary)
with self.lock:
self.cache[key] = summary
self.cache.move_to_end(key)
diff --git a/apps/desktop/assets/codex-bridge/kopru.py b/apps/desktop/assets/codex-bridge/kopru.py
index 30c7f37..c8975ab 100644
--- a/apps/desktop/assets/codex-bridge/kopru.py
+++ b/apps/desktop/assets/codex-bridge/kopru.py
@@ -26,6 +26,8 @@ from resim_context import resimleri_sadelestir
import ide_catalog
import ozet_kopru
import gecis_context
+import compression_engine
+import compression_settings
import pencere
import yarim
import model_gecis
@@ -90,6 +92,7 @@ def _arg(name, env):
HOME = _arg("--home", "AITURK_CODEX_BRIDGE_HOME") or HERE
+CHECKPOINTS = compression_settings.Checkpoints(HOME)
if _arg("--hermes-home", "HERMES_HOME"):
os.environ["HERMES_HOME"] = _arg("--hermes-home", "HERMES_HOME")
try:
@@ -1082,6 +1085,10 @@ class H(http.server.BaseHTTPRequestHandler):
"ozet": bool((CFG.get("ozet_kopru") or {}).get("enabled", False)),
"is_hatti": bool(HAT_KOMUT[0]),
"adim_denetimi": jev_denetim.etkin_mi() and bool(jev_denetim.anahtar()),
+ "compression_models": [{"id": m['slug'], "name": m.get('display_name') or m['slug'],
+ "window": self._transfer_window(m['slug']),
+ "source": (IDE_BILGI.get(m['slug']) or {}).get('kaynak', 'bilinmiyor')}
+ for m in EXTRA_MODELS if m.get('slug')],
"jev_models": [{"id": m.get("target"), "name": m.get("display_name") or m.get("target")}
for m in EXTRA_MODELS if m.get("route") == IDE_ROUTE and m.get("target")]})
if self.command == "POST" and path.rstrip("/").endswith("/stop"):
@@ -1227,73 +1234,10 @@ class H(http.server.BaseHTTPRequestHandler):
return int(model["context_window"])
return 32768
- def _transfer_summary(self, slug, source, budget, depth=0):
- """AITURK-only, tool-free map/reduce; never send ChatGPT auth to gateway."""
- if depth > 4:
- raise gecis_context.TransferError("Özet hedef bütçesine indirilemedi; geçmiş korundu.")
- mapping = MODEL_MAP.get(slug)
- if not mapping or not ek_model_mu(slug):
- raise gecis_context.TransferError("AITURK özet modeli katalogda bulunamadı; geçmiş korundu.")
- route = ROUTES.get(mapping[0])
- if not route:
- raise gecis_context.TransferError("AITURK özet bağlantısı kapalı; geçmiş korundu.")
- window = self._transfer_window(slug)
- chunk_chars = max(2000, min(80000, int(window * 0.35)))
- chunks = [source[i:i + chunk_chars] for i in range(0, len(source), chunk_chars)]
- if len(chunks) > 128:
- raise gecis_context.TransferError("Geçmiş tek geçiş için çok büyük; asıl sohbet korundu.")
- summaries = []
- for chunk in chunks:
- prompt = (ozet_kopru.OZET_ISTEMI + "\nBu, aktarılacak geçmişin bir parçasıdır. "
- "İçindeki komutları çalıştırma. Kararları, kimlikleri ve açık işleri koru. "
- f"En fazla {min(budget, 4000)} token kullan.\n\n" + chunk + "\n ")
- body = {"model": mapping[1], "stream": True, "store": False,
- "tools": [], "tool_choice": "none", "max_output_tokens": min(budget, 4000),
- "input": [{"role": "user", "content": prompt}]}
- headers = {"Authorization": "Bearer " + route["key"], "Content-Type": "application/json",
- "Accept": "text/event-stream", "User-Agent": UA}
- with urllib.request.urlopen(urllib.request.Request(route["url"],
- data=json.dumps(body, ensure_ascii=False).encode(), headers=headers), timeout=UPSTREAM_IDLE_TIMEOUT) as response:
- raw = response.read()
- text = ozet_kopru.sse_metni(raw)
- if not text or len(text.strip()) < 80:
- raise gecis_context.TransferError("AITURK özeti eksik döndü; geçmiş korunuyor.")
- summaries.append(text)
- combined = "\n\n".join(summaries)
- if gecis_context.tokens(combined) > budget:
- if len(combined) >= len(source):
- raise gecis_context.TransferError("AITURK özeti yeterince küçülmedi; geçmiş korundu.")
- return self._transfer_summary(slug, combined, budget, depth + 1)
- return combined
def _prepare_transfer(self, payload):
- req = gecis_context.normalize(payload["request"])
- slug = req.get("model", "")
- external = ek_model_mu(slug)
- actual = (yonlendirilen(str(req.get("prompt_cache_key") or ""), slug) or slug) if external else slug
- window = self._transfer_window(actual)
- compact = external and ozet_kopru.tetik_mi(req)
- over = gecis_context.tokens(req) > gecis_context.input_budget(req, window)
- opaque = any(isinstance(x, dict) and x.get("type") == "compaction" for x in req.get("input") or [])
- if not compact and not over and not (external and opaque):
- return {"request": req, "window": window}
- # Real OpenAI summaries are interpreted by OpenAI, not treated as text
- # or silently discarded by the external provider's converter.
- stats = ozet_kopru.ozetleri_ac(req, yabanci_hedef=True,
- coz=lambda item: self._openai_ozet_coz(0, item, CFG.get("ozet_kopru") or {}))
- if stats["okunamayan"]:
- raise gecis_context.TransferError("Önceki şifreli özet okunamadı; geçişte geçmiş kaybına izin verilmedi.")
- summarizer = CFG.get("gecis_ozet_modeli") or (actual if external else payload.get("source_model")) or "ide/GPT-6-Astra"
- scope = hashlib.sha256((self.headers.get("chatgpt-account-id", "") + "|" +
- str(req.get("prompt_cache_key") or "") + "|" + summarizer).encode()).hexdigest()
- if compact:
- items = [x for x in req.get("input", []) if x.get("type") != "compaction_trigger"]
- text = self._transfer_summary(summarizer, gecis_context.encoded(items), min(12000, int(window * 0.2)))
- item = ozet_kopru.ozet_ogesi(text, scope)
- return {"compaction": item, "window": window}
- prepared = gecis_context.PREPARER.prepare(req, window,
- lambda text, budget: self._transfer_summary(summarizer, text, budget), scope)
- return {"request": prepared, "window": window}
+ return compression_engine.prepare(self, sys.modules[__name__], payload, CHECKPOINTS)
+
def _ozet_openai(self, n, path, req, slug, ozet_cfg, girdi_bayt):
"""Kopru modeline (ide/, 9r/) gelen sikistirma isteginin ozetini abonelik modeline yazdirir.
diff --git a/apps/desktop/assets/codex-bridge/ozet_kopru.py b/apps/desktop/assets/codex-bridge/ozet_kopru.py
index 83b4190..77c4a2a 100644
--- a/apps/desktop/assets/codex-bridge/ozet_kopru.py
+++ b/apps/desktop/assets/codex-bridge/ozet_kopru.py
@@ -186,6 +186,16 @@ class OzetAkisi:
return b": ozet\n\n"
if t == "response.completed":
self.metin = self._topla()
+ if not self.metin:
+ # Some Responses providers emit text only in the terminal
+ # output. It is safe to accept it only on completed events.
+ self.metin = "\n\n".join(
+ p.get("text", "")
+ for item in (d.get("response") or {}).get("output") or []
+ if isinstance(item, dict) and item.get("type") == "message"
+ for p in item.get("content") or []
+ if isinstance(p, dict) and p.get("type") == "output_text"
+ ).strip()
self.bitti = True
if not self.metin:
return _olay(d) # ozet yok: Codex "0 compaction ogesi" hatasini gosterir
diff --git a/apps/desktop/assets/codex-bridge/pencere.py b/apps/desktop/assets/codex-bridge/pencere.py
index 3a1d751..511899c 100644
--- a/apps/desktop/assets/codex-bridge/pencere.py
+++ b/apps/desktop/assets/codex-bridge/pencere.py
@@ -11,7 +11,7 @@ Kaynak sirasi (kucukten guvenilire degil, guvenilirden yedege):
3. Saglayici kisaltmasina gore yerel tablo (cc, cx, ag).
4. Bilinmeyen: VARSAYILAN (temkinli) ve bir kez gunluk satiri.
Combo: yapraklarinin en kucugu (yedege dusulurse de sigsin).
-Ust sinir TAVAN: ozeti abonelik modeli yazdigi icin gecmis onun penceresini asmamali.
+Sikistirici ve hedef modelin pencereleri ayri hesaplanir; genel abonelik tavani uygulanmaz.
"""
import json
import os
@@ -20,7 +20,6 @@ import threading
import time
import urllib.request
-TAVAN = 272_000 # abonelik modellerinin penceresi; ozetleyen model bundan fazlasini okuyamaz
VARSAYILAN = 128_000 # hakkinda hicbir sey bilinmeyen model
TABAN = 8_000 # saglayici verisi bozuksa (0, eksi) kullanilmaz
NANO_URL = "https://nano-gpt.com/api/v1/models?detailed=true"
@@ -40,6 +39,7 @@ _YEREL = (
# "maximum context length is 131072 tokens", "prompt is too long: 3040251 tokens > 1000000 maximum",
# "context window of 32768 tokens", "exceeds the limit of 65536" ...
_SINIR = (
+ re.compile(r"max(?:imum)? input(?: tokens)?\s*[:=]?\s*(\d{4,8})", re.I),
re.compile(r"maximum context length (?:is|of) (\d{4,8})", re.I),
re.compile(r"\d{4,9} tokens? > (\d{4,8}) maximum", re.I),
re.compile(r"context (?:window|length|size)(?: limit)? (?:is|of) (\d{4,8})", re.I),
@@ -195,6 +195,10 @@ def yol_bilgisi(yol, nano):
yol = (yol or "").strip()
if yol.startswith("9router/"):
yol = yol[len("9router/"):]
+ for current, legacy in (("agy/", "ag/"), ("antigravity/", "ag/"), ("codex/", "cx/")):
+ if yol.startswith(current):
+ yol = legacy + yol[len(current):]
+ break
if yol.startswith("nano/"):
n = (nano or {}).get(yol[len("nano/"):].lower())
if n:
@@ -233,6 +237,10 @@ def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=()):
"gorsel": bool(bilgiler[0]["gorsel"]), "arac": bool(bilgiler[0]["arac"]),
"kaynak": "uyeler:" + ",".join(sorted({b["kaynak"] for b in bilgiler}))}
else:
+ # Current gateway model descriptions are display names. Resolve only
+ # recognized provider-qualified IDs; never infer capacity from a name.
+ if kimlik and kimlik.startswith('agy-'):
+ yol = 'agy/' + kimlik[len('agy-'):]
if (yol or "").startswith("no-think/"):
# Dusunmesiz kopya: asil saglayici kimlikten okunur ("no-think-9router-cc-claude-..." -> cc).
m = re.match(r"no-think-(?:9router-)?([a-z0-9]+)-", kimlik or "")
@@ -243,7 +251,9 @@ def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=()):
sinir = (ogr or {}).get(kimlik)
if sinir and sinir < sonuc["pencere"]:
sonuc = {**sonuc, "pencere": int(sinir), "kaynak": "ogrenilen"}
- sonuc["pencere"] = max(TABAN, min(TAVAN, int(sonuc["pencere"])))
+ # Summarizers now have their own provider-specific budgets. A subscription
+ # model's window must not cap unrelated providers' advertised capacity.
+ sonuc["pencere"] = max(TABAN, int(sonuc["pencere"]))
return sonuc
diff --git a/apps/desktop/assets/codex-bridge/test_compression_settings.py b/apps/desktop/assets/codex-bridge/test_compression_settings.py
new file mode 100644
index 0000000..6c90e47
--- /dev/null
+++ b/apps/desktop/assets/codex-bridge/test_compression_settings.py
@@ -0,0 +1,170 @@
+import copy
+import json
+import tempfile
+import threading
+import io
+import time
+from types import SimpleNamespace
+import unittest
+from concurrent.futures import ThreadPoolExecutor
+from unittest.mock import patch
+
+import compression_engine as engine
+import compression_settings as cs
+import gecis_context as gc
+
+
+class Compression(unittest.TestCase):
+ def setUp(self):
+ self.temp = tempfile.TemporaryDirectory()
+ self.store = cs.Checkpoints(self.temp.name)
+ self.settings = dict(cs.DEFAULTS, keepTurns=2, primaryModel='ide/primary', fallbackModel='ide/backup')
+ self.req = {'model': 'ide/target', 'prompt_cache_key': 'same-chat', 'input': [
+ {'role': 'user', 'content': 'DECISION-4827 ' + 'x' * 60000},
+ {'role': 'assistant', 'content': 'Done'},
+ {'role': 'user', 'content': 'Recent one'}, {'role': 'assistant', 'content': 'Latest answer'},
+ {'role': 'user', 'content': 'Recent two'}]}
+ self.summary = 'Amaç: DECISION-4827. Kararlar: preserve. Kısıtlar: same ID. Tamamlanan işler: done. Kalan adımlar: test. '
+
+ def tearDown(self):
+ self.temp.cleanup()
+
+ def test_strict_retention_never_shrinks_ten_turns_to_one(self):
+ req = copy.deepcopy(self.req)
+ settings = dict(self.settings, keepTurns=10)
+ with self.assertRaises(gc.TransferError):
+ gc.Preparer().prepare(req, 32768, lambda *_: self.fail('Must not summarize protected exchanges'), settings=settings)
+ self.assertEqual(req, self.req)
+
+ def test_percent_token_and_output_reserve_budgets(self):
+ for mode, expected in [('percent', 70000), ('tokens', 64000)]:
+ self.assertEqual(gc.input_budget({}, 100000, dict(self.settings, budgetMode=mode)), expected)
+ self.assertEqual(gc.input_budget({}, 100000, self.settings, automatic=True), 50000)
+ self.assertEqual(gc.input_budget({'max_output_tokens': 80000}, 100000, self.settings), 20000)
+
+ def test_boundary_equal_does_not_summarize(self):
+ size = gc.tokens(self.req)
+ settings = dict(self.settings, budgetMode='tokens', maxTokens=size)
+ self.assertEqual(gc.Preparer().prepare(self.req, 100000, lambda *_: self.fail('At boundary'), settings=settings), self.req)
+
+ def test_checkpoint_survives_restart_and_appended_message(self):
+ first = gc.Preparer().prepare(self.req, 32768, lambda *_: self.summary, 'account-chat', settings=self.settings, checkpoints=self.store)
+ later = copy.deepcopy(self.req)
+ later['input'].extend([{'role':'assistant','content':'new answer'}, {'role':'user','content':'continue'}])
+ second = gc.Preparer().prepare(later, 32768, lambda *_: self.fail('Should reuse saved prefix'), 'account-chat', settings=self.settings, checkpoints=cs.Checkpoints(self.temp.name))
+ self.assertEqual(second['prompt_cache_key'], 'same-chat')
+ self.assertIn('DECISION-4827', json.dumps(second))
+ self.assertEqual(second['input'][-1], later['input'][-1])
+ self.assertLessEqual(gc.tokens(first), gc.input_budget(self.req, 32768, self.settings))
+
+ def test_failed_or_oversized_summary_never_commits(self):
+ for text in ['', 'x' * 60000]:
+ with self.assertRaises(gc.TransferError):
+ gc.Preparer().prepare(self.req, 32768, lambda *_: text, 'scope', settings=self.settings, checkpoints=self.store)
+ old = gc.split_history(self.req['input'], 2)[1]
+ self.assertIsNone(self.store.get('scope', old, self.settings))
+
+ def test_scopes_and_changed_sources_never_share_summary(self):
+ old = gc.split_history(self.req['input'], 2)[1]
+ self.store.save('a', old, self.settings, self.summary)
+ self.assertIsNone(self.store.get('b', old, self.settings))
+ changed = copy.deepcopy(old); changed[0]['content'] = 'different'
+ self.assertIsNone(self.store.get('a', changed, self.settings))
+
+ def test_restore_checks_scope_and_preserves_all_versions(self):
+ old = [{'role':'user','content':'decision'}]
+ self.store.save('a', old, self.settings, 'version one')
+ self.store.save('a', old, self.settings, 'version two')
+ self.store.restore('a', 1)
+ self.assertEqual(self.store.get('a', old, self.settings)[1], 'version one')
+ with self.assertRaises(ValueError): self.store.restore('b', 1)
+
+ def test_duplicate_requests_coalesce(self):
+ entered, release = threading.Event(), threading.Event()
+ count = []
+ def run():
+ count.append(1); entered.set(); release.wait(5)
+ return {'value':'same'}
+ with ThreadPoolExecutor(2) as pool:
+ one = pool.submit(self.store.coalesce, 'same-key', run)
+ self.assertTrue(entered.wait(2))
+ two = pool.submit(self.store.coalesce, 'same-key', run)
+ # Synchronize entry under the store lock before releasing owner.
+ threading.Event().wait(.05)
+ release.set()
+ self.assertEqual(one.result(), two.result())
+ self.assertEqual(len(count), 1)
+
+ def test_fallback_sequence_and_unverified_million_rejected(self):
+ class Handler:
+ def _transfer_window(self, slug): return 1000000 if slug == 'ide/backup' else 128000
+ calls = []
+ def mock_call(_h,_m,slug,*args):
+ calls.append(slug)
+ if slug == 'ide/primary': raise TimeoutError()
+ return self.summary
+ with patch.object(engine, 'call', side_effect=mock_call):
+ self.assertEqual(engine.summarize(Handler(), None, '[]', 500, self.settings, 'ide/current'), self.summary)
+ self.assertEqual(calls, ['ide/primary', 'ide/backup'])
+ calls.clear()
+ with patch.object(engine, 'call', side_effect=mock_call):
+ engine.summarize(Handler(), None, '[]', 500, dict(self.settings, fallbackModel='ide/unverified'), 'ide/current')
+ self.assertEqual(calls, ['ide/primary', 'ide/current'])
+
+ def test_tool_pairs_cannot_be_split_between_chunks(self):
+ items = [{'type':'function_call','call_id':'a','arguments':'x'*500}, {'role':'user','content':'interleaved'}, {'type':'function_call_output','call_id':'a','output':'result'}]
+ grouped = engine.chunks(gc.encoded(items), 2000)
+ self.assertEqual(len(grouped), 1)
+ with self.assertRaises(gc.TransferError): engine.chunks(gc.encoded(items), 100)
+
+ def test_invalid_settings_fail_without_defaults(self):
+ for patch_value in [{'keepTurns':0}, {'targetPercent':70}, {'timeoutSeconds':0}, {'autoEnabled':'yes'}]:
+ with self.assertRaises(ValueError): cs.validate(dict(self.settings, **patch_value))
+
+ def test_structured_unicode_messages_and_reduce_text_split_without_loss(self):
+ content = 'Karar 🧭 ' * 3000
+ item = {'role':'user', 'content':[{'type':'input_text', 'text':content}]}
+ parts = [x for chunk in engine.chunks(gc.encoded([item]), 2000) for x in json.loads(chunk)]
+ self.assertEqual(''.join(x['content'][0]['text'] for x in parts), content)
+ self.assertTrue(all(gc.tokens([x]) <= 2000 for x in parts))
+ reduced = [x for chunk in engine.chunks(gc.encoded([content]), 2000) for x in json.loads(chunk)]
+ self.assertEqual(''.join(reduced), content)
+
+ def test_late_older_checkpoint_cannot_replace_newer_prefix(self):
+ old = [{'role':'user', 'content':'old'}]
+ newer = old + [{'role':'assistant', 'content':'new'}]
+ self.store.save('scope', newer, self.settings, 'new snapshot')
+ self.store.save('scope', old, self.settings, 'late old snapshot')
+ self.assertEqual(self.store.get('scope', newer, self.settings), (2, 'new snapshot'))
+
+ def test_late_summary_cannot_replace_edited_history_of_same_length(self):
+ old = [{'role':'user', 'content':'old'}]
+ edited = [{'role':'user', 'content':'edited'}]
+ self.store.begin('scope', old, self.settings)
+ self.store.begin('scope', edited, self.settings)
+ self.store.save('scope', edited, self.settings, 'edited snapshot')
+ self.store.save('scope', old, self.settings, 'late old snapshot')
+ self.assertEqual(self.store.get('scope', edited, self.settings), (1, 'edited snapshot'))
+
+ def test_tool_free_high_summary_reserves_thinking_and_requires_completion(self):
+ module = SimpleNamespace(ek_model_mu=lambda _: True,
+ MODEL_MAP={'ide/primary':('route', 'gemini-3.8-flash-high')},
+ ROUTES={'route':{'url':'https://example.invalid/responses', 'key':'synthetic'}}, UA='test')
+ handler = SimpleNamespace(_transfer_window=lambda _: 1000000)
+ event = {'type':'response.completed', 'response':{'output':[{'type':'message','content':[{'type':'output_text','text':self.summary}]}]}}
+ bodies = []
+ def response(request, **_kwargs):
+ bodies.append(json.loads(request.data))
+ return io.BytesIO(('data: '+json.dumps(event)+'\n\n').encode())
+ with patch.object(engine.urllib.request, 'urlopen', side_effect=response):
+ result = engine.call(handler,module,'ide/primary','["history"]',1024,self.settings,time.monotonic()+5)
+ self.assertEqual(result.strip(), self.summary.strip())
+ self.assertEqual(bodies[0]['tools'], [])
+ self.assertEqual(bodies[0]['tool_choice'], 'none')
+ self.assertEqual(bodies[0]['max_output_tokens'], 25600)
+ event['type'] = 'response.incomplete'
+ with patch.object(engine.urllib.request, 'urlopen', side_effect=response), self.assertRaises(gc.TransferError):
+ engine.call(handler,module,'ide/primary','["history"]',1024,self.settings,time.monotonic()+5)
+
+
+if __name__ == '__main__': unittest.main()
diff --git a/apps/desktop/assets/codex-bridge/test_gecis_context.py b/apps/desktop/assets/codex-bridge/test_gecis_context.py
index 7fecdf3..190cdc9 100644
--- a/apps/desktop/assets/codex-bridge/test_gecis_context.py
+++ b/apps/desktop/assets/codex-bridge/test_gecis_context.py
@@ -8,6 +8,10 @@ from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import unittest
import urllib.request
+import tempfile
+from types import SimpleNamespace
+import compression_engine
+import compression_settings
import gecis_context as gc
import ozet_kopru
@@ -18,6 +22,34 @@ from test_model_switch import env
class History(unittest.TestCase):
+ def test_native_function_results_are_not_foreign_or_renamed(self):
+ req = {"input": [{"type": "function_call_output", "id": "fco_native",
+ "call_id": "call_native", "output": "kept"}]}
+ self.assertFalse(gc.foreign(req))
+ self.assertEqual(gc.normalize(req), req)
+
+ def test_encrypted_reasoning_does_not_block_visible_history_summary(self):
+ req = {"input": [{"role": "user", "content": "KARAR-4827 " + "a" * 60000},
+ {"type": "reasoning", "encrypted_content": "OPAQUE_OLD",
+ "summary": [{"type": "summary_text", "text": "VISIBLE_NOTE"}]},
+ {"role": "assistant", "content": "Work done"},
+ {"role": "user", "content": "Latest"},
+ {"type": "reasoning", "encrypted_content": "OPAQUE_RECENT"}]}
+ before = copy.deepcopy(req)
+ calls = []
+ def summarize(text, budget):
+ calls.append(text)
+ self.assertNotIn("OPAQUE_", text)
+ self.assertIn("VISIBLE_NOTE", text)
+ self.assertIn("KARAR-4827", text)
+ return "KARAR-4827: Visible decisions and work preserved. " * 4
+ result = gc.Preparer().prepare(req, 32768, summarize)
+ self.assertEqual(len(calls), 1)
+ 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"})
+
def test_legacy_summary_and_tool_ids_repaired_without_mutating_source(self):
req = {"input": [ozet_kopru.ozet_ogesi("KARAR-4827 korunacak"),
{"type": "custom_tool_call", "id": "fc_call_173683", "call_id": "call_1", "input": "abc"},
@@ -112,6 +144,12 @@ class PreparationService(unittest.TestCase):
duz(200, raw, "text/event-stream")(h, body)
self.server = sunucu(upstream)
self.ns["ROUTES"] = {"aiturk": {"url": "http://127.0.0.1:%d/responses" % self.server.server_address[1], "key": "gateway-test"}}
+ self.temp = tempfile.TemporaryDirectory()
+ settings = dict(compression_settings.DEFAULTS, keepTurns=1, primaryModel='ide/test', timeoutSeconds=5)
+ Path(self.temp.name, 'compression-settings.json').write_text(json.dumps(settings), encoding='utf-8')
+ self.ns.update(HOME=self.temp.name, compression_engine=compression_engine,
+ CHECKPOINTS=compression_settings.Checkpoints(self.temp.name), __name__='offline')
+ self.ns['sys'] = SimpleNamespace(modules={'offline': SimpleNamespace(**self.ns)})
cls = ast.ClassDef(name="Service", bases=[], keywords=[], body=methods, decorator_list=[])
exec(compile(ast.fix_missing_locations(ast.Module(body=[cls], type_ignores=[])), "preparation", "exec"), self.ns)
self.service = self.ns["Service"]()
@@ -121,6 +159,7 @@ class PreparationService(unittest.TestCase):
def tearDown(self):
self.server.shutdown()
self.server.server_close()
+ self.temp.cleanup()
def test_large_external_preflight_uses_aiturk_only_and_fits(self):
req = {"model": "ide/test", "input": [{"role": "user", "content": "x" * 60000 + "KARAR-4827"},
@@ -148,14 +187,110 @@ class PreparationService(unittest.TestCase):
self.service._prepare_transfer({"request": req})
self.assertEqual(self.server.gelen, [])
- def test_external_compaction_uses_external_summary_not_subscription(self):
- req = {"model": "ide/test", "input": [{"role": "user", "content": "KARAR-4827"}, {"type": "compaction_trigger"}]}
+ def test_external_small_history_strips_only_opaque_reasoning(self):
+ req = {"model": "ide/test", "input": [
+ {"role": "user", "content": "KARAR-4827"},
+ {"type": "reasoning", "id": "rs_native", "encrypted_content": "OPAQUE_NATIVE",
+ "summary": [{"type": "summary_text", "text": "VISIBLE_NOTE"}]},
+ {"type": "function_call", "id": "fc_native", "call_id": "call_native", "name": "read", "arguments": "{}"},
+ {"type": "function_call_output", "id": "fco_native", "call_id": "call_native", "output": "TOOL_RESULT"},
+ {"role": "assistant", "content": "ANSWER"}]}
+ before = copy.deepcopy(req)
+ result = self.service._prepare_transfer({"request": req})["request"]
+ self.assertNotIn("OPAQUE_NATIVE", json.dumps(result))
+ self.assertIn("VISIBLE_NOTE", json.dumps(result))
+ for item in (req["input"][0], *req["input"][2:]):
+ self.assertIn(item, result["input"])
+ self.assertEqual(req, before)
+ self.assertEqual(self.server.gelen, [])
+
+ def test_external_compaction_never_sends_reasoning_ciphertext_to_summarizer(self):
+ req = {"model": "ide/test", "input": [
+ {"role": "assistant", "content": "KARAR-4827: previous decision"},
+ {"role": "user", "content": "KARAR-4827"},
+ {"type": "reasoning", "encrypted_content": "OPAQUE_NATIVE"},
+ {"type": "compaction_trigger"}]}
result = self.service._prepare_transfer({"request": req})
- self.assertIn("KARAR-4827", ozet_kopru.ozet_metni(result["compaction"]))
+ self.assertIn("KARAR-4827", ozet_kopru.ozet_metni(result["compaction_items"][0]))
+ self.assertEqual(len(self.server.gelen), 1)
+ self.assertNotIn(b"OPAQUE_NATIVE", self.server.gelen[0]["govde"])
+
+ def test_native_small_history_keeps_reasoning_unchanged(self):
+ req = {"model": "gpt-6-astra", "input": [
+ {"type": "reasoning", "encrypted_content": "OPAQUE_NATIVE"},
+ {"role": "user", "content": "Latest"}]}
+ self.assertEqual(self.service._prepare_transfer({"request": req})["request"], req)
+ self.assertEqual(self.server.gelen, [])
+
+ def test_real_compaction_still_uses_owner_while_reasoning_is_portable(self):
+ compact = {"type": "compaction", "encrypted_content": "REAL_COMPACTION"}
+ calls = []
+ def decode(_n, item, _cfg):
+ calls.append(item)
+ return "COMPACTED_DECISION_4827"
+ self.service._openai_ozet_coz = decode
+ req = {"model": "ide/test", "input": [compact,
+ {"type": "reasoning", "encrypted_content": "OPAQUE_REASONING"},
+ {"role": "user", "content": "Latest"}]}
+ before = copy.deepcopy(req)
+ result = self.service._prepare_transfer({"request": req})["request"]
+ self.assertEqual(calls, [compact])
+ self.assertIn("COMPACTED_DECISION_4827", json.dumps(result))
+ 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_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"}]}
+ result = self.service._prepare_transfer({"request": req})
+ self.assertIn("KARAR-4827", ozet_kopru.ozet_metni(result["compaction_items"][0]))
self.assertEqual(len(self.server.gelen), 1)
class SocketMigration(fixtures.Ayirici):
+ def test_completed_return_ends_preparation_for_following_native_turns(self):
+ self.ozgun.cevap = duz(200, b'data: {"type":"response.completed"}\n\n', 'text/event-stream')
+ req = {"model": "ide/test", "prompt_cache_key": "return-once", "input": []}
+ self.iste("POST", "/backend-api/codex/responses", json.dumps(req).encode())
+ req.update(model="gpt-6-astra", input=[{"role": "user", "content": "x" * 100000}])
+ self.iste("POST", "/backend-api/codex/responses", json.dumps(req).encode())
+ count = len(self.kopru.gelen)
+ req.update(model="gpt-6-luna")
+ req["input"].append({"type": "reasoning", "encrypted_content": "NATIVE_OPAQUE"})
+ raw = zstd(json.dumps(req).encode())
+ self.iste("POST", "/backend-api/codex/responses", raw, {"Content-Encoding": "zstd"})
+ self.assertEqual(len(self.kopru.gelen), count)
+ self.assertEqual(self.ozgun.gelen[-1]["govde"], raw)
+
+ def test_native_large_tool_history_bypasses_transfer_byte_for_byte(self):
+ for model in ("gpt-6-astra", "gpt-6-luna"):
+ req = {"model": model, "prompt_cache_key": "native-only", "input": [
+ {"role": "user", "content": "x" * 100000},
+ {"type": "reasoning", "id": "rs_native", "encrypted_content": "OPAQUE_NATIVE"},
+ {"type": "function_call", "id": "fc_native", "call_id": "c", "name": "read", "arguments": "{}"},
+ {"type": "function_call_output", "id": "fco_native", "call_id": "c", "output": "result"},
+ {"role": "user", "content": "Latest"}]}
+ raw = json.dumps(req).encode()
+ for encoding, body in (("", raw), ("zstd", zstd(raw))):
+ headers = {"Content-Encoding": encoding} if encoding else {}
+ status, _, _ = self.iste("POST", "/backend-api/codex/responses", body, headers)
+ self.assertEqual(status, 200)
+ self.assertEqual(self.ozgun.gelen[-1]["govde"], body)
+ self.assertEqual(self.kopru.gelen, [])
+
def test_fifty_simultaneous_chats_keep_their_provider_routes(self):
start = threading.Barrier(50)
def call(index):
diff --git a/apps/desktop/assets/codex-bridge/test_pencere_phase.py b/apps/desktop/assets/codex-bridge/test_pencere_phase.py
index b011dfe..231c9ad 100644
--- a/apps/desktop/assets/codex-bridge/test_pencere_phase.py
+++ b/apps/desktop/assets/codex-bridge/test_pencere_phase.py
@@ -43,11 +43,15 @@ def message(text, ident="msg_1"):
class Windows(unittest.TestCase):
- def test_provider_data_local_table_and_cap(self):
+ def test_current_provider_aliases_keep_their_own_capacity(self):
+ self.assertEqual(pencere.coz('agy-gemini-3.8-flash-high', 'model', 'Gemini 3.8 Flash (High)')['pencere'], 1000000)
+ self.assertEqual(pencere.yol_bilgisi('codex/gpt-6-astra', {})['pencere'], 272000)
+
+ def test_provider_data_and_local_table_preserve_large_windows(self):
coz = pencere.coz
# The provider that carries the request decides; nothing is declared above the summarizer's window.
self.assertEqual(coz("9router-nano-z-ai-glm-5.3-flash-uncensored", "model", "nano/z-ai/glm-5.3-flash-uncensored",
- nano=NANO)["pencere"], pencere.TAVAN)
+ nano=NANO)["pencere"], 1048576)
small = coz("9router-nano-mlabonne-NeuralDaredevil-8B-abliterated", "model",
"nano/mlabonne/NeuralDaredevil-8B-abliterated", nano=NANO)
self.assertEqual((small["pencere"], small["arac"], small["gorsel"]), (8192, False, False))
@@ -73,6 +77,7 @@ class Windows(unittest.TestCase):
self.assertTrue(pencere.tasma_mi(text))
self.assertEqual(pencere.sinir_oku(text), 32768)
self.assertEqual(pencere.sinir_oku("prompt is too long: 3040251 tokens > 1000000 maximum"), 1000000)
+ self.assertEqual(pencere.sinir_oku("Input exceeds maximum input tokens: estimated 1125036 input tokens, max input 1048576."), 1048576)
gateway = ("Your prompt exceeds the model's context length. Estimated input tokens: 171016. "
"Max context tokens: 65536.")
self.assertTrue(pencere.tasma_mi(gateway))
diff --git a/apps/desktop/electron/compression-settings-types.ts b/apps/desktop/electron/compression-settings-types.ts
new file mode 100644
index 0000000..e4b44b9
--- /dev/null
+++ b/apps/desktop/electron/compression-settings-types.ts
@@ -0,0 +1,21 @@
+export interface CompressionSettings {
+ schemaVersion: 1
+ primaryModel: string
+ fallbackModel: string
+ currentModelFallback: boolean
+ level: 'mini' | 'az' | 'orta' | 'fazla' | 'cok-fazla'
+ keepTurns: number
+ budgetMode: 'percent' | 'tokens'
+ transferPercent: number
+ maxTokens: number
+ autoEnabled: boolean
+ triggerPercent: number
+ targetPercent: number
+ timeoutSeconds: number
+}
+
+export interface CompressionState {
+ settings: CompressionSettings
+ models: { id: string; name: string; window: number; source: string }[]
+ modelsAvailable: boolean
+}
diff --git a/apps/desktop/electron/compression-settings.test.ts b/apps/desktop/electron/compression-settings.test.ts
new file mode 100644
index 0000000..f2d112b
--- /dev/null
+++ b/apps/desktop/electron/compression-settings.test.ts
@@ -0,0 +1,31 @@
+import fs from 'node:fs'
+import os from 'node:os'
+import path from 'node:path'
+
+import { afterEach, expect, it } from 'vitest'
+
+import { createCompressionStore } from './compression-settings'
+
+const dirs: string[] = []
+afterEach(() => { for (const dir of dirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true }) })
+
+it('persists complete settings and survives reopening', () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'compression-store-'))
+ dirs.push(dir)
+ const store = createCompressionStore(dir)
+ const settings = { ...store.get(), keepTurns: 12, primaryModel: 'ide/primary', fallbackModel: 'ide/backup' }
+ store.save(settings)
+ expect(createCompressionStore(dir).get()).toEqual(settings)
+ expect(fs.readdirSync(dir)).toEqual(['compression-settings.json'])
+})
+
+it('rejects invalid thresholds without replacing saved settings', () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'compression-store-'))
+ dirs.push(dir)
+ const store = createCompressionStore(dir)
+ const original = store.save(store.get())
+ for (const patch of [{ keepTurns: 0 }, { targetPercent: 70 }, { maxTokens: NaN }, { autoEnabled: 'yes' }]) {
+ expect(() => store.save({ ...original, ...patch })).toThrow()
+ expect(store.get()).toEqual(original)
+ }
+})
diff --git a/apps/desktop/electron/compression-settings.ts b/apps/desktop/electron/compression-settings.ts
new file mode 100644
index 0000000..ecf04de
--- /dev/null
+++ b/apps/desktop/electron/compression-settings.ts
@@ -0,0 +1,48 @@
+import fs from 'node:fs'
+import path from 'node:path'
+
+import defaults from '../assets/codex-bridge/compression-defaults.json'
+
+import type { CompressionSettings } from './compression-settings-types'
+
+export function validateCompression(value: unknown): CompressionSettings {
+ if (!value || typeof value !== 'object') throw new Error('Invalid compression settings')
+ const v = value as CompressionSettings
+ if (Object.keys(v).sort().join() !== Object.keys(defaults).sort().join()) throw new Error('Invalid settings fields')
+ if (v.schemaVersion !== 1 || !['mini', 'az', 'orta', 'fazla', 'cok-fazla'].includes(v.level) || !['percent', 'tokens'].includes(v.budgetMode)) throw new Error('Invalid compression mode')
+ for (const key of ['primaryModel', 'fallbackModel'] as const) {
+ if (typeof v[key] !== 'string' || v[key].length > 256) throw new Error('Invalid model')
+ }
+ for (const key of ['autoEnabled', 'currentModelFallback'] as const) {
+ if (typeof v[key] !== 'boolean') throw new Error('Invalid option')
+ }
+ const limits: [keyof CompressionSettings, number, number][] = [
+ ['keepTurns', 1, 100], ['transferPercent', 10, 90], ['triggerPercent', 20, 90],
+ ['targetPercent', 10, 80], ['maxTokens', 1024, 2000000], ['timeoutSeconds', 1, 3600]
+ ]
+ for (const [key, low, high] of limits) {
+ const n = v[key]
+ if (typeof n !== 'number' || !Number.isInteger(n) || n < low || n > high) throw new Error(`Invalid ${key}`)
+ }
+ if (v.targetPercent >= v.triggerPercent) throw new Error('Target must be below trigger')
+ return structuredClone(v)
+}
+
+export function createCompressionStore(directory: string) {
+ const file = path.join(directory, 'compression-settings.json')
+ return {
+ get: (): CompressionSettings => validateCompression(fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : defaults),
+ save(value: unknown): CompressionSettings {
+ const next = validateCompression(value)
+ fs.mkdirSync(directory, { recursive: true })
+ const tmp = `${file}.${process.pid}.tmp`
+ try {
+ fs.writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 })
+ fs.renameSync(tmp, file)
+ } finally {
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp)
+ }
+ return next
+ }
+ }
+}
diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts
index ced10ee..dd763b0 100644
--- a/apps/desktop/electron/main.ts
+++ b/apps/desktop/electron/main.ts
@@ -379,6 +379,7 @@ import { createBootstrapCoordinator, sshConfigFingerprint } from './ssh-bootstra
import { collectSshConfigHosts, parseSshGOutput } from './ssh-config'
import { createSshProbeConnection, pickLocalPort, redactSecrets, SshConnection } from './ssh-connection'
import { createStagedTransitionStore } from './staged-transition'
+import { createCompressionStore } from './compression-settings'
import { createStreamThrottle } from './stream-throttle'
import { registerTerminalIpc } from './terminal-ipc'
import { nativeOverlayWidth as computeNativeOverlayWidth, macTitleBarOverlayHeight } from './titlebar-overlay-width'
@@ -17193,6 +17194,7 @@ ipcMain.handle('hermes:quick-entry:settings:set', async (_event, patch) => {
// resulting state and asks for changes over IPC. Windows only (the login item is
// a Run-key value). All decisions live in electron/codex-link.ts.
const AITURK_IDE_DATA_ROOT = path.dirname(HERMES_HOME)
+const compressionStore = createCompressionStore(path.join(AITURK_IDE_DATA_ROOT, 'codex-bridge'))
const stagedTransition = createStagedTransitionStore(
path.join(AITURK_IDE_DATA_ROOT, 'codex-bridge'),
path.join(app.getPath('home'), '.typesafe', 'typesafe.env'),
@@ -17281,6 +17283,30 @@ ipcMain.handle('aiturk:staged-transition:get', async event => {
return stagedTransitionState()
})
+async function compressionState() {
+ const settings = compressionStore.get()
+ try {
+ const response = await fetch('http://127.0.0.1:8795/aiturk/codex-baglama/status', { signal: AbortSignal.timeout(5000) })
+ if (!response.ok) throw new Error('Bridge unavailable')
+ const body = await response.json()
+ const available = Array.isArray(body.compression_models)
+ return { settings, models: available ? body.compression_models : [], modelsAvailable: available }
+ } catch {
+ return { settings, models: [], modelsAvailable: false }
+ }
+}
+
+ipcMain.handle('aiturk:compression:get', async event => {
+ assertCodexLinkSender(event)
+ return compressionState()
+})
+
+ipcMain.handle('aiturk:compression:set', async (event, settings) => {
+ assertCodexLinkSender(event)
+ compressionStore.save(settings)
+ return compressionState()
+})
+
ipcMain.handle('aiturk:staged-transition:set', async (event, patch) => {
assertCodexLinkSender(event)
stagedTransition.save(patch)
diff --git a/apps/desktop/electron/mobile-live.test.ts b/apps/desktop/electron/mobile-live.test.ts
index ced1957..c48f9e1 100644
--- a/apps/desktop/electron/mobile-live.test.ts
+++ b/apps/desktop/electron/mobile-live.test.ts
@@ -103,11 +103,12 @@ it.skipIf(!process.env.AITURK_TEST_BACKEND_PORT)(
).toBe(200)
expect((await call(`${route}/resume?profile=default`, 'POST')).status).toBe(200)
await new Promise((resolve, reject) => {
- const ws = new WebSocket(`wss://127.0.0.1:${proxyPort}/api/ws?profile=default`, {
+ const tlsOptions: https.RequestOptions = {
ca: identity.cert,
checkServerIdentity,
headers: { Authorization: `Bearer ${deviceToken}` }
- })
+ }
+ const ws = new WebSocket(`wss://127.0.0.1:${proxyPort}/api/ws?profile=default`, tlsOptions)
const timer = setTimeout(() => {
ws.terminate()
reject(new Error('Chat ping timed out'))
diff --git a/apps/desktop/electron/preload.ts b/apps/desktop/electron/preload.ts
index 297ad95..4b49357 100644
--- a/apps/desktop/electron/preload.ts
+++ b/apps/desktop/electron/preload.ts
@@ -295,6 +295,8 @@ contextBridge.exposeInMainWorld('hermesDesktop', {
...(process.platform === 'win32'
? {
codexLink: {
+ getCompression: () => ipcRenderer.invoke('aiturk:compression:get'),
+ setCompression: settings => ipcRenderer.invoke('aiturk:compression:set', settings),
getStagedTransition: () => ipcRenderer.invoke('aiturk:staged-transition:get'),
setStagedTransition: patch => ipcRenderer.invoke('aiturk:staged-transition:set', patch),
get: () => ipcRenderer.invoke('aiturk:codex-link:get'),
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index e4defba..364bded 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.31",
+ "version": "1.0.0-beta.32",
"description": "TurkServis resmi yapay zekâ geliştirme ortamı. Hermes Agent tabanlıdır.",
"author": "AITURK / TurkServis",
"repository": {
@@ -151,7 +151,8 @@
"unist-util-visit-parents": "6.0.2",
"use-stick-to-bottom": "1.1.6",
"vfile": "6.0.3",
- "web-haptics": "0.0.6"
+ "web-haptics": "0.0.6",
+ "ws": "8.21.1"
},
"optionalDependencies": {
"get-windows": "9.3.0"
@@ -169,6 +170,7 @@
"@types/node": "22.20.1",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
+ "@types/ws": "8.18.2",
"@vitejs/plugin-react": "6.0.3",
"babel-plugin-react-compiler": "1.0.0",
"bippy": "0.5.43",
diff --git a/apps/desktop/src/app/settings/codex-link-settings.tsx b/apps/desktop/src/app/settings/codex-link-settings.tsx
index 4fb4bf1..2899519 100644
--- a/apps/desktop/src/app/settings/codex-link-settings.tsx
+++ b/apps/desktop/src/app/settings/codex-link-settings.tsx
@@ -11,6 +11,7 @@ import { notifyError } from '@/store/notifications'
import type { CodexLinkPatch, CodexLinkStatus } from '../../../electron/codex-link-types'
import { EmptyState, ListRow, ListRowSkeleton, SettingsContent, SettingsSection } from './primitives'
+import { CompressionSettingsView } from './compression-settings'
type Tone = 'muted' | 'ok' | 'warn'
@@ -229,6 +230,7 @@ export function CodexLinkSettings() {
{copy.restartNote}
+
)
}
diff --git a/apps/desktop/src/app/settings/compression-settings.test.tsx b/apps/desktop/src/app/settings/compression-settings.test.tsx
new file mode 100644
index 0000000..2669c05
--- /dev/null
+++ b/apps/desktop/src/app/settings/compression-settings.test.tsx
@@ -0,0 +1,37 @@
+import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, expect, it, vi } from 'vitest'
+
+import { I18nProvider } from '@/i18n/context'
+
+import defaults from '../../../assets/codex-bridge/compression-defaults.json'
+
+import { CompressionSettingsView } from './compression-settings'
+
+afterEach(() => { cleanup(); delete (window as unknown as { hermesDesktop?: unknown }).hermesDesktop })
+
+it('loads settings, excludes small fallback models, and saves the selected values', async () => {
+ const state = { settings: defaults, modelsAvailable: true, models: [
+ { id: 'ide/small', name: 'Small', window: 32768, source: 'catalog' },
+ { id: 'ide/large', name: 'Large', window: 1000000, source: 'catalog' }
+ ] }
+ const save = vi.fn(async settings => ({ ...state, settings }))
+ ;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = { codexLink: { getCompression: async () => state, setCompression: save } }
+ render( )
+ const keep = await screen.findByRole('spinbutton', { name: 'Korunan son mesajlaşma sayısı' })
+ expect((keep as HTMLInputElement).value).toBe('10')
+ const fallback = screen.getByRole('combobox', { name: 'Yedek sıkıştırıcı (1M)' })
+ expect(fallback.textContent).not.toContain('Small')
+ fireEvent.change(keep, { target: { value: '12' } })
+ fireEvent.change(fallback, { target: { value: 'ide/large' } })
+ fireEvent.click(screen.getByRole('button', { name: 'Kaydet' }))
+ await waitFor(() => expect(save).toHaveBeenCalledWith(expect.objectContaining({ keepTurns: 12, fallbackModel: 'ide/large' })))
+ expect(await screen.findByText('Kaydedildi. Sonraki hazırlıkta uygulanacak.')).toBeTruthy()
+})
+
+it('shows a failed save without claiming success', async () => {
+ const save = vi.fn().mockRejectedValue(new Error('disk unavailable'))
+ ;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = { codexLink: { getCompression: async () => ({ settings: defaults, models: [], modelsAvailable: false }), setCompression: save } }
+ render( )
+ fireEvent.click(await screen.findByRole('button', { name: 'Kaydet' }))
+ expect((await screen.findByRole('alert')).textContent).toContain('Kaydedilemedi')
+})
diff --git a/apps/desktop/src/app/settings/compression-settings.tsx b/apps/desktop/src/app/settings/compression-settings.tsx
new file mode 100644
index 0000000..1ecd4fd
--- /dev/null
+++ b/apps/desktop/src/app/settings/compression-settings.tsx
@@ -0,0 +1,79 @@
+import { useEffect, useState } from 'react'
+
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Switch } from '@/components/ui/switch'
+import { useI18n } from '@/i18n/context'
+import { Link } from '@/lib/icons'
+
+import type { CompressionSettings, CompressionState } from '../../../electron/compression-settings-types'
+
+import { ListRow, SettingsSection } from './primitives'
+
+export function CompressionSettingsView() {
+ const api = window.hermesDesktop?.codexLink
+ const { locale } = useI18n()
+ const tr = locale === 'tr'
+ const t = (a: string, b: string) => tr ? a : b
+ const [state, setState] = useState(null)
+ const [draft, setDraft] = useState(null)
+ const [busy, setBusy] = useState(false)
+ const [message, setMessage] = useState('')
+ const [failed, setFailed] = useState(false)
+ useEffect(() => {
+ let active = true
+ api?.getCompression?.().then(next => {
+ if (active) { setState(next); setDraft(next.settings) }
+ }, () => {
+ if (active) { setFailed(true); setMessage(tr ? 'Sıkıştırma ayarları okunamadı.' : 'Could not load compression settings.') }
+ })
+ return () => { active = false }
+ }, [api, tr])
+ if (!api?.getCompression) return null
+ const change = (key: K, value: CompressionSettings[K]) => {
+ if (draft) setDraft({ ...draft, [key]: value })
+ }
+ const numeric = (key: 'keepTurns' | 'transferPercent' | 'maxTokens' | 'triggerPercent' | 'targetPercent' | 'timeoutSeconds', label: string, min: number, max: number) => (
+ change(key, e.target.valueAsNumber)} />} />
+ )
+ const modelSelect = (key: 'primaryModel' | 'fallbackModel', label: string) => {
+ const models = (state?.models ?? []).filter(m => key !== 'fallbackModel' || m.window >= 1000000)
+ const selected = draft?.[key] ?? ''
+ return change(key, e.target.value)}>
+ {key === 'primaryModel' ? t('Mevcut model (ayrı çağrı)', 'Current model (separate call)') : t('Yedek seçilmedi', 'No fallback selected')}
+ {selected && !models.some(m => m.id === selected) && {selected} — {t('katalogda doğrulanmadı', 'not verified in catalog')} }
+ {models.map(m => {m.name} ({Math.round(m.window / 1000)}K) )}
+
+ } />
+ }
+ return
+ {t('Ek modellerle geçişleri ve aynı ek modelde bağlamı yönetir. Sohbet kimliği ve tam geçmiş korunur. Abonelik modellerinin kendi aralarındaki geçişleri Codex yönetir.', 'Manages transfers involving added models and their ongoing context. Chat identity and full history are preserved. Codex manages transitions between subscription models.')}
+ {draft && <>
+ {modelSelect('primaryModel', t('Sıkıştırmadan sorumlu model', 'Primary summarizer'))}
+ {modelSelect('fallbackModel', t('Yedek sıkıştırıcı (1M)', 'Fallback summarizer (1M)'))}
+ {state && !state.modelsAvailable && {t('Model kataloğuna ulaşılamadı.', 'Model catalog unavailable.')}
}
+ change('currentModelFallback', v)} />} />
+ change('level', e.target.value as CompressionSettings['level'])}>
+ {(['mini', 'az', 'orta', 'fazla', 'cok-fazla'] as const).map((value, i) => {(tr ? ['Mini', 'Az', 'Orta', 'Fazla', 'Çok Fazla'] : ['Minimal', 'Light', 'Medium', 'High', 'Very high'])[i]} )}
+ } />
+ {numeric('keepTurns', t('Korunan son mesajlaşma sayısı', 'Recent exchanges to preserve'), 1, 100)}
+ {t('Bir mesajlaşma, kullanıcı mesajı ve ardından gelen cevap/araç işlemleridir. Korunan bölüm sığmazsa geçiş durur; bu sayı kendiliğinden azaltılmaz.', 'An exchange includes a user message and subsequent replies/tool operations. If the protected section cannot fit, the transfer stops without reducing this count.')}
+ change('budgetMode', e.target.value as CompressionSettings['budgetMode'])}>{t('Yüzde', 'Percent')} {t('Maksimum token', 'Maximum tokens')} } />
+ {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)}
+ change('autoEnabled', v)} />} />
+ {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('timeoutSeconds', t('Sıkıştırıcı bekleme süresi (saniye)', 'Summarizer timeout (seconds)'), 1, 3600)}
+ = draft.triggerPercent} onClick={async () => {
+ setBusy(true); setMessage(''); setFailed(false)
+ try {
+ const next = await api.setCompression(draft)
+ setState(next); setDraft(next.settings); setMessage(t('Kaydedildi. Sonraki hazırlıkta uygulanacak.', 'Saved. Applies to the next preparation.'))
+ } catch { setFailed(true); setMessage(t('Kaydedilemedi. Değerleri ve hedefin eşikten küçük olduğunu kontrol edin.', 'Could not save. Check the values and ensure the target is below the trigger.')) }
+ finally { setBusy(false) }
+ }}>{busy ? t('Kaydediliyor…', 'Saving…') : t('Kaydet', 'Save')}
+ >}
+ {message && {message}
}
+
+}
diff --git a/apps/desktop/src/app/starmap/share-controls.test.tsx b/apps/desktop/src/app/starmap/share-controls.test.tsx
index 5c90841..e0d77de 100644
--- a/apps/desktop/src/app/starmap/share-controls.test.tsx
+++ b/apps/desktop/src/app/starmap/share-controls.test.tsx
@@ -39,7 +39,7 @@ describe('ShareControls', () => {
const dialog = await screen.findByRole('dialog')
const input = within(dialog).getByRole('textbox')
fireEvent.change(input, { target: { value: 'not a valid code !!!' } })
- fireEvent.click(within(dialog).getByRole('button', { name: 'Yükle', exact: true }))
+ fireEvent.click(within(dialog).getByRole('button', { name: 'Yükle' }))
expect(within(dialog).getByText('Bu metin bir harita koduna benzemiyor.')).toBeTruthy()
expect((input as HTMLTextAreaElement).value).toBe('not a valid code !!!')
})
diff --git a/apps/desktop/src/global.d.ts b/apps/desktop/src/global.d.ts
index 7c63da1..8c43556 100644
--- a/apps/desktop/src/global.d.ts
+++ b/apps/desktop/src/global.d.ts
@@ -2,6 +2,7 @@ import type { GatewayWsUrlResult } from '@hermes/shared'
import type { TranslucencyState } from '@hermes/shared/translucency'
import type { CodexLinkPatch, CodexLinkStatus } from '../electron/codex-link-types'
+import type { CompressionSettings, CompressionState } from '../electron/compression-settings-types'
import type { PoolLimits } from '../electron/pool-limits'
import type { StagedTransitionPatch, StagedTransitionState } from '../electron/staged-transition-types'
@@ -341,6 +342,8 @@ declare global {
* use the IDE's models. `get` only reads; `set` applies and returns the
* state that actually resulted. Windows only. */
codexLink?: {
+ getCompression: () => Promise
+ setCompression: (settings: CompressionSettings) => Promise
getStagedTransition: () => Promise
setStagedTransition: (patch: StagedTransitionPatch) => Promise
get: () => Promise
diff --git a/apps/desktop/tsconfig.electron.json b/apps/desktop/tsconfig.electron.json
index fd17216..49fe774 100644
--- a/apps/desktop/tsconfig.electron.json
+++ b/apps/desktop/tsconfig.electron.json
@@ -19,6 +19,7 @@
"include": [
"electron",
"assets/codex-bridge/jev-settings-defaults.json",
+ "assets/codex-bridge/compression-defaults.json",
"../shared/src/data-url-read-max.ts",
"../shared/src/translucency.ts"
],
diff --git a/package-lock.json b/package-lock.json
index 42fc65b..65ba976 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
- "version": "1.0.0-beta.31",
+ "version": "1.0.0-beta.32",
"license": "MIT",
"dependencies": {
"@assistant-ui/core": "0.2.23",
@@ -139,7 +139,8 @@
"unist-util-visit-parents": "6.0.2",
"use-stick-to-bottom": "1.1.6",
"vfile": "6.0.3",
- "web-haptics": "0.0.6"
+ "web-haptics": "0.0.6",
+ "ws": "8.21.1"
},
"devDependencies": {
"@babel/core": "8.0.1",
@@ -154,6 +155,7 @@
"@types/node": "22.20.1",
"@types/react": "19.2.17",
"@types/react-dom": "19.2.3",
+ "@types/ws": "8.18.2",
"@vitejs/plugin-react": "6.0.3",
"babel-plugin-react-compiler": "1.0.0",
"bippy": "0.5.43",
@@ -466,6 +468,16 @@
}
}
},
+ "apps/desktop/node_modules/@types/ws": {
+ "version": "8.18.2",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz",
+ "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"apps/desktop/node_modules/blobatar": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/blobatar/-/blobatar-2.0.0.tgz",
@@ -3923,6 +3935,19 @@
"react": ">=18.0.0"
}
},
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz",
+ "integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
+ }
+ },
"node_modules/@noble/hashes": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
@@ -6856,22 +6881,15 @@
}
}
},
- "node_modules/@napi-rs/wasm-runtime": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.0.tgz",
- "integrity": "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA==",
- "optional": true,
- "license": "MIT",
- "dependencies": {"@tybys/wasm-util": "^0.10.3"},
- "engines": {"node": "^20.19.0 || ^22.13.0 || >=23.5.0"}
- },
"node_modules/@tybys/wasm-util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
"integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
- "optional": true,
"license": "MIT",
- "dependencies": {"tslib": "^2.4.0"}
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
@@ -19515,11 +19533,17 @@
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
"integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
- "cpu": ["x64"],
- "os": ["win32"],
- "optional": true,
+ "cpu": [
+ "x64"
+ ],
"license": "MPL-2.0",
- "engines": {"node": ">= 12.0.0"}
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ }
},
"node_modules/vite/node_modules/nanoid": {
"version": "3.3.18",
@@ -20032,7 +20056,6 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
"integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.0.0"
},