Compare commits

..
Author SHA1 Message Date
yilsem 8f53b090f7 feat(codex-link): persist direct subscription and AITURK connection modes
Server CI and Windows package / verify (push) Successful in 2m54s
Server CI and Windows package / windows-package (push) Successful in 7m17s
2026-10-13 19:56:40 +03:00
yilsem 049c4d1338 fix(installer): preserve healthy Python runtime and Codex connections
Server CI and Windows package / verify (push) Successful in 2m47s
Server CI and Windows package / windows-package (push) Successful in 6m58s
2026-10-13 17:22:28 +03:00
yilsem 4924781e2d fix(bridge): stop replaying explicit terminal provider failures 2026-10-13 17:18:38 +03:00
yilsem 4037c65204 ci: install ripgrep for real search regressions
Server CI and Windows package / verify (push) Successful in 2m51s
Server CI and Windows package / windows-package (push) Successful in 6m14s
2026-10-13 16:35:24 +03:00
yilsem e7d3080e0a fix: preserve native model switches and bound project searches
Server CI and Windows package / verify (push) Failing after 1m40s
Server CI and Windows package / windows-package (push) Skipped
2026-10-13 16:32:50 +03:00
yilsem db5e876f40 fix(bridge): preserve plaintext peer task payloads for external models
Server CI and Windows package / verify (push) Successful in 3m16s
Server CI and Windows package / windows-package (push) Successful in 8m18s
2026-10-13 08:05:00 +03:00
yilsem ab7a721fbc Ship beta34 with external agent tasks, terminal SSE and live token limits
Server CI and Windows package / verify (push) Successful in 3m13s
Server CI and Windows package / windows-package (push) Successful in 7m56s
2026-10-13 07:32:57 +03:00
yilsem 2cca6a66a6 Preserve namespaced Codex tools across external model calls
Server CI and Windows package / verify (push) Successful in 2m11s
Server CI and Windows package / windows-package (push) Successful in 10m44s
2026-10-13 04:54:52 +03:00
yilsem a68d3e2272 Restart stale Codex bridge when installing compression settings
Server CI and Windows package / verify (push) Successful in 3m11s
Server CI and Windows package / windows-package (push) Successful in 7m1s
2026-10-13 04:14:01 +03:00
yilsem dfa762bcd2 Include compression defaults in Windows bridge resources
Server CI and Windows package / verify (push) Successful in 3m0s
Server CI and Windows package / windows-package (push) Canceled after 2m58s
2026-10-13 04:07:54 +03:00
yilsem 912752bdd3 Cap merged Codex Link model catalog at 1000 entries
Server CI and Windows package / verify (push) Successful in 2m59s
Server CI and Windows package / windows-package (push) Successful in 9m6s
2026-10-13 03:53:29 +03:00
yilsem 91ecad8e5c Preserve native Codex history and add configurable cross-model compression
Server CI and Windows package / verify (push) Successful in 4m47s
Server CI and Windows package / windows-package (push) Successful in 8m47s
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.
2026-10-13 03:36:42 +03:00
yilsem 1c6e3656b8 ci: verify combined Codex and Jev settings in Windows release
Server CI and Windows package / verify (push) Successful in 2m31s
Server CI and Windows package / windows-package (push) Successful in 14m14s
2026-10-13 01:24:40 +03:00
yilsem 883b1dc824 feat(settings): configure Jev evaluation and model transitions 2026-10-13 01:07:58 +03:00
yilsem 369f6c2ca8 fix(codex-link): isolate model transfers and widen local connection capacity
Server CI and Windows package / verify (push) Successful in 2m3s
Server CI and Windows package / windows-package (push) Successful in 6m58s
2026-10-13 00:35:08 +03:00
yilsem 2718b460ed fix(voice): keep watcher alive through quiet checks and reconnects
Server CI and Windows package / verify (push) Successful in 3m5s
Server CI and Windows package / windows-package (push) Successful in 13m2s
2026-10-12 20:06:46 +03:00
yilsem b4e8664115 Normalize native launcher source and test file endings
Server CI and Windows package / verify (push) Successful in 1m44s
Server CI and Windows package / windows-package (push) Successful in 9m44s
2026-10-12 19:28:37 +03:00
yilsem b66d9164ad Wire authenticated voice watch notices through an owned native launcher 2026-10-12 19:24:33 +03:00
yilsem eeee2756c7 release(desktop): beta29 with Sesli bridge version 9
Server CI and Windows package / verify (push) Successful in 42s
Server CI and Windows package / windows-package (push) Successful in 9m11s
2026-10-12 17:38:12 +03:00
81 changed files with 6398 additions and 164 deletions

No files matched your search

+18
View File
@@ -23,6 +23,9 @@ jobs:
run: |
set -eu
test "$(git rev-parse HEAD)" = "$GITHUB_SHA"
apt-get update
apt-get install -y --no-install-recommends ripgrep
rg --version
python3 -m venv "$RUNNER_TEMP/bootstrap"
"$RUNNER_TEMP/bootstrap/bin/pip" install uv
"$RUNNER_TEMP/bootstrap/bin/uv" venv --python 3.11 "$RUNNER_TEMP/ci-venv"
@@ -32,6 +35,12 @@ jobs:
"$RUNNER_TEMP/ci-venv/bin/python" -m pytest tests/tools/test_protected_instruction_unattended.py tests/tools/test_file_write_safety.py tests/tools/test_computer_use_approval_isolation.py tests/cli/test_cli_approval_ui.py tests/cli/test_single_query_session_finalize.py -q -p no:cacheprovider -rf
- name: Codex bridge regression tests
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m unittest discover -s apps/desktop/assets/codex-bridge'
- name: Managed Python update preservation
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest tests/test_install_ps1_reuse_healthy_venv.py tests/test_install_ps1_python_fallback_venv.py -q -p no:cacheprovider -rf'
- name: Project scoped and bounded search regressions
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest tests/tools/test_search_project_scope.py tests/tools/test_search_files_cpu_windows.py tests/tools/test_search_zero_match_and_multipath.py tests/tools/test_search_giant_line_containment.py tests/tools/test_search_budget_truncation.py -q -p no:cacheprovider -rf'
- name: Native watch and speech mux pytest regressions
run: '"$RUNNER_TEMP/ci-venv/bin/python" -m pytest apps/desktop/assets/codex-bridge/test_bekci_watch.py apps/desktop/assets/codex-bridge/test_native_speech_mux.py apps/desktop/assets/codex-bridge/test_live_limits.py -q -p no:cacheprovider -rf'
- name: Mobile and desktop startup regression tests
run: |
npm install --prefix "$RUNNER_TEMP/mobile-tests" --no-save --ignore-scripts --no-audit --no-fund --legacy-peer-deps vitest@4.1.10 ws@8.21.1
@@ -54,8 +63,17 @@ jobs:
github-server-url: https://gitea.turkservis.online
- name: Install locked source dependencies
run: npm ci --no-audit --no-fund
- name: Install Windows launcher cross compiler and framework reference
run: |
set -eu
apt-get update
apt-get install -y --no-install-recommends mono-mcs=6.8.0.105+dfsg-3.2 libmono-system-web-extensions4.0-cil=6.8.0.105+dfsg-3.2
- name: Compile Windows launcher from source and verify failure gate
run: node --test apps/desktop/scripts/build-native-watch-launcher.test.mjs
- 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 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
+21 -2
View File
@@ -18,6 +18,24 @@ 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.
`/models` içindeki doğrulanmış sayısal bağlam bilgisi, model adından çıkarılan
yerel varsayılandan önce gelir; gözlenmiş daha düşük bir gerçek sınır korunur.
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.
@@ -42,8 +60,9 @@ npm run builder --workspace=apps/desktop -- --win nsis --x64 --publish never
Dağıtılacak derleme temiz, yayımlanmış Git commit'inden üretilmelidir. İlk açılış
ajan kurulumunu paketteki `install-stamp.json` commit'ine sabitler. Windows
imzası ve temiz kurulum doğrulanmadan bir paket doğrulanmış üretim sürümü diye
sunulmamalıdır. macOS ve Linux paketleri bu beta kapsamında doğrulanmamıştır.
paketleri imzasız EXE/ZIP olarak yayımlanır; SHA-256 ve gerçek Windows çalıştırma
kontrolü gereklidir. Kullanıcı açıkça istemeden imzalama yapılmaz. macOS ve Linux
paketleri bu beta kapsamında doğrulanmamıştır.
## Lisans ve kaynak
+60
View File
@@ -8,6 +8,66 @@ path; broad diagnostic probes should apply this policy to recursive walks.
from __future__ import annotations
from pathlib import Path
SEARCH_RG_THREADS = 2
SEARCH_MAX_CONCURRENT = 2
_PROJECT_MARKERS = ('.git', '.hg', 'pyproject.toml', 'package.json',
'Cargo.toml', 'go.mod', 'CMakeLists.txt')
def _project_root(path):
"""Prefer the enclosing repository over nested monorepo packages."""
package = None
for parent in (path, *path.parents):
if (parent / '.git').exists() or (parent / '.hg').exists():
return parent
if package is None and any((parent / marker).exists() for marker in _PROJECT_MARKERS):
package = parent
return package
def local_search_scope(path, cwd, home):
"""Resolve broad local roots without starting a recursive search process.
Returns (root, notice, error). Only one level of candidate directories is
inspected; no repository-wide discovery is needed to reject an umbrella.
Real paths prevent symlink aliases from bypassing broad-root detection.
"""
root = Path(path)
if not root.is_absolute():
root = Path(cwd) / root
root = root.resolve()
if not root.is_dir():
return path, None, None
home = Path(home).resolve()
project = _project_root(Path(cwd).resolve())
root_project = _project_root(root)
if root_project is not None:
return path, None, None
candidates = []
try:
# Bound metadata work even in directories containing many entries.
from itertools import islice
for child in islice(root.iterdir(), 256):
if child.name in SEARCH_PRUNE_DIR_NAMES or not child.is_dir():
continue
if any((child / marker).exists() for marker in _PROJECT_MARKERS):
candidates.append(str(child))
except OSError:
pass
broad = (root == Path(root.anchor) or root == home or root in home.parents
or len(candidates) > 1 or (project is not None and root in project.parents))
if not broad:
return path, None, None
if project is not None and root in project.parents:
chosen = str(project)
return chosen, f'Search scoped to current project: {chosen}. Set allow_broad=true for the requested umbrella.', None
choices = ', '.join(sorted(candidates)[:12]) or 'choose an explicit project directory'
return path, None, ('Search scope needed: requested root spans projects or a home/drive. '
f'Candidates: {choices}. Set allow_broad=true only for an intentional broad search.')
# Keep this policy conservative and name-based so it works for local and remote
# shell backends alike. The same set is used by context discovery and search
@@ -0,0 +1,2 @@
# Generated by beforePack from NativeWatchBootstrap.cs; never commit opaque output.
/native-watch.exe
@@ -0,0 +1,136 @@
# Native watch launcher — reviewable Windows candidate
This is an owned startup hook for the original Codex engine, not another model
engine. Do not activate it until root has built a new packaged release and
reviewed the evidence. No environment variable is set by build/provision tools.
## Build and provision
Normal Windows `beforePack` invokes `scripts/build-native-watch-launcher.mjs`.
The resulting `native-watch.exe` is included in the existing codex-bridge
extraResources directory along with Python modules. Windows hosts use the .NET Framework C# compiler. The existing Linux/Wine
Windows-package job installs mono-mcs and libmono-system-web-extensions4.0-cil
6.8.0.105+dfsg-3.2 from its Ubuntu 22.04 apt repository and compiles with mcs.
Both hosts rebuild the same C# source; missing compilers or invalid source fail
packaging and remove stale output. The generated EXE is ignored by git.
This is a Windows artifact cross-build, not a Linux runtime launcher. The executable is a managed console bootstrap; use
an absolute managed **python.exe**, not pythonw.exe. It checks pinned hashes of
original binary, interpreter and launcher script. Original is checked again in
Python immediately before use. Parent directory ACLs and the selected official
binary hash are provisioning authority; this does not claim Authenticode trust.
After installing the new package, root provisions an owner-local candidate:
```powershell
py -3 prepare_native_watch.py --source <packaged-codex-bridge> --destination <owned-candidate-directory> --original <official-absolute-codex.exe> --expected-sha256 <independently-verified-official-sha256> --python <managed-python.exe> --bridge-home <bridge-data-directory> --hermes-home <existing-agent-home>
```
`prepare_native_watch.py` copies only the relevant resources and writes the
adjacent native-watch.json (paths/hashes only, no keys). It never reads
CODEX_CLI_PATH to locate the original and rejects its own executable as original.
Root alone may later point Desktop's CODEX_CLI_PATH at this EXE. No Python argv
are placed inside CODEX_CLI_PATH. No installed app.asar/binary modification.
Rollback is root removing its override to restore original app startup.
Installed app read-only inspection (OpenAI.Codex_26.1007.2314.0):
`.vite/build/application-network-startup-CfAQmSYg.js` tc()/Ks() constructs
`-c` pairs before/after `app-server`, `--analytics-default-enabled`. Recognizer
accepts that grammar plus explicit stdio forms, rejects daemon/websocket/unknown
commands and passes those unchanged to the original. Approvals flow untouched.
One original engine only; no extra version process at startup. Original cwd and
args preserved except one per-child `-c openai_base_url` selecting the in-process
bridge's ephemeral loopback `/backend-api/codex`. Normal original models and
public member catalog reuse kopru.H. Native environment is restored after bridge
import. Bridge logging uses stderr so stdout remains native JSONL only.
## Receipt, delivery and lifecycle
Before routing/rewrite, all three actual request values must agree:
session-id, thread-id and prompt_cache_key. The active mux connection ID and
realtime generation are captured then. Neither current tab nor notice data can
create that mapping. thread/unsubscribe, realtime stop/start/closed/error and EOF
invalidate an epoch. UI-only switches not observable in native protocol cannot
be detected here; root must verify Desktop emits stop/unsubscribe for its switch.
Concurrent active native threads have independent request mappings.
SSE `event: bekci.watch` and nonstream `_bekci_watch` are removed before native
model text, history observers, response rewriting and response preview logging.
Only `/v1/bekci/events` and the fixed `https://ai.turkservis.online` origin are
accepted. The same primary route public member key authenticates POST; redirects
are not followed. No backend keys, raw server tenant/session/history identifiers,
or reconstructed history exist in this client.
Only authenticated SSE `ready` binds the receipt. Binding's legacy tenant_id,
bekci_session_id, history_revision fields contain **local opaque scopes**, not
literal server claims: member-key fingerprint, key+public-subscription digest,
public-receipt revision digest. Server remains authoritative for hidden history.
TLS plus same-key proxy ready is the receipt validity attestation. SQLite also
locks receipt scope to its original connection/thread/generation across restarts.
SQLite WAL/FULL ledger commits `uncertain` before enqueue. Replays or a crash
between commit and append are suppressed, including possible lost delivery.
Cursor is persisted after durable admission/disposition; it is a local replay
ack, not a server consumption API or exactly-once guarantee. Native RPC acceptance
does not prove audio output. Replay IDs never cause automatic uncertain resend.
Keepalives, bounded reconnect attempts, receipt expiry, lifecycle abort and
simultaneous threads are handled. Receipt expiry invalidates its local binding.
No raw notice text or credentials are persisted in the ledger.
## Evidence and exact remaining operations
- `.claude/sesli-client-all-tests.log`: 20 files, 203 passed, 0 failed.
- `.claude/sesli-client-tests-red.log`: compiled Windows bootstrap lost stdout/
stderr before fix; interactive flush subsequently exposed by real native probe
and covered by `test_executable_flushes_interactive_stdin_before_eof`.
- `.claude/native-launcher-proof.json`: compiled candidate, pinned official native
initialize accepted, stdout JSON, EOF exit 0. No native voice call performed.
- `.claude/native-launcher-candidate/native-watch.exe`: concrete provisioned
candidate under worktree, not activated or installed.
Source now stamps desktop beta.30 and bridge 10. Root still must run
normal full package CI/build/signing policy, inspect packaged resources, provision the
new packaged candidate with trusted official SHA/managed runtime, activate only
authorized Desktop startup, verify thread-switch notifications and exercise a
real Sesli-Hizli-Zeki call through deployed server to audible appendSpeech. The bootstrap assigns itself to a non-inheritable KILL_ON_JOB_CLOSE Windows
Job Object before Python starts. Its descendants inherit membership; abrupt
bootstrap termination closes the handle and kills only that owned tree. Owned
fake and pinned original initialize/kill tests passed for both Windows-built and
Linux-built EXEs running on Windows. Actual Desktop UI termination remains a
manual acceptance step. Atomic generation enforcement inside native appendSpeech
is still unavailable (the native RPC has no expected-generation field).
No commit, push, deployment, installed runtime change or AITURK delegation.
## 2026-10-12 source verification update
- `.claude/native-watch-linux-build.log`: exact CI image digest
`sha256:b226b27b1a7e95ad25eb3d6414ea90c56c04eb6d8329967f4d721dce50b989ff`,
real apt dependencies, two compiler regressions passed, Windows PE produced.
- `.claude/native-watch-windows-build.log`: same two tests passed with Framework csc.
- `.claude/native-watch-linux-artifact-windows-tests.log`: 25 tests passed with
the cross-built artifact, including pinned original initialize and abrupt kill.
- `.claude/sesli-client-release-all-tests.log`: 220 passed, 21 files, zero failures.
- `.claude/electron-beta30-tests.log`: 47 passed including Python/Electron version alignment.
Receipt validation accepts up to 24 hours plus 60 seconds clock skew. Actual
expiry remains authoritative; expired, boolean, nonnumeric, NaN, infinite and
over-bound receipts are rejected. Long research has no local 15-minute limit.
Provision the exact rebuilt candidate with the explicit original SHA-256
`d13914ced6c7af174d8d638db938284231227312104b06a25e9608c632a7daec` only
when that is the selected independently verified official original. SHA-1 is
not the executable integrity pin; a Git source identity and an executable
SHA-256 have different roles. Fixed-origin TLS plus the same public member key
and authenticated ready event attest the receipt; none claims Authenticode.
No full installer build, signing, activation, or audible acceptance is claimed.
CI verify explicitly runs test_bekci_watch.py and test_native_speech_mux.py using
ci-venv pytest in addition to the unchanged unittest suite. Both focused files
pass on Linux without skips. Tests that execute native-watch.exe reside in the
explicitly Windows-only test_native_watch_cleanup.py; their owned-process proof
is Windows evidence, not Linux CI runtime coverage.
Sideband JSON extraction and SSE filtering apply only to the resolved exact
Sesli-Hizli-Zeki target, including when no watch owner is active. Other models
retain their original Jev/response pipeline without the sideband frame buffer.
After the final scope correction, rebuild/provision from final source; the older
local candidate hash proof does not include this last kopru.py change.
@@ -0,0 +1,89 @@
// Thin Windows CLI_PATH executable. Runtime and original are owner-provisioned,
// absolute, SHA-pinned; never consult PATH or CODEX_CLI_PATH.
using System;
using System.IO;
using System.Text;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Collections.Generic;
using System.Threading;
using System.Runtime.InteropServices;
using System.Web.Script.Serialization;
class NativeWatchBootstrap {
// Assign this bootstrap BEFORE creating Python. Descendants inherit job
// membership atomically; the non-inheritable handle belongs only to us.
// Windows closes it even on TerminateProcess, killing Python and native.
static IntPtr ownedJob;
[StructLayout(LayoutKind.Sequential)] struct BasicLimits {
public long ProcessTime, JobTime;
public uint Flags;
public UIntPtr MinWorkingSet, MaxWorkingSet;
public uint ActiveProcesses;
public UIntPtr Affinity;
public uint Priority, Scheduling;
}
[StructLayout(LayoutKind.Sequential)] struct IoCounters {
public ulong ReadOps, WriteOps, OtherOps, ReadBytes, WriteBytes, OtherBytes;
}
[StructLayout(LayoutKind.Sequential)] struct ExtendedLimits {
public BasicLimits Basic;
public IoCounters Io;
public UIntPtr ProcessMemory, JobMemory, PeakProcessMemory, PeakJobMemory;
}
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
static extern IntPtr CreateJobObject(IntPtr attributes, string name);
[DllImport("kernel32.dll", SetLastError=true)]
static extern bool SetInformationJobObject(IntPtr job, int kind, ref ExtendedLimits limits, uint size);
[DllImport("kernel32.dll", SetLastError=true)]
static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
static void OwnProcessTree() {
ownedJob=CreateJobObject(IntPtr.Zero,null);
var limits=new ExtendedLimits(); limits.Basic.Flags=0x2000; // KILL_ON_JOB_CLOSE
if(ownedJob==IntPtr.Zero || !SetInformationJobObject(ownedJob,9,ref limits,(uint)Marshal.SizeOf(typeof(ExtendedLimits)))
|| !AssignProcessToJobObject(ownedJob,Process.GetCurrentProcess().Handle)) throw new Exception();
// Do not close here or in finally: the bootstrap is itself a member.
// The OS closes this handle at exit, preserving our native exit code.
}
static string Quote(string s) {
var b=new StringBuilder("\""); int slashes=0;
foreach(char c in s) {
if(c=='\\') {slashes++; continue;}
if(c=='"') { b.Append('\\',slashes*2+1); b.Append(c); }
else {b.Append('\\',slashes); b.Append(c);}
slashes=0;
}
b.Append('\\',slashes*2); b.Append('"'); return b.ToString();
}
static string Pin(Dictionary<string,object> c,string field) {
string p=(string)c[field], expected=(string)c[field+"_sha256"];
if(!Path.IsPathRooted(p) || String.Equals(Path.GetFullPath(p),Process.GetCurrentProcess().MainModule.FileName,StringComparison.OrdinalIgnoreCase)) throw new Exception();
using(var f=File.OpenRead(p)) using(var sha=SHA256.Create()) {
string actual=BitConverter.ToString(sha.ComputeHash(f)).Replace("-","").ToLowerInvariant();
if(actual!=expected.ToLowerInvariant()) throw new Exception();
}
return p;
}
static void Pump(Stream source, Stream target) {
byte[] buffer=new byte[8192]; int count;
while((count=source.Read(buffer,0,buffer.Length))>0) { target.Write(buffer,0,count); target.Flush(); }
}
static int Main(string[] args) {
try {
string manifest=Path.ChangeExtension(Process.GetCurrentProcess().MainModule.FileName,".json");
var c=new JavaScriptSerializer().Deserialize<Dictionary<string,object>>(File.ReadAllText(manifest));
string original=Pin(c,"original");
string python=Pin(c,"python");
string script=Pin(c,"script");
OwnProcessTree();
var argv=new List<string>{"-u",script,manifest}; argv.AddRange(args);
var quoted=new List<string>(); foreach(var a in argv) quoted.Add(Quote(a));
var p=Process.Start(new ProcessStartInfo(python,String.Join(" ",quoted.ToArray())) { UseShellExecute=false, CreateNoWindow=true, RedirectStandardInput=true, RedirectStandardOutput=true, RedirectStandardError=true });
var input=new Thread(()=> { try { Pump(Console.OpenStandardInput(),p.StandardInput.BaseStream); p.StandardInput.Close(); } catch(IOException) {} });
input.IsBackground=true; input.Start();
var output=new Thread(()=>Pump(p.StandardOutput.BaseStream,Console.OpenStandardOutput()));
var error=new Thread(()=>Pump(p.StandardError.BaseStream,Console.OpenStandardError()));
output.Start(); error.Start();
p.WaitForExit(); output.Join(); error.Join(); return p.ExitCode;
} catch { Console.Error.WriteLine("Owned native launcher integrity/configuration failure."); return 78; }
}
}
@@ -27,6 +27,35 @@ Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE wr
Tests: `python -m unittest discover` in this directory.
## Version 11: isolated routes and cross-provider preparation
- Pure subscription chats and native voice retain their ChatGPT route. Added
models use the separate AITURK worker; model selection never globally changes
the provider for another chat.
- Cross-provider history repairs custom tool IDs and unwraps old AITURK summaries
before sending them to OpenAI. Real OpenAI encrypted compaction is interpreted
by OpenAI before transfer; unreadable context stops the transfer instead of
silently dropping it. The saved transcript is not rewritten.
- Old context above 70% of the target window is summarized through AITURK, with
instructions, tools, recent messages and complete tool pairs protected. Output
allowance is reserved. Unknown windows use a conservative guard. Summaries are
approximate; if protected content alone cannot fit, a visible error keeps the
source history intact. No middle-of-message truncation is used by this path.
- Preparation emits SSE keep-alive comments while it waits. The status version
matches the IDE, avoiding needless healthy-process restarts on reconnect.
- Both HTTP listeners accept a burst queue of 128 instead of Python 3.11's five;
disk logging and transfer records no longer share the request-counter lock.
This is local capacity, not a change to any provider's rate or account limits.
- The catalog cap is 1000; native entries retain their order, followed by combos
A–Z and provider/model A–Z. The official desktop picker owns submenus: this
bridge does not patch the Codex app or promise provider folders.
- Turning Codex Link off restores its owned config line. If that write fails,
the working bridge remains alive and the UI does not claim restoration.
Acceptance covers socket routing (including native voice endpoints), 50 concurrent
synthetic requests, tool-ID regression, context budgets and summary failures.
Microphone/audio and public installer acceptance are separate release checks.
## Version 8 (IDE beta.27): changing the model by saying so
Measured on a live voice chat (11 Oct 2026): a model picked on the phone reaches this machine only with a typed
@@ -0,0 +1,44 @@
# Codex sesli entegrasyon incelemesi — 12 Ekim 2026
## Uygulanan değişiklik ve doğrulama
Çözülmüş hedef tam `Sesli-Hizli-Zeki` ise nihai metin Jev'e gönderilmez. Büyük/küçük harf, boşluk, `ide/` öneki veya sonek normalizasyonu yapılmaz. `ide/` kullanıcı slug'ı önce mevcut mapping ile hedefe çözülür. Taşıma hatası, boş yanıt ve kopan akışın katman kurtarması korunur; yedek katmana geçince muafiyet taşınmaz. Araç/commentary hazırlık bypass'ı korunur. İstisna yanıt tamponlamasını değiştirmez.
`test_jev_denetim.py` exact hedefi, benzer isimleri ve fallback katmanının tekrar denetlenmesini çalıştırır. `test_denetim_ek.py` gerçek `_responses` metodu üzerinden başarılı yanıt, bağlantı kopması ve HTTP 400 yollarında çözülmüş hedef aktarımını, `instructions` ve developer mesajının korunmasını doğrular. Bunlar mevcut handler test düzeneğini kullanır; canlı sağlayıcı testi değildir.
Paket README'sindeki komutla `py -3 -m unittest discover`: **185 test, 25.305 saniye, OK**. Mevcut testlerde ResourceWarning ve beklenen yerel bağlantı reddi çıktısı vardı. İlk yeni test eski imzada hata verdi. Ayrıca muafiyet fonksiyonu geçici mock ile False yapıldığında yeni test gerçek davranış farkıyla başarısız oldu: özgün cevap yerine Katman-1 cevabı geldi (`MUTATION_DETECTED=True`).
## Gerçek Codex ses yolu
İncelenen istemci: WindowsApps `OpenAI.Codex_26.1007.2314.0_x64__2p2nqsd0c76g0`, `app/resources/app.asar`, salt okunur. CLI: `codex-cli 0.155.1`. `codex app-server generate-ts --experimental --out <geçici-dizin>` ile yerel binary protokolü üretildi.
Protokolün `ClientRequest.ts` dosyasında gerçek yöntemler:
- `thread/realtime/appendText`: `{threadId, text, role}`; role = user/developer/assistant. Girdi ekler; tek başına ses üretimini garanti ettiği iddia edilmez.
- `thread/realtime/appendSpeech`: `{threadId, text}`; açıklaması “append speakable text to thread realtime”.
- `thread/realtime/start`: thread kapsamlı oturum; websocket, webrtc veya existingCall taşıması. `clientManagedHandoffs` varsayılan false; automatic Codex response handoff seçenekleri var. Başka oturumun existingCall bağlantısı kullanılmadı.
- `turn/start`, `turn/steer`, `thread/queue/add`, `thread/queue/start` var. `turn/steer` aktif `expectedTurnId` ister; bitmiş tura uygulanacak genel uyandırma yolu değildir. `codex queue --thread ... --message ...` mevcut CLI yüzeyidir; kuyruğa koymanın ses çıkışıyla aynı şey olduğu doğrulanmadı.
İstemcinin `webview/assets/app-initial-b8e1c8014bb4.js` dosyasında `cra` ve `lra`, sırasıyla appendText/appendSpeech RPC'sini `manager.sendRequest` ile çağırıyor. Realtime runtime aynı yöntemleri dışarı sunuyor; durdurulmuş durumda no-op. **Somut arka plan tüketicisi:** kullanım limiti yaklaşma bildirimi geldiğinde istemci `appendSpeech` çağırıp uyarıyı sesli söylüyor (`[Composer] failed to speak realtime usage warning` hata kolu). Bu, kullanıcı yeni mesaj yazmadan konuşma istemenin mevcut istemci mekanizması olduğunu kanıtlar. Bekçi düzeltmesinin canlı ses çıkışı test edilmiş değildir.
`CompilerIntrinsics-87ea46a69911.js` içinde WebRTC data channel üzerinden `conversation.item.create` gönderimi de var. Aynı aramada `response.created` olay işleyicisi görüldü; bunu `response.create` gönderim kanıtı saymıyoruz. Ham Realtime protokolü icat etmek yerine mevcut thread realtime RPC'si entegrasyon adayıdır.
## Köprüdeki somut eksik
`bridge.py:H._isle` yalnız `/responses` ve model listesini alt köprüye yollar; diğer yollar, `/realtime/calls` dahil, doğrudan ChatGPT'ye gider. WebSocket upgrade 426 ile reddedilir. Dolayısıyla `kopru.py:H._passthrough` içindeki isteğe bağlı `voice_prep.baslat(Location, headers, ...)` kancası standart ayırıcı topolojisinde çağrı oluşturmayı göremez.
`voice_prep.py` ve `ses_tuneli.py` kaynak worktree ve kurulu AITURK resources/codex-bridge içinde yok. Yerel aramada `Temp/oldb/voice_prep.py` yedek kopyası bulundu ve yalnız okundu: `Hazirlayici` aynı call'a `wss://api.openai.com/v1/live/` sideband açıyor; `session.context.append` ile bağlam ekliyor. Bu eski dosyada response.create/conversation.item.create gönderimi bulunmadı. Yedek dosya kurulu/aktif bileşen kanıtı değildir; çalıştırılmadı veya kopyalanmadı. `ses_tuneli` bulunamadı.
## Bekçi entegrasyonunda kalanlar
Diğer ajanın auth + session + cursor notices SSE sözleşmesi henüz bu çalışmaya ulaşmadı. Sözleşme sonrası bildirimin doğrulanmış session kimliğini doğru Codex host/thread ve canlı realtime ömrüne bağlamak gerekiyor. SSE yeniden bağlanma/cursor tekrarlarında aynı düzeltmeyi iki kez seslendirmeme, bitmiş ses oturumuna göndermeme ve yeni kullanıcı konuşmasıyla çakışmayı ele alma davranışları kabul testine dahil edilmeli. Bunlar uygulanmış sözleşme değildir.
Yalnız bize ait deney oturumunda, ilk ses cevabı bittikten sonra bir notice üretilip mevcut app-server yetkili bağlantısından `thread/realtime/appendSpeech` ile düzeltme konuşmasının başlaması; başka thread'in etkilenmemesi; yeni kullanıcı mesajı gerekmemesi gözlenmeli. Bu canlı test yapılmadan “entegrasyon tamamlandı” denemez. Hermes masaüstü TTS/voice hook'ları bu Codex kanalının yerine kullanılmadı. İmzalı istemciye API eklenmedi.
## Kokla / Codex alt ajan yolu
Mevcut yerel `C:/Users/maste/.codex/skills/codex-kokla-ajan/SKILL.md` açıkça `ide/GPT-6-Astra` CLI slug'ını, `GPT-6-Astra` OmniRoute combo kimliğini belirtiyor. `gpt-6-astra` tek modelini combo yerine kullanmayı yasaklıyor. Burada katalog yeniden sorgulanmadı; bilgi skill'in kaydıdır.
Mevcut CLI yolu: `codex exec --model 'ide/GPT-6-Astra' --color never -C '<ayrı-worktree>' -o '<sonuç>' '<görev>'`. `--json` süreç olaylarını, `exec resume` düzeltme devamını destekliyor. Skill ana ajana süreç kimliği, hedef, kalanlar, sonuç dosyası kaydı; çıktı takibi; bağımsız diff/test kontrolü; sıkıştırma sonrası kayıt ve kanıt okuma sorumluluğu veriyor. Bu görevde yeni bir CLI alt ajanı başlatılmadı. Runtime'ın sunulan `spawn_agent` aracı çağrısı `unsupported call: spawn_agent` döndü; bu nedenle kullanılabilir canlı alt ajan oluşturduğu iddia edilmez.
`C:/Users/maste/.codex/hooks/codex_continuity.py` save işlemi session/cwd kapsamlı; PreCompact yedekliyor, SessionStart/UserPromptSubmit geri yüklüyor, Stop aktif kayıtta devam uyarısı veriyor. Tamamlanma için kalan iş olmaması ve kanıt dosyası şartı var. Bu kayıt bağımsız test yerine geçmez. Runtime plugin CLI add/list/remove sağlar; istemcinin özel JS fonksiyonlarına genel plugin erişimi kanıtlanmadı. AITURK IDE devri yapılmadı.
@@ -0,0 +1,210 @@
"""Public Omni receipts. All scopes here are LOCAL opaque identities, never backend IDs."""
import hashlib
import http.client
import json
import socket
import sqlite3
import ssl
import threading
import time
from urllib.parse import urlsplit
from native_speech_mux import Binding, WatchReceiptAdapter
EVENTS_PATH = '/v1/bekci/events'
TRUSTED_ORIGIN = 'https://ai.turkservis.online'
# Public protocol lifetime, with bounded server/client clock skew. The server's
# expiry remains the watch deadline; there is no shorter research timeout.
MAX_RECEIPT_TTL = 24 * 60 * 60
RECEIPT_CLOCK_SKEW = 60
def opaque(*parts):
return hashlib.sha256('\0'.join(parts).encode()).hexdigest()
class Frames:
def __init__(self):
self.buf = b''
def feed(self, chunk):
self.buf += chunk
if len(self.buf) > 262144:
raise ValueError('SSE frame too large')
while True:
# Normalize only complete CRLF, including a CR split across reads.
self.buf = self.buf.replace(b'\r\n', b'\n')
if b'\n\n' not in self.buf:
return
frame, self.buf = self.buf.split(b'\n\n', 1)
event, data, cursor = '', [], None
for line in frame.decode('utf-8').split('\n'):
field, _, value = line.partition(':')
value = value.removeprefix(' ')
if field == 'event': event = value
elif field == 'data': data.append(value)
elif field == 'id': cursor = value
yield event, '\n'.join(data), cursor, frame + b'\n\n'
class MetadataFilter:
"""Strip sideband before rewrites, logging or transcript observers."""
def __init__(self, accept):
self.frames, self.accept = Frames(), accept
def feed(self, chunk):
out = []
for event, data, _, raw in self.frames.feed(chunk):
if event == 'bekci.watch':
try: self.accept(json.loads(data))
except (ValueError, TypeError): pass
else: out.append(raw)
return b''.join(out)
def flush(self):
# An incomplete sideband frame must never escape into model text.
raw, self.frames.buf = self.frames.buf, b''
return b'' if b'bekci.watch' in raw else raw
class Ledger:
"""Commit uncertain BEFORE enqueue. Crash ambiguity sacrifices delivery, never resends."""
def __init__(self, path):
self.db = sqlite3.connect(path, check_same_thread=False)
self.lock = threading.Lock()
self.db.execute('PRAGMA journal_mode=WAL')
self.db.execute('PRAGMA synchronous=FULL')
self.db.executescript('CREATE TABLE IF NOT EXISTS delivery(scope TEXT,id TEXT,status TEXT,PRIMARY KEY(scope,id)); CREATE TABLE IF NOT EXISTS cursors(scope TEXT PRIMARY KEY,cursor TEXT); CREATE TABLE IF NOT EXISTS bindings(scope TEXT PRIMARY KEY,epoch TEXT);')
def bind_epoch(self, scope, correlation):
epoch = opaque(correlation.connection_id,correlation.thread_id,str(correlation.generation))
with self.lock, self.db:
self.db.execute('INSERT OR IGNORE INTO bindings VALUES(?,?)',(scope,epoch))
return self.db.execute('SELECT epoch FROM bindings WHERE scope=?',(scope,)).fetchone()[0] == epoch
def claim(self, scope, nid):
with self.lock, self.db:
return self.db.execute('INSERT OR IGNORE INTO delivery VALUES(?,?,?)', (scope,nid,'uncertain')).rowcount == 1
def ack(self, scope, nid, cursor, status):
with self.lock, self.db:
self.db.execute('UPDATE delivery SET status=? WHERE scope=? AND id=?', (status,scope,nid))
self.db.execute('INSERT OR REPLACE INTO cursors VALUES(?,?)', (scope,cursor))
def cursor(self, scope):
with self.lock:
row = self.db.execute('SELECT cursor FROM cursors WHERE scope=?',(scope,)).fetchone()
return row[0] if row else None
class WatchOwner:
def __init__(self, mux, ledger, *, origin=TRUSTED_ORIGIN, test_loopback=False):
u = urlsplit(origin)
if origin != TRUSTED_ORIGIN and not (test_loopback and u.scheme == 'http' and u.hostname == '127.0.0.1'):
raise ValueError('untrusted Omni origin')
self.mux, self.ledger, self.origin = mux, ledger, u
self.adapter = WatchReceiptAdapter(mux)
self.lock, self.watches = threading.Lock(), {}
def capture(self, headers, body):
return self.adapter.capture(headers, body)
def live(self, c):
s = self.mux.snapshot(); t = s['threads'].get(c.thread_id, {})
return (not s['closed'] and not s['injection_disabled'] and s['connection_id'] == c.connection_id
and t.get('active') and t.get('generation') == c.generation)
def accept(self, payload, correlation, key, route_url):
if not correlation or not self.live(correlation): return 'stale_generation'
route = urlsplit(route_url)
if (route.scheme,route.netloc) != (self.origin.scheme,self.origin.netloc) or not key: return 'untrusted_route'
if (not isinstance(payload,dict) or set(payload) != {'subscription','events_url','expires_at'}
or payload['events_url'] != EVENTS_PATH or not isinstance(payload['subscription'],str)
or not 20 <= len(payload['subscription']) <= 128 or type(payload['expires_at']) not in (int,float)
or not time.time()*1000 < payload['expires_at'] <= (time.time()+MAX_RECEIPT_TTL+RECEIPT_CLOCK_SKEW)*1000): return 'invalid_receipt'
scope = opaque('public-watch',key,payload['subscription'])
if not self.ledger.bind_epoch(scope,correlation): return 'receipt_replay_mismatch'
with self.lock:
if scope in self.watches: return 'already_watching'
if len(self.watches) >= 256: return 'capacity_exhausted'
stop = threading.Event()
self.watches[scope] = stop
threading.Thread(target=self._watch,args=(dict(payload),correlation,key,scope,stop),daemon=True).start()
return 'subscribing'
def close(self):
with self.lock:
for stop in self.watches.values(): stop.set()
def _watch(self, p, c, key, scope, stop):
# These are local labels only. TLS+same-key authenticated ready attests
# receipt validity; server owns literal tenant/session/history mapping.
binding = Binding(opaque('member',key),scope,opaque('history-receipt',p['subscription']),c.connection_id,c.thread_id,c.generation)
deadline = p['expires_at']/1000
try:
backoff = 1
while not stop.is_set() and time.time() < deadline and self.live(c):
conn = (http.client.HTTPSConnection(self.origin.hostname,self.origin.port,context=ssl.create_default_context(),timeout=10)
if self.origin.scheme == 'https' else http.client.HTTPConnection(self.origin.hostname,self.origin.port,timeout=10))
finished = threading.Event()
transport, resp, guard_thread = None, None, None
def interrupt_transport():
# getresponse may detach conn.sock for HTTP/1.0 or
# Connection: close. Keep our actual socket until cleanup.
if transport is not None:
try: transport.shutdown(socket.SHUT_RDWR)
except OSError: pass
def guard():
while not finished.wait(.05):
if stop.is_set() or time.time() >= deadline or not self.live(c):
stop.set()
interrupt_transport()
return
try:
# Bound TCP/TLS connection establishment separately from
# headers/stream reads: quiet research has no idle deadline.
conn.connect()
transport = conn.sock
transport.settimeout(None)
if stop.is_set() or time.time() >= deadline or not self.live(c): break
guard_thread = threading.Thread(target=guard,daemon=True)
guard_thread.start()
body = {'subscription':p['subscription']}
cursor = self.ledger.cursor(scope)
if cursor: body['cursor'] = cursor
conn.request('POST', EVENTS_PATH, json.dumps(body), {'Authorization':'Bearer '+key,'Content-Type':'application/json'})
resp = conn.getresponse()
if resp.status != 200:
if resp.status not in (408,429) and not 500 <= resp.status < 600: break
elif 'text/event-stream' not in resp.getheader('Content-Type',''):
break
else:
parser, ready = Frames(), False
while self.live(c) and not stop.is_set() and time.time() < deadline:
chunk = resp.read1(4096)
if not chunk: break
for event,data,event_id,_ in parser.feed(chunk):
if stop.is_set() or time.time() >= deadline or not self.live(c): break
if not data: continue # keepalive
value = json.loads(data)
if event == 'ready':
if not isinstance(value,dict) or not isinstance(value.get('cursor'),str): raise ValueError('invalid ready')
ready = self.live(c) and self.mux.bind(binding)
if not ready: return
elif event == 'notice':
if not ready: raise ValueError('notice before ready')
self._notice(binding,scope,value,event_id,deadline,c)
except (OSError,ValueError,http.client.HTTPException):
pass # no credential-bearing exceptions in logs
finally:
finished.set()
interrupt_transport()
if guard_thread is not None: guard_thread.join()
if resp is not None: resp.close()
conn.close()
if transport is not None: transport.close()
if stop.is_set() or time.time() >= deadline or not self.live(c): break
if stop.wait(min(backoff,max(0,deadline-time.time()))): break
backoff = min(backoff*2,15)
finally:
with self.mux._lock:
k = (binding.tenant_id,binding.bekci_session_id)
if self.mux._bindings.get(k) == binding: self.mux._bindings.pop(k,None)
with self.lock: self.watches.pop(scope,None)
def _notice(self,b,scope,value,event_id,deadline,c):
if not self.live(c) or not isinstance(value,dict): return
n, cursor = value.get('notice'), value.get('cursor')
if (not isinstance(n,dict) or not isinstance(cursor,str) or not 0 < len(cursor) <= 512
or event_id != cursor or not isinstance(n.get('id'),str) or not 0 < len(n['id']) <= 256
or n.get('type') not in ('duzeltme','arastirma') or not isinstance(n.get('metin'),str)
or not 0 < len(n['metin']) <= 8000): return
if not self.ledger.claim(scope,n['id']):
self.ledger.ack(scope,n['id'],cursor,'replay_suppressed'); return
now = time.time()
notice = dict(notice_id=n['id'],tenant_id=b.tenant_id,bekci_session_id=b.bekci_session_id,
history_revision=b.history_revision,connection_id=b.connection_id,thread_id=b.thread_id,
expected_generation=b.generation,issued_at=now,expires_at=min(now+60,deadline),text=n['metin'])
result = self.mux.submit(notice)
self.ledger.ack(scope,n['id'],cursor,result['status'])
+241 -16
View File
@@ -1,8 +1,8 @@
"""AITURK IDE "Codex Baglama" ayiricisi.
Kullanici karari (11 Ekim 2026): "Codex'in modelleri bir tarafta. Bizim sonradan eklediklerimiz diger tarafta.
Bizim sonradan eklediklerimiz kopru kullaniyor. Codex'in modelleri kopru kullanmiyor." ve "Codex'i secip Codex'e
yaziyorsan bizim kopru yardim amacli bile yaninda olamaz."
13 Ekim 2026 aciklamasi: abonelik ve ek modellerin saglayici yollari ayri
kalir; gecis gecmisinin hazirlanmasi yasak degildir. Ses Codex'in yerlisik
ses hattinda kalir. Saf abonelik sohbeti degistirilmez.
Codex'te tek adres ayari (`openai_base_url`) oldugu icin butun istekler buraya gelir. Bu dosya YALNIZ ayirir:
@@ -12,8 +12,9 @@ Codex'te tek adres ayari (`openai_base_url`) oldugu icin butun istekler buraya g
* Model listesi -> kopru ayaktaysa ek modelleri eklenmis liste; degilse chatgpt.com'un listesi.
* Geri kalan her sey -> chatgpt.com'a, oldugu gibi.
BU DOSYAYA OZELLIK EKLENMEZ. Sikistirma, not, talimat, model komutu, gecmis duzeltme: hepsi kopru.py'dedir ve yalniz
ek modeller icindir. Kopru cokerse, guncellenirse ya da yeniden baslatilirsa Codex'in kendi modelleri etkilenmez.
Karisik gecmiste yerel aktarim hazirligi inference'dan ayridir. Abonelik
inference'i her zaman CHATGPT'ye; ek model inference'i KOPRU'ye gider.
Saf abonelik sohbetleri ve ses, ozet servisinin durumundan etkilenmez.
Veri klasoru (bridge.json, log/): --home <klasor> ya da AITURK_CODEX_BRIDGE_HOME. Ajan klasoru: --hermes-home.
Yerel denetim: GET /aiturk/codex-baglama/status, POST .../stop, POST .../refresh, POST .../kopru-yenile.
@@ -30,9 +31,11 @@ import threading
import time
import urllib.error
import urllib.request
import gecis_context
import terminal_stream
HERE = os.path.dirname(os.path.abspath(__file__))
SURUM = "8" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
SURUM = "18" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
try:
with open(os.path.abspath(__file__), "rb") as _f:
KOD = hashlib.sha256(_f.read()).hexdigest()[:8]
@@ -95,14 +98,43 @@ def _zstd_hazir():
ZSTD_OK = _zstd_hazir()
kilit = threading.Lock()
log_kilit = threading.Lock()
transfer_kilit = threading.Lock()
sira = [0]
# Store only hashed conversation keys and the last external model, not messages
# or credentials. Survives restarts so returning long chats keep their guard.
TRANSFER_PATH = os.path.join(HOME, "gecis-sohbetler.json")
try:
with open(TRANSFER_PATH, encoding="utf-8") as f:
TRANSFERS = json.load(f)
if not isinstance(TRANSFERS, dict):
TRANSFERS = {}
except (OSError, ValueError):
TRANSFERS = {}
def remember_transfer(scope, model):
with transfer_kilit:
if TRANSFERS.get(scope) == model:
return
TRANSFERS[scope] = model
while len(TRANSFERS) > 2048:
del TRANSFERS[next(iter(TRANSFERS))]
try:
os.makedirs(HOME, exist_ok=True)
with open(TRANSFER_PATH + ".tmp", "w", encoding="utf-8") as f:
json.dump(TRANSFERS, f)
os.replace(TRANSFER_PATH + ".tmp", TRANSFER_PATH)
except OSError:
pass # in-memory guard remains active
def log(msg):
satir = time.strftime("%Y-%m-%d %H:%M:%S ") + msg
try:
os.makedirs(LOGDIR, exist_ok=True)
with kilit:
with log_kilit:
with open(os.path.join(LOGDIR, "ayirici.log"), "a", encoding="utf-8") as f:
f.write(satir + "\n")
except OSError:
@@ -254,6 +286,8 @@ class H(http.server.BaseHTTPRequestHandler):
govde if self.command == "POST" else None)
if kod == 200 and isinstance(veri, dict):
veri.update({"ayirici": True, "ayirici_kod": KOD, "kopru": "calisiyor"})
# IDE compares the splitter, not the older worker's version.
veri["surum"] = SURUM
veri["ok"] = bool(veri.get("ok")) and ZSTD_OK
return self._json(200, veri)
return self._json(200, {"ok": ZSTD_OK, "surum": SURUM, "kod": KOD, "zstd": ZSTD_OK, "ide_durum": "kapali",
@@ -261,12 +295,14 @@ class H(http.server.BaseHTTPRequestHandler):
"ayirici_kod": KOD, "kopru": "kapali"})
return self._json(404, {"error": {"message": "unknown control path"}})
def _ilet(self, n, hedef, yol, govde, etiket, yedekli=False, sessiz=False):
def _ilet(self, n, hedef, yol, govde, etiket, yedekli=False, sessiz=False, rewritten=False, started=False):
"""Istegi hedefe OLDUGU GIBI iletir, cevabi geldigi gibi (akarak) geri verir.
yedekli=True: cevap basliklari gelmeden bir sorun olursa hicbir sey gonderilmez ve False doner (cagiran baska
hedefi dener). Diger durumlarda True doner."""
tag = f"[{n} {etiket} {self.command} {yol[:70]}]"
basliklar = {k: v for k, v in self.headers.items() if k.lower() not in ISTEK_ATLA}
if rewritten:
basliklar = {k: v for k, v in basliklar.items() if k.lower() != "content-encoding"}
if self.command == "GET" and "/models" in yol:
# Liste her seferinde tam gelsin: istemcinin elindeki onbellek damgasi iki tarafin listesini karistirir.
basliklar = {k: v for k, v in basliklar.items() if k.lower() not in ("if-none-match", "if-modified-since")}
@@ -280,6 +316,9 @@ class H(http.server.BaseHTTPRequestHandler):
if yedekli:
return False
log(f"{tag} HATA {type(e).__name__}: {e}")
if started:
self._transfer_fail("Model bağlantısı kurulamadı; sohbet geçmişi korundu.")
return True
if hedef == KOPRU:
self._json(503, {"error": {"message": "AITURK köprüsü şu an çalışmıyor; birkaç saniye sonra yeniden deneyin "
"ya da ekrandaki seçiciden Codex'in kendi modelini seçin."}})
@@ -295,6 +334,13 @@ class H(http.server.BaseHTTPRequestHandler):
if durum >= 400:
hata = yanit.read()
log(f"{tag} {durum}: {hata[:300]!r}")
if started:
try:
message = json.loads(hata).get("error", {}).get("message", "Model isteği reddedildi.")
except (ValueError, AttributeError):
message = "Model isteği reddedildi (HTTP %d)." % durum
self._transfer_fail(message)
return True
self.send_response(durum)
for k, v in yanit.headers.items():
if k.lower() not in YANIT_ATLA:
@@ -303,14 +349,51 @@ class H(http.server.BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(hata)
return True
self.send_response(durum)
for k, v in yanit.headers.items():
if k.lower() not in YANIT_ATLA:
self.send_header(k, v)
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
observer = None
first_chunks = []
failure_key = getattr(self, "_failure_key", None)
if (hedef == KOPRU and failure_key is not None and durum == 200
and "text/event-stream" in yanit.headers.get("Content-Type", "")
and yanit.headers.get("Content-Encoding", "identity") == "identity"):
observer = terminal_stream.Observer()
if not started:
# Inspect only until the first SSE frame/comment. Keep normal
# streaming and heartbeats; an error-only response can still
# return a truthful HTTP failure before headers are committed.
try:
while True:
first = yanit.read1(4096)
if not first:
break
first_chunks.append(first)
observer.feed(first)
if observer.error and not observer.payload_seen:
yanit.close()
self._json(400, terminal_stream.error_body(observer.error))
return True
if observer.comment_seen or observer.payload_seen or sum(map(len, first_chunks)) >= observer.LIMIT:
break
except (OSError, http.client.HTTPException):
# A transport interruption without an explicit provider
# failure keeps the client's existing recovery behavior.
yanit.close()
self.close_connection = True
return True
if not started:
self.send_response(durum)
for k, v in yanit.headers.items():
if k.lower() not in YANIT_ATLA:
self.send_header(k, v)
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
toplam = 0
event_tail = b""
completed = False
try:
for first in first_chunks:
toplam += len(first)
self.wfile.write(b"%x\r\n" % len(first) + first + b"\r\n")
self.wfile.flush()
while True:
try:
parca = yanit.read1(4096) # gelen kadarini hemen verir; bekletmez
@@ -322,8 +405,23 @@ class H(http.server.BaseHTTPRequestHandler):
if not parca:
break
toplam += len(parca)
if observer is not None:
observer.feed(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()
if observer is not None and observer.error and not observer.completed:
self.server.failure_replays.remember(failure_key, observer.error)
log(f"{tag} terminal worker error; identical automatic replay will return HTTP 400")
self.wfile.write(b"0\r\n\r\n")
self.wfile.flush()
except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError):
@@ -332,9 +430,70 @@ 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):
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
def _transfer_chunk(self, body):
self.wfile.write(b"%x\r\n" % len(body) + body + b"\r\n")
self.wfile.flush()
def _transfer_fail(self, message):
event = {"type": "response.failed", "response": {"id": "resp_aiturk_transfer", "status": "failed",
"output": [], "error": {"code": "aiturk_transfer_failed", "message": message}}}
self._transfer_chunk(b"event: response.failed\ndata: " + json.dumps(event, ensure_ascii=False).encode() + b"\n\n")
self.wfile.write(b"0\r\n\r\n")
self.wfile.flush()
def _prepare(self, req, source_model):
"""Preparation is separate from inference. ChatGPT auth never goes to AITURK upstream.
The local worker needs subscription auth only to render opaque OpenAI
summaries through their owner. It returns history, never a task answer.
"""
done, result = threading.Event(), {}
def run():
try:
headers = {"Content-Type": "application/json", "X-AITURK-Transfer": "1"}
for key in ("Authorization", "chatgpt-account-id", "originator"):
if self.headers.get(key):
headers[key] = self.headers[key]
data = json.dumps({"request": req, "source_model": source_model}, ensure_ascii=False).encode()
request = urllib.request.Request(KOPRU + "/aiturk/codex-baglama/prepare-transfer", data=data, headers=headers)
with urllib.request.urlopen(request, timeout=BEKLEME) as response:
result["value"] = json.load(response)
except urllib.error.HTTPError as e:
try:
result["error"] = json.loads(e.read())["error"]["message"]
except Exception:
result["error"] = "Geçiş hazırlığı başarısız; geçmiş korundu."
except Exception:
result["error"] = "AITURK geçiş hazırlığına ulaşılamadı; geçmiş korundu."
finally:
done.set()
threading.Thread(target=run, daemon=True).start()
started = not done.wait(0.2)
if started:
self._transfer_begin()
self._transfer_chunk(b": AITURK context preparation\n\n")
while not done.wait(10):
self._transfer_chunk(b": AITURK context preparation\n\n")
if "error" in result:
if not started:
self._transfer_begin()
self._transfer_fail(result["error"])
return None, True
return result["value"], started
def _isle(self):
self._native_return_scope = None
self._failure_key = None
with kilit:
sira[0] += 1
n = sira[0]
@@ -352,8 +511,67 @@ class H(http.server.BaseHTTPRequestHandler):
son = yol.split("?", 1)[0].rstrip("/")
if self.command == "POST" and son.endswith("/responses"):
model = model_oku(govde, self.headers.get("Content-Encoding"))
if ek_model_mu(model):
return self._ilet(n, KOPRU, yol, govde, f"KOPRU {model}", sessiz=True)
external = ek_model_mu(model)
try:
encoding = (self.headers.get("Content-Encoding") or "").lower()
raw = _zstd_ac(govde) if encoding == "zstd" else gzip.decompress(govde) if encoding == "gzip" else govde
req = json.loads(raw)
if not isinstance(req, dict):
req = {}
except Exception:
req = {}
if external:
self._failure_key = terminal_stream.request_key(req or {"raw": govde.hex()}, self.headers)
previous_failure = self.server.failure_replays.take(self._failure_key)
if previous_failure:
log(f"[{n} {model}] terminal worker error replay stopped before upstream request")
return self._json(400, terminal_stream.error_body(previous_failure))
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
# 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_history(req))
# Format repair is stateless: the saved transcript can still contain
# foreign wrappers after a successful native return (or a restart).
if external or mixed or gecis_context.foreign(req):
try:
normalized = gecis_context.normalize(req)
except gecis_context.TransferError as e:
self._transfer_begin()
return self._transfer_fail(str(e))
# Small returning requests need no summarizer or local worker.
# Native opaque state must remain on its owner route. Its byte
# size is not a semantic token budget and cannot trigger a
# foreign summarizer, including when transfer state is cold.
if external or (mixed and not gecis_context.native_opaque(normalized)
and gecis_context.tokens(normalized) > int(32768 * 0.70)):
prepared, started = self._prepare(normalized, source)
if prepared is None:
return
if "compaction" in prepared or "compaction_items" in prepared:
if not started:
self._transfer_begin()
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()
return
normalized = prepared["request"]
else:
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.
@@ -370,7 +588,14 @@ class H(http.server.BaseHTTPRequestHandler):
class Sunucu(http.server.ThreadingHTTPServer):
def __init__(self, *args, **kwargs):
self.failure_replays = terminal_stream.FailureReplays()
super().__init__(*args, **kwargs)
daemon_threads = True
# Shipped Python 3.11 defaults to only five pending connections. Model
# discovery, voice and simultaneous chats can arrive in the same burst.
request_queue_size = 128
allow_reuse_address = False # kapi doluysa sessizce yan yana dinlemek yerine acikca hata
@@ -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
}
@@ -0,0 +1,231 @@
"""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<gecmis>\n' + chunk + '\n</gecmis>')
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 ''
if not external and gc.native_opaque(original):
# Defense in depth for direct/older splitter callers. Native encrypted
# state never enters owner decoding or cross-provider summarization.
return {'request': gc.normalize(original)}
# 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_history(original):
return {'request': gc.normalize(original)}
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)
@@ -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)
@@ -0,0 +1,258 @@
"""Provider-neutral history preparation. Never edits the saved Codex transcript.
Inference routing and voice are not responsibilities of this module. A caller
supplies the target window and an AITURK summarizer; subscription-only requests
must not enter the budget preparation path.
"""
import copy
import hashlib
import json
import re
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": "fco"}
RATIO = 0.70
class TransferError(ValueError):
pass
def encoded(value):
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def tokens(value):
# Deliberately more conservative than the old 3.2 bytes/token estimate.
return (len(encoded(value).encode("utf-8")) + 1) // 2
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):
for item in req.get("input") or []:
if not isinstance(item, dict):
continue
identity = str(item.get("id") or "")
want = PREFIXES.get(item.get("type"))
if ("chatcmpl-" in identity or "astrabridge" in identity
or str(item.get("encrypted_content") or "").startswith(ozet_kopru.ISARET)
or (want and identity and not identity.startswith(want + "_"))):
return True
return False
def native_opaque(req):
"""Native encrypted state belongs to the subscription, even in mixed history."""
return any(isinstance(item, dict) and item.get("type") == "compaction" and item.get("encrypted_content")
and not str(item["encrypted_content"]).startswith(ozet_kopru.ISARET)
for item in req.get("input") or [])
def foreign_history(req):
"""Semantic migration evidence, excluding tool ID format mismatches."""
return any(isinstance(item, dict) and (
"chatcmpl-" in str(item.get("id") or "")
or "astrabridge" in str(item.get("id") or "")
or str(item.get("encrypted_content") or "").startswith(ozet_kopru.ISARET))
for item in req.get("input") or [])
def normalize(req):
"""Repair local wrappers/IDs, retaining real OpenAI encrypted compaction.
Only foreign server references (not inline tool results) are removed. Their
visible summary/text, when supplied, is retained as ordinary context.
"""
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):
result.append(item)
continue
encrypted = item.get("encrypted_content")
if item.get("type") == "compaction" and isinstance(encrypted, str) and encrypted.startswith(ozet_kopru.ISARET):
text = ozet_kopru.ozet_metni(item)
if not text:
raise TransferError("Eski AITURK özeti okunamadı; geçmiş değiştirilmedi.")
result.append(ozet_kopru._ozet_mesaji(text))
continue
identity = item.get("id")
if "chatcmpl-" in str(identity) and item.get("type") in ("reasoning", "item_reference"):
visible = [p.get("text", "") for p in item.get("summary", []) if isinstance(p, dict)]
if any(visible):
result.append(ozet_kopru._ozet_mesaji("\n".join(visible)))
continue
if ("chatcmpl-" in str(identity) or "astrabridge" in str(identity)) and (
item.get("type") == "message" or item.get("role")):
# Inline message content is portable; its foreign server ID is not.
item.pop("id", None)
prefix = PREFIXES.get(item.get("type"))
if prefix and isinstance(identity, str) and identity and not identity.startswith(prefix + "_"):
item["id"] = prefix + "_" + identity.split("_", 1)[-1]
result.append(item)
out["input"] = result
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 isinstance(item, dict) and item.get("type") == "agent_message":
# Peer messages carry delegated tasks/results. External Responses
# translators do not understand this Codex-specific item type.
# Keep provenance as data, never as system/developer authority.
content = item.get("content") or []
if isinstance(content, str):
content = [{"type": "input_text", "text": content}]
portable = []
for part in content:
if isinstance(part, dict) and part.get("type") == "encrypted_content":
# Codex peer delivery can put the literal task/result in
# this wrapper. It is distinct from encrypted reasoning.
# External translators otherwise silently drop the task.
text = part.get("encrypted_content")
if (not isinstance(text, str) or text.startswith("gAAAA")
or re.fullmatch(r"[A-Za-z0-9_+/=-]{128,}", text)):
raise TransferError("Alt ajan iletisi şifreli veya okunamıyor; görev eksik gönderilmedi.")
portable.append({"type": "input_text", "text": text})
else:
portable.append(part)
result.append({"type": "message", "role": "user", "content": [
{"type": "input_text", "text": "Agent message: " + encoded({
"author": item.get("author"), "recipient": item.get("recipient")})}
] + portable})
continue
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[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")}
earlier = [i for i, x in enumerate(items[:cut]) if isinstance(x, dict) and x.get("call_id") in tail_calls]
if not earlier:
break
cut = min(earlier)
protected, old, recent = [], [], []
for i, item in enumerate(items):
if isinstance(item, dict) and item.get("role") in ("system", "developer"):
protected.append(item)
elif i < cut:
old.append(item)
else:
recent.append(item)
return protected, old, recent
class Preparer:
def __init__(self):
self.cache = OrderedDict()
self.lock = threading.Lock()
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 = portable_reasoning(normalize(request))
window = int(window)
if window < 1024:
raise TransferError("Hedef modelin bağlam kapasitesi doğrulanamadı.")
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
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 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.")
# 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)
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:
summary = summarize(source, budget)
if not isinstance(summary, str) or len(summary.strip()) < 80:
raise TransferError("AITURK özeti tamamlanamadı; eski geçmiş korundu.")
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)
while len(self.cache) > 32:
self.cache.popitem(last=False)
return candidate
PREPARER = Preparer()
@@ -86,7 +86,7 @@ def ide_yapilandirma(agent=AGENT):
return base.rstrip("/"), varsayilan, adlar
EN_COK_MODEL = 300
EN_COK_MODEL = 1000
AITURK_EKI = " (AITURK)"
# Gecidin kisa saglayici kodu -> kullanicinin gormek istedigi kod (10 Ekim 2026: "agy/gemini").
_SAGLAYICI_KISA = {"ag": "agy"}
@@ -233,7 +233,7 @@ def sirala_codex(modeller, ekler, ozgun_kimlikler=()):
ad = model.get("display_name") or model.get("slug", "")
if yalin(ad) in ozgun_adlar:
model["display_name"] = f"{ad}{AITURK_EKI}"
modeller[:] = ozgunler + ek_modeller
modeller[:] = (ozgunler + ek_modeller)[:EN_COK_MODEL]
# ---- Secicide model adinin altindaki satir ----
@@ -348,11 +348,13 @@ def yukle(ua="AstraBridge/1.0 (codex)", timeout=30, home=None, ag=True):
if not girdiler:
raise ValueError("Gecitte kullanilabilir OmniRoute sohbet modeli yok")
kayitlar = {m["id"]: m for m in metadata if m.get("id")}
ilanlar = {m["id"]: m for m in data if m.get("id")}
nano = pencere.nano_yukle(home, ua, ag=ag) # ag=False: acilista yalniz onbellek
ogr = pencere.ogrenilen(home)
aracsiz = pencere.aracsizlar(home)
for g in girdiler:
bilgi = pencere.coz(g["id"], g["kind"], g.get("yol"), kayitlar.get(g["id"]), nano, ogr, aracsiz)
bilgi = pencere.coz(g["id"], g["kind"], g.get("yol"), kayitlar.get(g["id"]), nano, ogr, aracsiz,
ilan=ilanlar.get(g["id"]))
g["pencere"], g["gorsel"], g["arac"], g["pencere_kaynak"] = (bilgi["pencere"], bilgi["gorsel"],
bilgi["arac"], bilgi["kaynak"])
g["claude"] = pencere.claude_var(g["kind"], g.get("yol"), kayitlar.get(g["id"]))
@@ -0,0 +1,22 @@
{
"enabled": true,
"endpoint": "https://api.typesafe.ai/v1/systemone",
"model": "jev-1.13.0",
"fallbackModel": "Katman-1",
"rejectionRules": [
{ "from": "Katman-1", "to": "Katman-2" },
{ "from": "Katman-2", "to": "Katman-3" },
{ "from": "Katman-3", "to": "Katman-4" }
],
"exemptModels": ["Sesli-Hizli-Zeki", "Katman-4"],
"thresholds": { "ret": 0.5, "sart": 0.7 },
"questions": {
"ret": "The assistant did NOT do what the user asked or did NOT answer the user's question: it refused, declined, said it cannot or will not, avoided giving the requested judgment, or redirected to a different topic.",
"sart": "The assistant changed the request on its own: it added its own conditions, softened or relabeled what was asked, or left part of it out, instead of doing exactly what the user asked."
},
"timeoutSeconds": 3600,
"criteria": { "ret": { "true": "", "false": "" }, "sart": { "true": "", "false": "" } },
"maxConcurrent": 0,
"queueWaitSeconds": 3600,
"failureAction": "pass"
}
+64 -16
View File
@@ -18,6 +18,7 @@ import json
import os
import re
import urllib.request
import jev_settings
ADRES = "https://api.typesafe.ai/v1/systemone"
MODEL = "jev-1.13.0"
@@ -32,26 +33,44 @@ SORU = {
_ANAHTAR = {"okundu": False, "deger": ""}
def muaf_mi(hedef):
"""12 Ekim 2026: yalnız çözülmüş exact sesli combo hedefi Jev'den muaftır.
"""Settings' always-pass list matches exact resolved model IDs.
Taşıma hatalarının katman sırasını değiştirmez; yedek model kendi adına denetlenir.
"""
return hedef == "Sesli-Hizli-Zeki"
return hedef in jev_settings.read()[0]['exemptModels']
def sonrakiler(hedef):
"""Jev gecirmezse sirayla denenecek katmanlar. Secili model bir katmansa ondan sonrakiler; katman degilse
butun katmanlar bastan (once Katman-1). Son katman (Katman-4) kendisi gelince bos doner: sorgulanmadan gecer."""
"""Model-specific rejection edges take precedence over the general fallback.
Stop at an always-pass model or before revisiting any target.
"""
settings = jev_settings.read()[0]
ad = str(hedef or "")
if not ad or ad == KATMAN[-1]:
if not settings['enabled'] or not ad:
return []
if ad in KATMAN:
return list(KATMAN[KATMAN.index(ad) + 1:])
return list(KATMAN)
routes = {r['from']: r['to'] for r in settings['rejectionRules']}
result, seen = [], {ad}
# An exempt model can still use the general fallback on transport failure.
# Repeated targets stop the walk even if a file was edited outside Settings.
while len(result) < 65:
target = routes.get(ad, settings['fallbackModel'])
if target in seen:
break
seen.add(target)
result.append(target)
if target in settings['exemptModels']:
break
ad = target
return result
def anahtar(ortam=None, dosyalar=None):
"""TypeSafe anahtari: ortam degiskeni, sonra ~/.typesafe/typesafe.env. Yoksa ""."""
if ortam is None and dosyalar is None:
saved = jev_settings.key()
if saved:
return saved
ortam = os.environ if ortam is None else ortam
deger = str(ortam.get("TYPESAFE_API_KEY") or "").strip()
if deger:
@@ -147,24 +166,53 @@ def komutlar(govde):
return "\n".join(satirlar)
def _gonder(govde, key):
istek = urllib.request.Request(ADRES, data=json.dumps(govde).encode("utf-8"), method="POST",
def _gonder(govde, key, settings=None):
settings = settings or jev_settings.read()[0]
istek = urllib.request.Request(settings['endpoint'], data=json.dumps(govde).encode("utf-8"), method="POST",
headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"})
with urllib.request.urlopen(istek) as yanit:
with urllib.request.urlopen(istek, timeout=settings['timeoutSeconds']) as yanit:
return json.load(yanit)
class Puan(dict):
"""Carry the thresholds used for this evaluation through a settings change."""
pass
def sor(istek, cevap, key, gonder=_gonder):
"""Jev'e sorar. Donus: {"ret", "sart"} olasiliklari | None (sorulamadi)."""
govde = {"model": MODEL, "state": {"user_request": istek, "assistant_reply": cevap},
"questions": {k: {"type": "noul", "instructions": v} for k, v in SORU.items()}}
settings = jev_settings.read()[0]
govde = {"model": settings['model'], "state": {"user_request": istek, "assistant_reply": cevap},
"questions": {k: {"type": "noul", "instructions": v} for k, v in settings['questions'].items()}}
for name, criteria in settings.get('criteria', {}).items():
values = {k: v for k, v in criteria.items() if v.strip()}
if values:
govde['questions'][name]['criteria'] = values
if not jev_settings.acquire(settings):
return None
try:
yanitlar = (gonder(govde, key) or {}).get("answers") or {}
return {k: float(yanitlar[k]["noul"]) for k in SORU}
response = gonder(govde, key, settings) if gonder is _gonder else gonder(govde, key)
yanitlar = (response or {}).get("answers") or {}
result = Puan({k: float(yanitlar[k]["noul"]) for k in SORU})
if any(not 0 <= v <= 1 for v in result.values()):
return None
result.thresholds = settings['thresholds']
return result
except Exception:
return None
finally:
jev_settings.release()
def karar(puan):
"""True: adim gecmez, sonraki katman yapar. Istisna yok."""
return puan["ret"] >= ESIK["ret"] or puan["sart"] >= ESIK["sart"]
thresholds = getattr(puan, 'thresholds', None) or jev_settings.read()[0]['thresholds']
return puan["ret"] >= thresholds["ret"] or puan["sart"] >= thresholds["sart"]
def hata_gecis():
return jev_settings.read()[0]['failureAction'] == 'next'
def etkin_mi():
return bool(jev_settings.read()[0]['enabled'])
@@ -0,0 +1,89 @@
"""Per-user settings, re-read for new evaluations without restarting active chats."""
import base64
import copy
import ctypes
import json
import os
import sys
import threading
import time
from pathlib import Path
DEFAULTS = json.loads(Path(__file__).with_name('jev-settings-defaults.json').read_text(encoding='utf-8'))
_condition = threading.Condition()
_active = 0
def home():
if '--home' in sys.argv:
return Path(sys.argv[sys.argv.index('--home') + 1])
return Path(os.environ.get('AITURK_CODEX_BRIDGE_HOME') or Path(__file__).parent)
def read():
try:
data = json.loads((home() / 'jev-settings.json').read_text(encoding='utf-8'))
except FileNotFoundError:
return copy.deepcopy(DEFAULTS), None
settings = data['settings']
# A corrupted file must not silently change routing or contact another host.
from urllib.parse import urlsplit
url = urlsplit(settings['endpoint'])
if url.scheme != 'https' or not url.netloc or url.username or url.password:
raise ValueError('Invalid Jev endpoint')
for name in ('ret', 'sart'):
n = settings['thresholds'][name]
if not isinstance(n, (int, float)) or not 0 <= n <= 1 or not settings['questions'][name]:
raise ValueError('Invalid Jev rule')
for name in ('timeoutSeconds', 'queueWaitSeconds'):
if not isinstance(settings[name], int) or not 1 <= settings[name] <= 3600:
raise ValueError('Invalid Jev timeout')
if not isinstance(settings['maxConcurrent'], int) or not 0 <= settings['maxConcurrent'] <= 512:
raise ValueError('Invalid Jev concurrency')
if not settings['fallbackModel'] or not isinstance(settings['rejectionRules'], list) or settings['failureAction'] not in ('pass', 'next'):
raise ValueError('Invalid Jev routing')
return settings, data.get('protectedKey')
def key():
_, encrypted = read()
if not encrypted:
return ''
if os.name != 'nt':
raise RuntimeError('Windows credential protection required')
from ctypes import wintypes
class Blob(ctypes.Structure):
_fields_ = [('cbData', wintypes.DWORD), ('pbData', ctypes.POINTER(ctypes.c_ubyte))]
raw = base64.b64decode(encrypted, validate=True)
buf = (ctypes.c_ubyte * len(raw)).from_buffer_copy(raw)
source, result = Blob(len(raw), buf), Blob()
decrypt = ctypes.windll.crypt32.CryptUnprotectData
decrypt.argtypes = [ctypes.POINTER(Blob), ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, wintypes.DWORD, ctypes.POINTER(Blob)]
decrypt.restype = wintypes.BOOL
if not decrypt(ctypes.byref(source), None, None, None, None, 0, ctypes.byref(result)):
raise ctypes.WinError()
try:
return ctypes.string_at(result.pbData, result.cbData).decode('utf-8')
finally:
ctypes.windll.kernel32.LocalFree.argtypes = [ctypes.c_void_p]
ctypes.windll.kernel32.LocalFree(result.pbData)
def acquire(settings):
global _active
deadline = time.monotonic() + settings['queueWaitSeconds']
with _condition:
while settings['maxConcurrent'] and _active >= settings['maxConcurrent']:
remaining = deadline - time.monotonic()
if remaining <= 0:
return False
_condition.wait(remaining)
_active += 1
return True
def release():
global _active
with _condition:
_active -= 1
_condition.notify_all()
+202 -34
View File
@@ -25,6 +25,9 @@ except ImportError:
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
@@ -32,7 +35,12 @@ import is_hatti
import internet
import jev_denetim
SURUM = "8"
from bekci_watch import MetadataFilter
# Installed only by owned native launcher, never an HTTP control endpoint.
WATCH_OWNER = None
SURUM = "18"
HERE = os.path.dirname(os.path.abspath(__file__))
try:
with open(os.path.abspath(__file__), "rb") as _f:
@@ -84,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:
@@ -118,12 +127,13 @@ NO_RETRY_PATTERNS = CFG.get("no_retry_patterns", ["All fusion panel models faile
LOGDIR = os.path.join(HOME, "log")
os.makedirs(LOGDIR, exist_ok=True)
lock = threading.Lock()
log_lock = threading.Lock()
seq = [0]
def log(msg):
line = time.strftime("%Y-%m-%d %H:%M:%S ") + msg
print(line, flush=True)
with lock:
print(line, file=sys.stderr, flush=True)
with log_lock:
with open(os.path.join(LOGDIR, "bridge.log"), "a", encoding="utf-8") as f:
f.write(line + "\n")
@@ -353,15 +363,40 @@ def arac_semalarini_duzelt(req):
n += bool(degisti)
return n
def flatten_request(req):
def flatten_request(req, tool_names=None):
"""additional_tools/namespace/custom -> duz function tools. Donus: custom arac adlari kumesi."""
if isinstance(req.get("input"), list) and any(
isinstance(item, dict) and item.get("type") == "agent_message" for item in req["input"]):
import gecis_context
req["input"] = gecis_context.portable_reasoning(req)["input"]
custom = set()
tools = []
def add_tool(t):
tool_names = tool_names if tool_names is not None else {}
identities = {}
wire_names = {}
def add_tool(t, namespace=None):
ty = t.get("type")
if ty in ("function", "custom"):
namespace = t.get("namespace") or namespace
name = t["name"]
if namespace:
import hashlib
identity = (namespace, name)
# A hash avoids collisions between underscore-containing namespace/name pairs.
alias = "ns_" + hashlib.sha256(json.dumps(identity).encode()).hexdigest()[:24] + "_" + re.sub(r"[^a-zA-Z0-9_-]", "_", name)[:32]
if alias in tool_names and tool_names[alias] != identity:
raise ValueError("Tool namespace alias collision")
tool_names[alias] = identity
identities[identity] = alias
t = dict(t, name=alias, description=f"Tool {namespace}.{name}. " + t.get("description", ""))
wire = t["name"]
identity = (namespace, name)
if wire in wire_names and wire_names[wire] != identity:
raise ValueError("Tool namespace alias collision")
wire_names[wire] = identity
if ty == "namespace":
for s in t.get("tools", []):
add_tool(s)
add_tool(s, ".".join(filter(None, (namespace, t.get("name")))))
elif ty == "function":
tools.append({"type": "function", "name": t["name"], "description": t.get("description", ""),
"parameters": t.get("parameters") or {"type": "object", "properties": {}}, "strict": False})
@@ -377,13 +412,34 @@ def flatten_request(req):
tools.append(t)
for t in req.get("tools", []) or []:
add_tool(t)
for it in req.get("input", []) or []:
if it.get("type") == "additional_tools":
for t in it.get("tools", []):
add_tool(t)
plain_names = {t.get("name") for t in tools if t.get("name") not in tool_names}
def flatten_name(item):
namespace, name = item.get("namespace"), item.get("name")
alias = identities.get((namespace, name))
if not namespace and name not in plain_names:
matches = [a for a, identity in tool_names.items() if identity[1] == name]
if len(matches) == 1:
alias = matches[0] # Legacy history recorded before namespaces were preserved.
if alias:
item = dict(item, name=alias)
item.pop("namespace", None)
return item
choice = req.get("tool_choice")
if isinstance(choice, dict):
req["tool_choice"] = flatten_name(choice)
if isinstance(choice.get("tools"), list):
req["tool_choice"] = dict(req["tool_choice"], tools=[flatten_name(t) for t in choice["tools"]])
new_input = []
for it in req.get("input", []) or []:
ty = it.get("type")
if ty == "additional_tools":
for t in it.get("tools", []):
add_tool(t)
continue
if ty in ("function_call", "custom_tool_call"):
it = flatten_name(it)
if ty in ("function_call_output", "custom_tool_call_output") and not it.get("call_id"):
# Desktop thread handoffs have no matching tool call. Preserve their
# content as imported context, not an invalid tool-role message.
@@ -517,9 +573,10 @@ class SseRewriter:
Ayrica dis modelin asistan mesajlarina phase damgasi koyar: mesajin "bitti" olayi, arkasindan ne
geldigi belli olana kadar bekletilir (arac cagrisi ya da baska oge -> commentary; yanit bitti ->
final_answer). Metin parcalari (delta) bekletilmez, canli akar."""
def __init__(self, custom, hedef=None, tasma=None, aracsiz=None, dolu=False):
def __init__(self, custom, hedef=None, tasma=None, aracsiz=None, dolu=False, tool_names=None):
self.dolu = dolu # istek hedef pencerenin buyuk kismini dolduruyor
self.custom = custom
self.tool_names = tool_names or {}
self.buf = b""
self.custom_ids = set()
self.bekleyen = None # phase karari bekleyen "output_item.done" olayi
@@ -624,9 +681,21 @@ class SseRewriter:
self.bekleyen = d
return once
return once + [d]
def _restore_tool_name(self, item):
if isinstance(item, dict) and item.get("type") in ("function_call", "custom_tool_call"):
identity = self.tool_names.get(item.get("name"))
if identity:
item["namespace"], item["name"] = identity
return item
def _fix(self, d):
t = d.get("type", "")
item = d.get("item")
if isinstance(item, dict) and item.get("type") == "custom_tool_call":
fix_tool_ids({"input": [item]})
if t.startswith("response.custom_tool_call_input.") and isinstance(d.get("item_id"), str):
identity = {"type": "custom_tool_call", "id": d["item_id"]}
fix_tool_ids({"input": [identity]})
d["item_id"] = identity["id"]
if item and item.get("type") == "function_call" and item.get("name") in self.custom:
self.custom_ids.add(item.get("id"))
args = item.get("arguments") or ""
@@ -639,6 +708,7 @@ class SseRewriter:
# Normalize before Codex persists the response. Request-only repair
# cannot help when the user later disables the bridge.
fix_tool_ids({"input": [d["item"]]})
self._restore_tool_name(d["item"])
return d
if t.startswith("response.function_call_arguments.") and d.get("item_id") in self.custom_ids:
return None # JSON parcalari custom input'a cevrilemez; done olayinda tam metin gidiyor
@@ -651,8 +721,10 @@ class SseRewriter:
item = {"type": "custom_tool_call", "id": item.get("id"), "call_id": item.get("call_id"),
"name": item.get("name"), "input": inp, "status": "completed"}
fix_tool_ids({"input": [item]})
out.append(item)
out.append(self._restore_tool_name(item))
d["response"]["output"] = out
fix_tool_ids({"input": out})
self._restore_tool_name(d.get("item"))
return d
def feed(self, chunk):
self.buf += chunk
@@ -919,12 +991,24 @@ class H(http.server.BaseHTTPRequestHandler):
if hata:
gecmedi = True
elif metin and jev_denetim.muaf_mi(hedef):
log(f"{tag} Jev atlandi: Sesli-Hizli-Zeki")
log(f"{tag} Jev atlandi: her zaman gec ({hedef})")
elif metin:
t0 = time.time()
puan = jev_denetim.sor(istek, metin, jev_denetim.anahtar())
# Jev may wait for a slot or a remote response. Keep this chat alive.
import threading as jev_threading
jev_done, jev_result = jev_threading.Event(), [None]
def denetle():
try:
jev_result[0] = jev_denetim.sor(istek, metin, jev_denetim.anahtar())
finally:
jev_done.set()
jev_threading.Thread(target=denetle, daemon=True).start()
while not jev_done.wait(10):
w(b": jev-denetim\n\n")
puan = jev_result[0]
if puan is None:
log(f"{tag} katman denetimi: Jev'e sorulamadi, cevap oldugu gibi geciyor")
gecmedi = jev_denetim.hata_gecis()
log(f"{tag} katman denetimi: Jev'e sorulamadi, {'siradaki katman' if gecmedi else 'cevap oldugu gibi geciyor'}")
else:
gecmedi = jev_denetim.karar(puan)
log(f"{tag} katman denetimi: ret={puan['ret']:.2f} sart={puan['sart']:.2f} -> "
@@ -948,11 +1032,16 @@ class H(http.server.BaseHTTPRequestHandler):
resp, rewriter, tag = yeni
hedef = ad # Muafiyet yalnız ilk exact hedefe ait; yedek katmana taşınmaz.
log(f"{tag} katman denetimi: adim bu katmana verildi")
if not kalan:
if jev_denetim.muaf_mi(hedef):
return self._stream(resp, resp.status, tag, rewriter, izle, basliksiz=True)
if gecmedi:
# Son katman ne dondurduyse o gider, hicbir sey eklenmez; dondurmediyse hicbir sey gitmez.
w(son_govde)
# Headers already declare SSE. A raw upstream JSON/HTML body
# here leaves Codex waiting for a terminal event and retrying.
event = {"type": "response.failed", "response": {
"id": "resp_astrabridge_%d" % time.time_ns(),
"object": "response", "status": "failed", "output": [], "error": {
"code": "jev_no_fallback", "message": "Jev denetiminden gecen cevap alinamadi; gecis kurallarini kontrol edin."}}}
w(b"event: response.failed\ndata: " + json.dumps(event).encode() + b"\n\n")
else:
for chunk in parcalar:
if izle is not None:
@@ -1050,7 +1139,13 @@ class H(http.server.BaseHTTPRequestHandler):
"ide_model": int((sayi.split() or ["0"])[0]),
"ozet": bool((CFG.get("ozet_kopru") or {}).get("enabled", False)),
"is_hatti": bool(HAT_KOMUT[0]),
"adim_denetimi": bool(jev_denetim.anahtar())})
"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"):
self._send_json(200, {"ok": True})
log("AITURK IDE istegiyle kapatiliyor")
@@ -1070,7 +1165,20 @@ class H(http.server.BaseHTTPRequestHandler):
return
body = self._read()
path = self.path
if self.command == "POST" and path == "/aiturk/codex-baglama/prepare-transfer":
# Non-simple header prevents browser-origin requests to this local service.
if self.headers.get("Origin") or self.headers.get("X-AITURK-Transfer") != "1":
return self._send_json(403, {"error": {"message": "local transfer only"}})
try:
result = self._prepare_transfer(json.loads(body))
return self._send_json(200, result)
except Exception as e:
log(f"gecis hazirligi basarisiz: {type(e).__name__}")
message = str(e) if isinstance(e, gecis_context.TransferError) else "AITURK geçiş özeti hazırlanamadı; geçmiş korundu."
return self._send_json(422, {"error": {"message": message}})
if path.startswith("/aiturk/codex-baglama/"):
if WATCH_OWNER is not None:
return self._send_json(404, {"error": "not available on native owner"})
return self._control(path)
if DEBUG:
log(f"[{n} giris {self.command} {path[:70]}] cl={self.headers.get('Content-Length')} enc={self.headers.get('Content-Encoding')} govde={len(body)}")
@@ -1151,7 +1259,7 @@ class H(http.server.BaseHTTPRequestHandler):
govde = ozet_kopru.coz_istegi(oge, ozet_cfg.get("openai_modeli", "gpt-6-luna"))
hdrs = {k: v for k, v in self.headers.items() if k.lower() not in REQ_HOP and k.lower() != "content-encoding"}
hdrs["Content-Type"] = "application/json"; hdrs["Accept"] = "text/event-stream"
r = urllib.request.Request(CHATGPT + self.path, data=json.dumps(govde, ensure_ascii=False).encode("utf-8"), headers=hdrs, method="POST")
r = urllib.request.Request(CHATGPT + "/backend-api/codex/responses", data=json.dumps(govde, ensure_ascii=False).encode("utf-8"), headers=hdrs, method="POST")
try:
resp = urllib.request.urlopen(r, timeout=float(ozet_cfg.get("coz_zaman_asimi", 300)))
metin = ozet_kopru.sse_metni(resp.read())
@@ -1166,6 +1274,26 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"[{n} ozet kopru] OpenAI ozeti duz metne cevrildi ({len(metin)} karakter, onbellege yazildi)")
return metin or None
def _transfer_window(self, slug):
if ek_model_mu(slug):
info = IDE_BILGI.get(slug) or {}
value = info.get("pencere")
# Unknown is not an excuse to disable the guard. Use a conservative
# fallback until the provider reports its real capacity.
return int(value) if value and pencere.bekci_calisir(info.get("kaynak"), value) else 32768
account = self.headers.get("chatgpt-account-id") or "-"
cached = LAST_MODELS.get(account)
if cached:
for model in json.loads(cached["body"]).get("models", []):
if model.get("slug") == slug and model.get("context_window"):
return int(model["context_window"])
raise gecis_context.TransferError("Abonelik modelinin bağlam kapasitesi katalogda bulunamadı; geçmiş korundu.")
def _prepare_transfer(self, payload):
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.
Donus: True = yanit istemciye gitti; False = abonelik istegi baslamadan basarisiz oldu
@@ -1391,6 +1519,7 @@ class H(http.server.BaseHTTPRequestHandler):
except Exception as e:
log(f"[{n} responses] govde cozulemedi: {e}")
return self._send_json(400, {"error": {"message": f"bridge: govde cozulemedi: {e}"}})
self.watch_correlation = WATCH_OWNER.capture(dict(self.headers), req) if WATCH_OWNER else None
slug = req.get("model", "")
gelen = slug # Codex'in bu sohbet icin gonderdigi ad (listeden secilen model)
if not ek_model_mu(gelen):
@@ -1549,11 +1678,12 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"{tag} {cikan} resim not ile degistirildi ({kazanc // 1024} KB)")
rewriter = None
if flat if flat is not None else route.get("flatten_tools"):
custom = flatten_request(req)
tool_names = {}
custom = flatten_request(req, tool_names)
duzelen = arac_semalarini_duzelt(req)
if duzelen:
log(f"{tag} {duzelen} aracin semasi duzeltildi (ust duzey oneOf/anyOf/allOf)")
rewriter = SseRewriter(custom, hedef=target, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren)
rewriter = SseRewriter(custom, hedef=target, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren, tool_names=tool_names)
fix_tool_ids(req)
if route_name == catalog.get('route', '9router') and target.startswith('ag/gemini-'):
merged = coalesce_context(req)
@@ -1571,15 +1701,17 @@ class H(http.server.BaseHTTPRequestHandler):
if eklendi:
log(f"{tag} girdi kullanici mesajiyla bitmiyordu ({sonlar}), devam mesaji eklendi")
hedef_pencere = (IDE_BILGI.get(slug) or {}).get("pencere")
# Penceresi bilinmeyen (varsayilan degerli) modelde bekci calismaz: tahmine dayanarak gecmis kirpilmaz.
# Comboda tek bilinmeyen uye, bilinen kucuk uyenin sinirini gecersiz kilmaz (pencere.bekci_calisir).
pencere_biliniyor = bool(hedef_pencere) and pencere.bekci_calisir((IDE_BILGI.get(slug) or {}).get("kaynak"), hedef_pencere)
if hedef_pencere and pencere_biliniyor and CFG.get("tasma_bekcisi", True) and not ozet_tetik:
kirpilan, sigdi, tahmin = pencere.sigdir(req, hedef_pencere, pencere.oran_sec(target, bool((IDE_BILGI.get(slug) or {}).get("claude"))))
# Final check includes instructions added after transfer preparation.
# Never cut the middle of historical text: semantic preparation happens
# before inference, while the disk transcript remains untouched.
pencere_kaynagi = (IDE_BILGI.get(slug) or {}).get("kaynak")
if not hedef_pencere or not pencere.bekci_calisir(pencere_kaynagi, hedef_pencere):
hedef_pencere = 32768
if CFG.get("tasma_bekcisi", True) and not ozet_tetik:
tahmin = pencere.tahmini_token(req, 2.0)
sigdi = tahmin <= int(hedef_pencere * 0.90)
if rewriter is not None:
rewriter.dolu = tahmin >= hedef_pencere * 0.7
if kirpilan:
log(f"{tag} tasma bekcisi: {kirpilan} buyuk eski parca kisaltildi (tahmini {tahmin} token, pencere {hedef_pencere})")
if not sigdi:
log(f"{tag} tasma bekcisi: istek gonderilmedi (tahmini {tahmin} token > pencere {hedef_pencere})")
return self._send_sse_failed("context_length_exceeded",
@@ -1607,9 +1739,9 @@ class H(http.server.BaseHTTPRequestHandler):
# secili model baska bir rotada olsa bile. Gecit yoksa denetim yapilmaz. Ozet isteginde ve aracsiz
# (rewriter yok) yolda kapalidir.
jev_rota = ROUTES.get(ide_route)
jev_sira = ([] if ozet_tetik or rewriter is None or jev_rota is None or not jev_denetim.anahtar()
else jev_denetim.sonrakiler(target))
jev_istek = jev_denetim.son_istek(req) if jev_sira else ""
jev_etkin = jev_denetim.etkin_mi() and not (ozet_tetik or rewriter is None or jev_rota is None or not jev_denetim.anahtar())
jev_sira = jev_denetim.sonrakiler(target) if jev_etkin else []
jev_istek = jev_denetim.son_istek(req) if jev_etkin else ""
def jev_ac(ad):
katman_req = json.loads(data)
@@ -1619,7 +1751,7 @@ class H(http.server.BaseHTTPRequestHandler):
yanit = urllib.request.urlopen(urllib.request.Request(
jev_rota["url"], data=json.dumps(katman_req, ensure_ascii=False).encode("utf-8"), headers=jev_hdrs, method="POST"),
timeout=UPSTREAM_IDLE_TIMEOUT)
return (yanit, SseRewriter(custom, hedef=ad, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren),
return (yanit, SseRewriter(custom, hedef=ad, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren, tool_names=tool_names),
f"[{n} {slug}=>{ad}]")
# A 9Router combo (Fusion-*, no provider prefix) runs a whole panel upstream. Once it may
# have started (timeout, 504, cut stream) resending it runs and bills it twice (28 Sep 2026);
@@ -1645,7 +1777,7 @@ class H(http.server.BaseHTTPRequestHandler):
log(f"{tag} deneme {attempt}: HTTP {e.code} {errbody[:100]!r} -> tekrar")
time.sleep(min(2 * attempt, 8)); continue
log(f"{tag} HTTP {e.code} {errbody[:200]!r}")
if jev_sira: # secili model hata verdi: adimi siradaki katman yapar
if jev_etkin: # secili model hata verdi: adimi siradaki katman yapar
return self._denetimli_akis(None, tag, rewriter, izle, jev_istek, jev_sira, jev_ac, hedef=target)
if pencere.tasma_mi(etxt):
# Hedef "baglam sigmiyor" dedi: Codex'in tanidigi tasma hatasi olarak iletilir ve hedefin
@@ -1669,16 +1801,33 @@ class H(http.server.BaseHTTPRequestHandler):
break
log(f"{tag} deneme {attempt}: {e}")
time.sleep(min(2 * attempt, 8)); continue
# Only the resolved exact voice target owns this sideband protocol.
# Other models retain their original response/Jev/stream pipeline.
if target == 'Sesli-Hizli-Zeki':
# Capture was taken before rewriting/request dispatch. Only this exact
# route credential may authenticate the returned public receipt.
correlation = self.watch_correlation
accept_watch = lambda metadata: WATCH_OWNER.accept(metadata, correlation, route['key'], route['url']) if WATCH_OWNER else None
if 'application/json' in str(resp.headers.get('Content-Type', '')):
try:
reply = json.loads(resp.read())
if isinstance(reply, dict):
metadata = reply.pop('_bekci_watch', None)
if metadata is not None: accept_watch(metadata)
return self._send_json(resp.status, reply)
finally:
resp.close()
resp = WatchResponse(resp, accept_watch)
# Headers go to the client from here on: a cut stream is never resent.
if ozet_tetik:
akis = ozet_kopru.OzetAkisi(rewriter, tohum=str(n), en_az=ozet_kopru.en_az_karakter(girdi_bayt, ozet_cfg))
sonuc = self._stream(resp, resp.status, tag, akis)
log(f"{tag} ozet kopru: {akis.sonuc_notu()}")
return sonuc
if jev_sira and resp.status == 200:
if jev_etkin and resp.status == 200:
return self._denetimli_akis(resp, tag, rewriter, izle, jev_istek, jev_sira, jev_ac, hedef=target)
return self._stream(resp, resp.status, tag, rewriter, izle)
if jev_sira: # secili modele ulasilamadi: adimi siradaki katman yapar
if jev_etkin: # secili modele ulasilamadi: adimi siradaki katman yapar
return self._denetimli_akis(None, tag, rewriter, izle, jev_istek, jev_sira, jev_ac, hedef=target)
self._send_json(503, {"error": {"message": f"bridge: hedef ulasilamadi {last}"}})
@@ -1688,8 +1837,27 @@ class H(http.server.BaseHTTPRequestHandler):
do_DELETE = _handle
do_PATCH = _handle
class WatchResponse:
"""Remove metadata before any response rewriter, log or history observer."""
def __init__(self, response, accept):
self.response, self.filter = response, MetadataFilter(accept)
self.done = False
def __getattr__(self, name):
return getattr(self.response, name)
def read1(self, size):
while not self.done:
raw = self.response.read1(size)
if raw:
data = self.filter.feed(raw)
if data: return data
else:
self.done = True
return self.filter.flush()
return b''
class TS(http.server.ThreadingHTTPServer):
daemon_threads = True
request_queue_size = 128
# Windows'ta SO_REUSEADDR baska bir surecin ayni portu "calmasina" izin verir.
# Kapali: port doluysa sessizce yan yana dinlemek yerine acikca hata verir.
allow_reuse_address = False
@@ -0,0 +1,573 @@
"""Owned native Codex stdio adapter; inert until explicitly launched by an owner.
Only a trusted in-process root adapter may install Binding objects. There is no
HTTP bind/RPC endpoint, tenant inference, or activated SSE subscription.
appendSpeech acceptance is not proof of audible speech or history persistence.
"""
from __future__ import annotations
import hmac
import hashlib
import json
import queue
import secrets
import subprocess
import sys
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import BinaryIO, TypedDict, Callable
@dataclass(frozen=True)
class WatchReceipt:
"""INTERNAL adapter contract, not a claim about backend wire field names.
Backend resolver authenticates an opaque handle using the member credential
against a trusted, configured Omni endpoint. Backend secret stays server-side.
The resolver must verify tenant, source request thread and history together.
No URL or identity supplied by a notice is trusted as subscription authority.
"""
opaque_handle: str
tenant_id: str
bekci_session_id: str
history_revision: str
thread_id: str
expires_at: float
@dataclass(frozen=True)
class RequestCorrelation:
"""Captured by the owned HTTP bridge from the actual incoming request.
Must be captured before metadata arrives, not reconstructed from its body.
generation is the mux epoch observed at request capture, never the latest tab.
"""
connection_id: str
thread_id: str
generation: int
class WatchReceiptAdapter:
"""One consumer; root chooses transport/decoder after backend agreement.
decode extracts only the opaque handle from bekci.watch SSE data or the
nonstream _bekci_watch value. resolve performs authenticated backend lookup.
Missing agreement is fail-closed, not fabricated ready metadata.
"""
def __init__(self, mux, *, decode: Callable[[object], str] | None = None,
resolve: Callable[[str], WatchReceipt] | None = None):
self.mux, self.decode, self.resolve = mux, decode, resolve
self._accepted = {}
def capture(self, headers: dict, body: dict) -> RequestCorrelation | None:
normalized = {k.lower(): v for k, v in headers.items()}
values = [normalized.get('session-id'), normalized.get('thread-id'), body.get('prompt_cache_key')]
if not all(isinstance(v, str) and v for v in values) or len(set(values)) != 1:
return None
snap = self.mux.snapshot()
state = snap['threads'].get(values[0], {})
if snap['closed'] or not state.get('active'):
return None
return RequestCorrelation(snap['connection_id'], values[0], state['generation'])
def accept(self, transport: str, payload: object, correlation: RequestCorrelation | None) -> str:
if transport not in ('bekci.watch', '_bekci_watch'):
return 'unsupported_metadata_transport'
if self.decode is None or self.resolve is None:
return 'backend_contract_not_established'
if correlation is None or correlation.connection_id != self.mux.connection_id:
return 'request_correlation_missing'
try:
handle = self.decode(payload)
if not isinstance(handle, str) or not 1 <= len(handle) <= 4096:
return 'invalid_receipt'
receipt = self.resolve(handle)
except Exception:
return 'receipt_verification_failed' # never expose credential-bearing exception text
if not isinstance(receipt, WatchReceipt) or receipt.opaque_handle != handle:
return 'invalid_receipt'
if (receipt.thread_id != correlation.thread_id or type(receipt.expires_at) not in (int, float)
or not time.time() < receipt.expires_at <= time.time() + 3600
or not all(isinstance(v, str) and 0 < len(v) <= 256 for v in
(receipt.tenant_id, receipt.bekci_session_id, receipt.history_revision, receipt.thread_id))):
return 'receipt_mismatch_or_expired'
binding = Binding(receipt.tenant_id, receipt.bekci_session_id, receipt.history_revision,
correlation.connection_id, receipt.thread_id, correlation.generation)
previous = self._accepted.get(handle)
if previous is not None and previous != (receipt, binding):
return 'receipt_replay_mismatch'
if previous is None and len(self._accepted) >= 4096:
return 'capacity_exhausted'
if not self.mux.bind(binding):
return 'stale_generation'
self._accepted[handle] = (receipt, binding)
return 'bound'
def notice(self, handle: str, notice: Notice) -> dict:
accepted = self._accepted.get(handle)
if accepted is None:
return {'status': 'binding_not_established'}
receipt, binding = accepted
if time.time() >= receipt.expires_at:
return {'status': 'receipt_expired'}
expected = dict(tenant_id=binding.tenant_id, bekci_session_id=binding.bekci_session_id,
history_revision=binding.history_revision, thread_id=binding.thread_id,
connection_id=binding.connection_id, expected_generation=binding.generation)
if not isinstance(notice, dict) or any(notice.get(k) != v for k, v in expected.items()):
return {'status': 'binding_mismatch'}
return self.mux.submit(notice)
def verified_original(path: str, expected_sha256: str) -> str:
"""Owner-pinned absolute original; no PATH/override discovery or extra engine."""
candidate = Path(path)
if not candidate.is_absolute() or not candidate.is_file() or candidate.resolve() == Path(__file__).resolve():
raise ValueError('original executable must be an explicitly pinned absolute file')
if len(expected_sha256) != 64:
raise ValueError('original executable hash required')
with candidate.open('rb') as stream:
actual = hashlib.file_digest(stream, 'sha256').hexdigest()
if not hmac.compare_digest(actual, expected_sha256.lower()):
raise ValueError('original executable hash mismatch')
return str(candidate.resolve())
def stdio_invocation(args: list[str]) -> bool:
"""Installed Desktop tc()/Ks() grammar, preserving every original argument."""
seen, i = False, 0
while i < len(args):
arg = args[i]
if arg in ('-c', '--config') and i + 1 < len(args):
i += 2
continue
if arg == 'app-server' and not seen:
seen = True
elif seen and arg in ('--stdio', '--analytics-default-enabled'):
pass
elif seen and arg == '--listen' and args[i+1:i+2] == ['stdio://']:
i += 1
else:
return False
i += 1
return seen
def launch_original(path: str, expected_sha256: str, args: list[str], *,
attach: Callable[[WatchReceiptAdapter], None] | None = None) -> int:
"""Source launcher entry for a future normal bridge module copy.
No activation/config mutation. Unknown invocation grammar passes native stdio
directly. Owner callback wires only this consumer after backend agreement.
No second engine, binary patch, token file or discovery of current UI tab.
"""
original = verified_original(path, expected_sha256)
observer = stdio_invocation(args)
mux = NativeSpeechMux([original, *args], observer=observer)
if observer and attach is not None:
try:
attach(WatchReceiptAdapter(mux))
except Exception:
# Optional adapter failure cannot take down native stdio. Invalidate
# any partial setup so no correction can escape a failed attachment.
mux._disabled = True
mux._bindings.clear()
return mux.run()
class Notice(TypedDict):
notice_id: str
tenant_id: str
bekci_session_id: str
history_revision: str
connection_id: str
thread_id: str
expected_generation: int
issued_at: float
expires_at: float
text: str
@dataclass(frozen=True)
class Binding:
"""Local scope labels (legacy field names), NOT literal server identity claims.
Trusted owner attests this opaque receipt scope belongs to this native epoch.
Install only from independently verified mapping, NEVER from notice fields.
Rebind explicitly for a new history revision or realtime generation.
"""
tenant_id: str
bekci_session_id: str
history_revision: str
connection_id: str
thread_id: str
generation: int
def _object(raw: bytes) -> dict:
try:
value = json.loads(raw)
return value if isinstance(value, dict) else {}
except (ValueError, UnicodeError):
return {}
def _id(value):
return (type(value), value) if type(value) in (str, int) else None
class NativeSpeechMux:
"""One original process, bounded FIFO, one writer in each direction.
argv is the original executable plus its original arguments. observer=True
must only be selected for a verified stdio app-server invocation. The CLI
deliberately recognizes only the exact ['app-server'] invocation; all other
invocations inherit the original stdio directly. No environment/cwd rewrite.
"""
def __init__(self, argv: list[str], *, observer: bool = False):
self.argv = list(argv)
self.observer = observer
self.connection_id = secrets.token_hex(24)
self._prefix = 'bekci-speech-' + secrets.token_hex(24) + ':'
self._token = secrets.token_urlsafe(32)
self._lock = threading.RLock()
self._queue = queue.Queue(maxsize=32)
self._ended = threading.Event()
self._eof = False
self._disabled = False
self._threads = {}
self._starts = set()
self._bindings = {}
self._receipts = {}
self._pending = {}
self._counter = 0
self._server = None
self._process = None
def snapshot(self) -> dict:
with self._lock:
return {'connection_id': self.connection_id,
'binding_status': 'established' if self._bindings else 'binding_not_established',
'threads': {k: dict(v) for k, v in self._threads.items()},
'closed': self._eof or self._ended.is_set(),
'injection_disabled': self._disabled}
def bind(self, binding: Binding) -> bool:
"""Trusted local API only. No untrusted endpoint can invoke this."""
with self._lock:
st = self._threads.get(binding.thread_id, {})
if (binding.connection_id != self.connection_id or not st.get('active')
or st.get('generation') != binding.generation or self._eof
or self._ended.is_set()):
return False
self._bindings[(binding.tenant_id, binding.bekci_session_id)] = binding
return True
def _invalidate(self, thread):
if not isinstance(thread, str):
return
if thread in self._threads:
self._threads[thread]['active'] = False
self._bindings = {k: b for k, b in self._bindings.items() if b.thread_id != thread}
def _observe_client(self, obj):
with self._lock:
rid = _id(obj.get('id'))
# Responses to server requests may legitimately reuse our ID.
if 'method' in obj and isinstance(obj.get('id'), str) and obj['id'].startswith(self._prefix):
self._disabled = True
self._pending.pop(obj['id'], None)
if obj.get('method') in ('thread/start', 'thread/resume', 'thread/fork') and rid:
if len(self._starts) < 4096:
self._starts.add(rid)
else:
self._disabled = True
if obj.get('method') in ('thread/realtime/stop', 'thread/realtime/start', 'thread/unsubscribe'):
params = obj.get('params')
if isinstance(params, dict):
self._invalidate(params.get('threadId'))
def _observe_server(self, obj) -> bool:
with self._lock:
rid = obj.get('id')
response = 'method' not in obj and (('result' in obj) != ('error' in obj))
if response and isinstance(rid, str) and rid in self._pending:
key = self._pending.pop(rid)
self._receipts[key]['status'] = 'rpc_error' if 'error' in obj else 'rpc_accepted'
return True
if response and _id(rid) in self._starts:
self._starts.discard(_id(rid))
result = obj.get('result')
thread = result.get('thread', {}) if isinstance(result, dict) else {}
tid = thread.get('id') if isinstance(thread, dict) else None
if isinstance(tid, str):
self._threads.setdefault(tid, {'generation': 0, 'active': False, 'realtime_session_id': None})
params = obj.get('params')
if not isinstance(params, dict) or 'id' in obj:
return False
tid = params.get('threadId')
if not isinstance(tid, str):
return False
method = obj.get('method')
if method == 'thread/realtime/started':
self._invalidate(tid)
previous = self._threads.get(tid, {}).get('generation', 0)
self._threads[tid] = {'generation': previous + 1, 'active': True,
'realtime_session_id': params.get('realtimeSessionId')}
elif method in ('thread/realtime/closed', 'thread/realtime/error'):
self._invalidate(tid)
return False
def _validate(self, notice):
if not isinstance(notice, dict) or set(notice) != set(Notice.__annotations__):
return 'invalid_schema'
for key in Notice.__annotations__:
if key not in ('expected_generation', 'issued_at', 'expires_at'):
if not isinstance(notice[key], str) or not notice[key].strip() or len(notice[key]) > (8000 if key == 'text' else 256):
return 'invalid_schema'
if type(notice['expected_generation']) is not int or notice['expected_generation'] < 1:
return 'invalid_schema'
if any(type(notice[k]) not in (int, float) for k in ('issued_at', 'expires_at')):
return 'invalid_schema'
now = time.time()
if not (now - 60 <= notice['issued_at'] <= now + 2 and now < notice['expires_at'] <= notice['issued_at'] + 60):
return 'expired_or_invalid_ttl'
if self._eof or self._ended.is_set():
return 'closed'
if self._disabled or not self.observer:
return 'injection_disabled'
binding = self._bindings.get((notice['tenant_id'], notice['bekci_session_id']))
if binding is None:
return 'binding_not_established'
if binding != Binding(notice['tenant_id'], notice['bekci_session_id'], notice['history_revision'],
notice['connection_id'], notice['thread_id'], notice['expected_generation']):
return 'binding_mismatch'
st = self._threads.get(binding.thread_id, {})
if not st.get('active') or st.get('generation') != binding.generation:
return 'stale_generation'
return None
def submit(self, notice: Notice) -> dict:
with self._lock:
error = self._validate(notice)
if error:
return {'status': error}
key = (notice['tenant_id'], notice['bekci_session_id'], notice['notice_id'])
if key in self._receipts:
return dict(self._receipts[key])
# Never evict dedupe during this process lifetime or retry uncertain sends.
if len(self._receipts) >= 4096:
return {'status': 'capacity_exhausted'}
receipt = {'status': 'queued'}
self._receipts[key] = receipt
try:
self._queue.put_nowait(('notice', (dict(notice), key)))
except queue.Full:
del self._receipts[key]
return {'status': 'busy'}
return dict(receipt)
def start_inlet(self) -> tuple[str, str]:
"""Return loopback URL and secret to trusted owner IN MEMORY; never log.
One bounded request at a time; 2s timeout applies only to inlet HTTP reads,
never the original engine. Owner must arrange private credential delivery.
"""
if not self.observer or self._server is not None:
raise ValueError('inlet requires an unused app-server observer')
mux = self
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args):
pass
def setup(self):
super().setup()
self.connection.settimeout(2)
def do_POST(self):
try:
auth = self.headers.get('Authorization', '')
if not hmac.compare_digest(auth.encode(), ('Bearer ' + mux._token).encode()):
self.send_error(401)
return
if self.headers.get('Origin') or self.headers.get('Transfer-Encoding'):
self.send_error(400)
return
lengths = self.headers.get_all('Content-Length', [])
if len(lengths) != 1 or not lengths[0].isdigit() or not 0 < int(lengths[0]) <= 65536:
self.send_error(400)
return
raw = self.rfile.read(int(lengths[0]))
if len(raw) != int(lengths[0]):
self.send_error(400)
return
if self.path == '/notice':
result = mux.submit(_object(raw))
elif self.path == '/status' and _object(raw) == {}:
result = mux.snapshot()
else:
self.send_error(404)
return
body = json.dumps(result).encode()
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
except (OSError, ValueError):
return
self._server = HTTPServer(('127.0.0.1', 0), Handler)
threading.Thread(target=self._server.serve_forever, daemon=True).start()
return f'http://127.0.0.1:{self._server.server_port}', self._token
def _put(self, item):
while not self._ended.is_set():
try:
self._queue.put(item, timeout=.1)
return True
except queue.Full:
pass
return False
def run(self, source: BinaryIO | None = None, target: BinaryIO | None = None) -> int:
if not self.observer:
return subprocess.call(self.argv) # exact inherited stdin/stdout/stderr/env/cwd
source = source if source is not None else sys.stdin.buffer
target = target if target is not None else sys.stdout.buffer
process = self._process = subprocess.Popen(self.argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
def read_client():
fragment = False
while not self._ended.is_set():
raw = source.readline(65536)
if not raw:
with self._lock:
self._eof = True
self._put(('eof', None))
return
complete = raw.endswith(b'\n')
if not fragment and complete:
self._observe_client(_object(raw))
elif not complete:
# Oversized records still forward byte-for-byte, but cannot be
# safely observed. Fail closed for injection for this connection.
with self._lock:
self._disabled = True
fragment = not complete
if not self._put(('raw', raw)):
return
def send_notice(value):
notice, key = value
with self._lock:
error = self._validate(notice)
if error:
self._receipts[key]['status'] = error
return
self._counter += 1
rid = self._prefix + str(self._counter)
self._pending[rid] = key
self._receipts[key]['status'] = 'sent_unknown'
raw = json.dumps({'id': rid, 'method': 'thread/realtime/appendSpeech',
'params': {'threadId': notice['thread_id'], 'text': notice['text']}},
ensure_ascii=True).encode() + b'\n'
# No lock during blocking pipe I/O: observer must drain server output.
# Protocol has no server-side generation precondition; this race remains.
process.stdin.write(raw)
process.stdin.flush()
def write_server():
partial = False
try:
while not self._ended.is_set():
try:
kind, value = self._queue.get(timeout=.1)
except queue.Empty:
continue
if kind == 'eof':
return
if kind == 'notice':
if partial:
# An oversized frame has disabled injection; reject now.
with self._lock:
self._receipts[value[1]]['status'] = 'injection_disabled'
else:
send_notice(value)
else:
# Re-apply lifecycle invalidation at the actual write boundary
# too: a prior server start may have arrived while queued.
if not partial and value.endswith(b'\n'):
obj = _object(value)
if obj.get('method') in ('thread/realtime/start', 'thread/realtime/stop'):
params = obj.get('params')
if isinstance(params, dict):
with self._lock:
self._invalidate(params.get('threadId'))
process.stdin.write(value)
process.stdin.flush()
partial = not value.endswith(b'\n')
except (OSError, ValueError):
pass
finally:
try:
process.stdin.close()
except OSError:
pass
reader = threading.Thread(target=read_client, daemon=True)
writer = threading.Thread(target=write_server, daemon=True)
reader.start()
writer.start()
fragment = False
try:
while True:
raw = process.stdout.readline(65536)
if not raw:
break
complete = raw.endswith(b'\n')
consumed = not fragment and complete and self._observe_server(_object(raw))
if not complete:
with self._lock:
self._disabled = True
fragment = not complete
if not consumed:
target.write(raw)
target.flush()
finally:
self._ended.set()
process.stdout.close()
with self._lock:
self._bindings.clear()
for st in self._threads.values():
st['active'] = False
for receipt in self._receipts.values():
if receipt['status'] == 'queued':
receipt['status'] = 'closed'
if self._server:
self._server.shutdown()
self._server.server_close()
return process.wait() # no deadline on legitimate native work
def main() -> int:
# Future launcher must pin/verify the original executable, never resolve the
# override recursively. No CODEX_CLI_PATH configuration is performed here.
if len(sys.argv) >= 5 and sys.argv[1] == '--pinned-original' and sys.argv[4] == '--':
try:
return launch_original(sys.argv[2], sys.argv[3], sys.argv[5:])
except ValueError:
print('original executable verification failed', file=sys.stderr)
return 2
if len(sys.argv) < 3 or sys.argv[2] != '--':
print('usage: native_speech_mux.py --pinned-original ORIGINAL_EXECUTABLE SHA256 -- ORIGINAL_ARGS...', file=sys.stderr)
return 2
argv = [sys.argv[1], *sys.argv[3:]]
return NativeSpeechMux(argv, observer=argv[1:] == ['app-server']).run()
if __name__ == '__main__':
raise SystemExit(main())
@@ -0,0 +1,42 @@
"""Owned startup entry. No user environment/config changes or override discovery."""
import json
import os
from pathlib import Path
import subprocess
import sys
import threading
from native_speech_mux import NativeSpeechMux, stdio_invocation, verified_original
from bekci_watch import Ledger, WatchOwner
def main(manifest, args):
cfg = json.loads(Path(manifest).read_text(encoding='utf-8-sig'))
original = verified_original(cfg['original'], cfg['original_sha256'])
if not stdio_invocation(args):
return subprocess.call([original,*args])
# Existing public catalog and original model passthrough handler is reused.
# A per-child CLI setting selects the ephemeral owned bridge; no global file edit.
saved_env = dict(os.environ)
os.environ['AITURK_CODEX_BRIDGE_HOME'] = cfg['bridge_home']
os.environ['HERMES_HOME'] = cfg['hermes_home']
try:
import kopru
finally:
os.environ.clear(); os.environ.update(saved_env)
from http.server import ThreadingHTTPServer
server = ThreadingHTTPServer(('127.0.0.1',0), kopru.H)
mux = NativeSpeechMux([original,*args,'-c',f'openai_base_url="http://127.0.0.1:{server.server_port}/backend-api/codex"'],observer=True)
Path(cfg['bridge_home']).mkdir(parents=True,exist_ok=True)
owner = WatchOwner(mux,Ledger(str(Path(cfg['bridge_home'])/'native-watch-ledger.sqlite3')))
kopru.WATCH_OWNER = owner
threading.Thread(target=server.serve_forever,daemon=True).start()
try:
return mux.run()
finally:
owner.close(); server.shutdown(); server.server_close()
if __name__ == '__main__':
try:
raise SystemExit(main(sys.argv[1],sys.argv[2:]))
except (OSError,ValueError,KeyError):
sys.stderr.write('Owned native launcher configuration/integrity failure.\n')
raise SystemExit(78)
@@ -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
+21 -4
View File
@@ -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:
@@ -217,7 +221,7 @@ def _yapraklar(dugum, derinlik=0):
return cikti
def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=()):
def coz(kimlik, tur, yol, kayit=None, nano=None, ogr=None, aracsiz=(), ilan=None):
"""Bir katalog girdisinin ilan edilecek penceresi ve yetenekleri.
kimlik: gecit kimligi; tur: "combo" | "model"; yol: gecidin aciklamasindaki "saglayici/model" yolu;
kayit: /router/catalog girdisi (combo topolojisi icin). Donus: {"pencere","gorsel","arac","kaynak"}."""
@@ -233,17 +237,30 @@ 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 "")
yol = (m.group(1) + "/" + yol[len("no-think/"):]) if m else yol
sonuc = yol_bilgisi(yol, nano) or dict(bilinmeyen)
# Exact /models metadata is stronger evidence than name-based local
# fallbacks. Do not infer a combo's capacity from its display name.
ilan = ilan if isinstance(ilan, dict) else {}
canli_sinirlar = [ilan[k] for k in ("context_length", "max_input_tokens")
if type(ilan.get(k)) is int and TABAN <= ilan[k] <= 10_000_000]
if canli_sinirlar:
sonuc = {**sonuc, "pencere": min(canli_sinirlar), "kaynak": "saglayici-katalogu"}
if kimlik in aracsiz:
sonuc = {**sonuc, "arac": False}
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
@@ -0,0 +1,28 @@
"""Provision a reviewable launcher candidate. Never sets CODEX_CLI_PATH or restarts apps."""
import argparse
import hashlib
import json
from pathlib import Path
import shutil
def prepare(source, destination, original, expected_sha256, python, bridge_home, hermes_home):
source, destination = Path(source).resolve(), Path(destination).resolve()
original, python = Path(original).resolve(), Path(python).resolve()
def digest(p):
with p.open('rb') as f: return hashlib.file_digest(f,'sha256').hexdigest()
if digest(original) != expected_sha256.lower(): raise ValueError('chosen official original hash mismatch')
if original == destination/'native-watch.exe': raise ValueError('recursive original')
if source == destination: raise ValueError('candidate must use separate destination')
destination.mkdir(parents=True,exist_ok=True)
for p in source.rglob('*'):
if p.is_file() and (p.suffix in ('.py','.pyd') or p.name in ('native-watch.exe','LICENSE')) and not p.name.startswith('test_'):
target=destination/p.relative_to(source);target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(p,target)
script=destination/'native_watch_launcher.py'
manifest=dict(original=str(original),original_sha256=expected_sha256.lower(),python=str(python),python_sha256=digest(python),script=str(script),script_sha256=digest(script),bridge_home=str(Path(bridge_home).resolve()),hermes_home=str(Path(hermes_home).resolve()))
(destination/'native-watch.json').write_text(json.dumps(manifest,indent=2),encoding='utf-8')
return destination/'native-watch.exe'
if __name__=='__main__':
p=argparse.ArgumentParser(description=__doc__)
for name in ('source','destination','original','expected-sha256','python','bridge-home','hermes-home'): p.add_argument('--'+name,required=True)
args=vars(p.parse_args());print(prepare(**args))
@@ -0,0 +1,119 @@
"""Transport-only handling of a worker's explicit terminal failure.
Never selects a model, evaluates content, or changes worker/Jev decisions.
If HTTP headers have already gone out, remember the failure for exactly one
identical retry. Codex then receives an HTTP error instead of re-running the
whole exhausted provider chain. Ordinary network interruptions remain retries.
"""
import hashlib
import json
import threading
import time
from collections import OrderedDict
def request_key(request, headers):
principal = "\0".join(headers.get(name, "") for name in
("Authorization", "ChatGPT-Account-Id", "session_id"))
body = json.dumps(request, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
return hashlib.sha256((principal + "\0" + body).encode()).digest()
class FailureReplays:
def __init__(self, ttl=120, capacity=128, clock=time.monotonic):
self.ttl, self.capacity, self.clock = ttl, capacity, clock
self.items = OrderedDict()
self.lock = threading.Lock()
def remember(self, key, error):
with self.lock:
self.items[key] = (self.clock() + self.ttl, error)
self.items.move_to_end(key)
while len(self.items) > self.capacity:
self.items.popitem(last=False)
def take(self, key):
with self.lock:
entry = self.items.pop(key, None)
return entry[1] if entry and entry[0] > self.clock() else None
def error_body(error):
# Preserve the actual provider failure; don't relabel it invalid_prompt,
# fabricate response.completed, or expose a rejected model response.
return {"error": {"type": "upstream_terminal_error", "code": error["code"],
"message": "Model yanıtı hata ile sonlandı. " + error["message"]}}
class Observer:
"""Bounded SSE observer. Forwarded stream bytes are never rewritten."""
LIMIT = 65536
def __init__(self):
self.tail = b""
self.data = []
self.size = 0
self.discard = False
self.discard_line = False
self.comment_seen = False
self.payload_seen = False
self.completed = False
self.error = None
def feed(self, chunk):
# read1() supplies small chunks, but also bound arbitrary caller input.
for offset in range(0, len(chunk), 4096):
self.tail += chunk[offset:offset + 4096]
while b"\n" in self.tail:
line, self.tail = self.tail.split(b"\n", 1)
if self.discard_line:
self.discard_line = False
continue
self._line(line.rstrip(b"\r"))
if len(self.tail) > self.LIMIT:
self.tail = b""
self.discard = self.discard_line = True
def _line(self, line):
if line.startswith(b":"):
self.comment_seen = True
elif line.startswith(b"data:"):
self.size += len(line)
if self.size > self.LIMIT:
self.discard = True
self.data.clear()
elif not self.discard:
self.data.append(line[5:].lstrip())
elif not line:
if self.data and not self.discard:
self._event(b"\n".join(self.data))
elif self.discard:
self.payload_seen = True
self.data, self.size, self.discard = [], 0, False
def _event(self, data):
try:
event = json.loads(data)
except (ValueError, UnicodeError):
self.payload_seen = True
return
if not isinstance(event, dict):
self.payload_seen = True
return
kind = event.get("type")
response = event.get("response")
error = event.get("error") if kind == "error" else response.get("error") if isinstance(response, dict) else None
# Known worker terminal errors only. Preserve context/rate-limit and
# other native error classifications rather than guessing semantics.
is_failure = (kind == "error" and isinstance(error, dict)
and error.get("type") == "upstream_error")
is_failure |= (kind == "response.failed" and isinstance(error, dict)
and error.get("code") == "jev_no_fallback")
if is_failure:
self.error = {"code": str(error.get("code") or "upstream_error")[:100],
"message": str(error.get("message") or "Sağlayıcı yanıtı tamamlanamadı.")[:2000]}
else:
self.payload_seen = True
if kind == "response.completed":
self.completed = True
self.error = None
@@ -23,7 +23,7 @@ class AccessPrograms(unittest.TestCase):
def setUp(self):
self.upstream = Mock(return_value=Mock(status=200))
self.env = {
"json": json,
"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "json": json,
"urllib": Mock(request=Mock(Request=urllib.request.Request, urlopen=self.upstream)),
"decompress": lambda body, encoding: body,
"MODEL_MAP": {},
@@ -0,0 +1,52 @@
"""Regression fixture matching actual Codex child delivery, with no user data."""
import copy
import unittest
import gecis_context
import kopru
class AgentPayloadTests(unittest.TestCase):
def request(self, text):
return {"model": "ide/test", "input": [{
"type": "agent_message", "author": "/root", "recipient": "/root/test",
"content": [
{"type": "input_text", "text": "Message Type: NEW_TASK\nPayload:\n"},
{"type": "encrypted_content", "encrypted_content": text},
],
}]}
def test_actual_peer_wrapper_survives_external_text_extraction(self):
task = "Run exec_command: Write-Output (157 * 29); report the exit code."
request = self.request(task)
original = copy.deepcopy(request)
kopru.flatten_request(request)
# An OpenAI-compatible translator consumes input_text, not Codex's
# encrypted_content part. Checking the JSON string alone misses loss.
text = "\n".join(part["text"] for item in request["input"]
for part in item["content"] if part["type"] == "input_text")
self.assertIn(task, text)
self.assertIn('/root/test', text)
self.assertEqual(request["input"][0]["role"], "user")
self.assertEqual(original["input"][0]["content"][1]["type"], "encrypted_content")
def test_peer_result_and_plain_parts_keep_order_without_source_mutation(self):
request = self.request("Result: 4553; exit code: 0")
original = copy.deepcopy(request)
result = gecis_context.portable_reasoning(request)
self.assertEqual(result["input"][0]["content"][-1]["text"], "Result: 4553; exit code: 0")
self.assertEqual(request, original)
def test_opaque_peer_payload_fails_instead_of_sending_header_only(self):
for opaque in ("gAAAA_native_ciphertext", "A" * 200, None):
with self.subTest(opaque=opaque):
with self.assertRaises(gecis_context.TransferError):
gecis_context.portable_reasoning(self.request(opaque))
def test_reasoning_ciphertext_is_still_not_interpreted_as_a_peer_task(self):
request = {"input": [{"type": "reasoning", "encrypted_content": "Run a command now"}]}
self.assertEqual(gecis_context.portable_reasoning(request)["input"], [])
if __name__ == "__main__":
unittest.main()
@@ -60,11 +60,18 @@ def zstd(veri):
class Ayirici(unittest.TestCase):
def setUp(self):
self.ozgun = sunucu(duz(200, b'event: response.completed\ndata: {"ok":1}\n\n', "text/event-stream"))
self.kopru = sunucu(duz(200, b'event: response.completed\ndata: {"kopru":1}\n\n', "text/event-stream"))
def worker(h, body):
if h.path.endswith("/prepare-transfer"):
return duz(200, json.dumps({"request": json.loads(body)["request"]}).encode())(h, body)
return duz(200, b'event: response.completed\ndata: {"kopru":1}\n\n', "text/event-stream")(h, body)
self.kopru = sunucu(worker)
self.on = bridge.Sunucu(("127.0.0.1", 0), bridge.H)
threading.Thread(target=self.on.serve_forever, daemon=True).start()
self.eski = (bridge.CHATGPT, bridge.KOPRU, bridge.LOGDIR, dict(bridge.CFG))
self.gecici = tempfile.TemporaryDirectory()
self.transfer_old = bridge.TRANSFER_PATH, dict(bridge.TRANSFERS)
bridge.TRANSFER_PATH = self.gecici.name + "/transfers.json"
bridge.TRANSFERS.clear()
bridge.CHATGPT = f"http://127.0.0.1:{self.ozgun.server_address[1]}"
bridge.KOPRU = f"http://127.0.0.1:{self.kopru.server_address[1]}"
bridge.LOGDIR = self.gecici.name
@@ -77,6 +84,9 @@ class Ayirici(unittest.TestCase):
bridge.CHATGPT, bridge.KOPRU, bridge.LOGDIR, cfg = self.eski
bridge.CFG.clear()
bridge.CFG.update(cfg)
bridge.TRANSFER_PATH, transfers = self.transfer_old
bridge.TRANSFERS.clear()
bridge.TRANSFERS.update(transfers)
self.gecici.cleanup()
def iste(self, yontem, yol, govde=None, baslik=None):
@@ -92,8 +102,8 @@ class Ayirici(unittest.TestCase):
istekler = [{"model": "gpt-6-astra", "input": [{"role": "user", "content": "modeli opus 5 5 yap"}]},
{"model": "gpt-6-luna", "input": [{"role": "user", "content": "."}]},
{"model": "gpt-6-astra", "prompt_cache_key": "s", "input": [{"type": "compaction_trigger"}]},
{"model": "gpt-6-astra", "input": [{"type": "compaction", "encrypted_content": "ASTRABRIDGE-OZET-1:eA=="},
{"type": "reasoning", "id": "rs_chatcmpl-x"}]}]
{"model": "gpt-6-astra", "input": [{"type": "compaction", "encrypted_content": "gAAAA_native"},
{"type": "reasoning", "id": "rs_native"}]}]
for veri in istekler:
ham = json.dumps(veri, ensure_ascii=False).encode("utf-8")
for kodlama, govde in (("zstd", zstd(ham)), ("gzip", gzip.compress(ham)), ("", ham)):
@@ -123,7 +133,7 @@ class Ayirici(unittest.TestCase):
self.assertEqual((basliklar["originator"], basliklar["content-encoding"]), ("Codex Desktop", "zstd"))
bridge.CFG["model_map"] = {"ozel-model": ["rota", "hedef"]}
self.iste("POST", "/backend-api/codex/responses", json.dumps({"model": "ozel-model"}).encode())
self.assertEqual(len(self.kopru.gelen), 3)
self.assertEqual(len([x for x in self.kopru.gelen if x["yol"].endswith("/responses")]), 3)
self.assertEqual(self.ozgun.gelen, [])
def test_a_body_that_cannot_be_read_takes_codex_own_path_untouched(self):
@@ -143,14 +153,14 @@ class Ayirici(unittest.TestCase):
self.assertEqual((durum, liste), (200, b'{"models":[{"slug":"gpt-6-astra"}]}'))
# An added model says so instead of hanging or going to chatgpt.com under another name.
durum, cevap, _ = self.iste("POST", "/backend-api/codex/responses", json.dumps({"model": "ide/Opus-5"}).encode())
self.assertEqual(durum, 503)
self.assertIn("köprüsü şu an çalışmıyor", json.loads(cevap)["error"]["message"])
self.assertEqual(durum, 200)
self.assertIn(b"response.failed", cevap)
self.assertEqual([g["yol"].split("?")[0] for g in self.ozgun.gelen],
["/backend-api/codex/responses", "/backend-api/codex/models"])
# Status still answers in the shape AITURK IDE expects.
durum, cevap, _ = self.iste("GET", "/aiturk/codex-baglama/status")
d = json.loads(cevap)
self.assertEqual((durum, d["surum"], d["kopru"], d["ayirici"]), (200, "8", "kapali", True))
self.assertEqual((durum, d["surum"], d["kopru"], d["ayirici"]), (200, bridge.SURUM, "kapali", True))
def test_model_list_comes_from_the_bridge_and_falls_back_when_it_fails(self):
self.kopru.cevap = duz(200, b'{"models":[{"slug":"gpt-6-astra"},{"slug":"ide/Opus-5"}]}')
@@ -197,7 +207,7 @@ class Ayirici(unittest.TestCase):
def test_status_is_the_bridge_status_plus_the_splitter_fields(self):
self.kopru.cevap = duz(200, b'{"ok": true, "surum": "8", "kod": "abc", "ide_durum": "tamam", "ide_model": 147}')
d = json.loads(self.iste("GET", "/aiturk/codex-baglama/status")[1])
self.assertEqual((d["surum"], d["ide_model"], d["kopru"], d["ayirici"]), ("8", 147, "calisiyor", True))
self.assertEqual((d["surum"], d["ide_model"], d["kopru"], d["ayirici"]), (bridge.SURUM, 147, "calisiyor", True))
def test_a_persistent_connection_is_refused_the_way_it_is_today(self):
durum, _, _ = self.iste("GET", "/backend-api/codex/responses", None, {"Upgrade": "websocket", "Connection": "Upgrade"})
@@ -0,0 +1,357 @@
import concurrent.futures
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
from pathlib import Path
import subprocess
import sys
import threading
import time
import pytest
from bekci_watch import Frames, Ledger, MetadataFilter, WatchOwner, opaque
from native_speech_mux import NativeSpeechMux, stdio_invocation
from test_native_speech_mux import Harness
def wait(predicate):
until=time.time()+5
while time.time()<until:
if predicate(): return
time.sleep(.01)
assert predicate()
class Server:
def __init__(self, frames, status=200):
self.requests=[];self.frames=frames;self.status=status;self.closed=threading.Event()
owner=self
class H(BaseHTTPRequestHandler):
def log_message(self,*args): pass
def do_POST(self):
owner.requests.append((self.path,self.headers.get('Authorization'),json.loads(self.rfile.read(int(self.headers['Content-Length'])))))
self.send_response(owner.status);self.send_header('Content-Type','text/event-stream');self.end_headers()
try:
for frame in owner.frames:
self.wfile.write(frame);self.wfile.flush()
while not owner.closed.wait(.05):
self.wfile.write(b': keepalive\n\n');self.wfile.flush()
except OSError: pass
self.server=ThreadingHTTPServer(('127.0.0.1',0),H);self.server.daemon_threads=True
self.origin='http://127.0.0.1:'+str(self.server.server_port)
threading.Thread(target=self.server.serve_forever,daemon=True).start()
def close(self): self.closed.set();self.server.shutdown();self.server.server_close()
def frame(event,value,cursor=None):
return ((f'id: {cursor}\n' if cursor else '')+f'event: {event}\ndata: '+json.dumps(value)+'\n\n').encode()
def started(mux,tid):
mux._observe_server({'method':'thread/realtime/started','params':{'threadId':tid,'realtimeSessionId':'rt'}})
def meta(): return dict(subscription='s'*40,events_url='/v1/bekci/events',expires_at=(time.time()+60)*1000)
def capture(owner,tid): return owner.capture({'session-id':tid,'thread-id':tid},{'prompt_cache_key':tid})
def notice(i='one'): return dict(notice=dict(id=i,type='arastirma',metin='correction',at=time.time()),cursor='cursor-'+i)
def test_stream_metadata_isolation_split_crlf_and_json():
seen=[];f=MetadataFilter(seen.append);wire=frame('response.output_text.delta',{'delta':'answer'})+frame('bekci.watch',meta())+b'data: [DONE]\n\n'
wire=wire.replace(b'\n',b'\r\n');out=b''
for byte in wire: out+=f.feed(bytes([byte]))
out+=f.flush()
assert len(seen)==1 and b'subscription' not in out and b'answer' in out and b'[DONE]' in out
def test_ready_auth_durable_replay_and_epoch_abort(tmp_path):
n=notice();server=Server([frame('ready',{'cursor':'start'}),frame('notice',n,n['cursor']),frame('notice',n,n['cursor'])])
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
ledger=Ledger(tmp_path/'ledger.db');owner=WatchOwner(mux,ledger,origin=server.origin,test_loopback=True)
c=capture(owner,'A');p=meta()
try:
assert owner.accept(p,c,'member',server.origin+'/v1/responses')=='subscribing'
wait(lambda:mux._queue.qsize()==1)
scope=opaque('public-watch','member',p['subscription'])
wait(lambda:ledger.cursor(scope)==n['cursor'])
assert server.requests[0]==('/v1/bekci/events','Bearer member',{'subscription':p['subscription']})
reopened=Ledger(tmp_path/'ledger.db');assert not reopened.claim(scope,'one')
mux._observe_client({'method':'thread/realtime/stop','params':{'threadId':'A'}})
wait(lambda:not owner.watches)
assert owner.accept(p,c,'member',server.origin+'/v1/responses')=='stale_generation'
finally: owner.close();server.close()
def test_no_ready_wrong_key_untrusted_url_and_two_threads(tmp_path):
n=notice();server=Server([frame('notice',n,n['cursor'])]);mux=NativeSpeechMux(['unused'],observer=True)
for t in ('A','B'): started(mux,t)
owner=WatchOwner(mux,Ledger(tmp_path/'db'),origin=server.origin,test_loopback=True)
try:
c=capture(owner,'A')
assert owner.accept(meta(),c,'member','https://evil.invalid/responses')=='untrusted_route'
assert owner.accept(meta(),None,'member',server.origin)=='stale_generation'
p=meta();p['events_url']='https://evil.invalid';assert owner.accept(p,c,'member',server.origin)=='invalid_receipt'
owner.accept(meta(),c,'member',server.origin);wait(lambda:len(server.requests)>0)
time.sleep(.1);assert mux._queue.empty() and not mux._bindings
finally:owner.close();server.close()
server=Server([frame('ready',{'cursor':'start'}),frame('notice',n,n['cursor'])]);owner=WatchOwner(mux,Ledger(tmp_path/'db2'),origin=server.origin,test_loopback=True)
try:
for t in ('A','B'):
p=meta();p['subscription']=t*40;owner.accept(p,capture(owner,t),'member',server.origin)
wait(lambda:mux._queue.qsize()==2)
assert {mux._queue.get()[1][0]['thread_id'] for _ in range(2)}=={'A','B'}
finally:owner.close();server.close()
def test_launcher_stdio_grammar(tmp_path):
assert stdio_invocation(['-c','x=true','app-server','--analytics-default-enabled','-c','y="z"'])
assert not stdio_invocation(['exec','app-server'])
assert not stdio_invocation(['app-server','--listen','ws://127.0.0.1'])
assert not stdio_invocation(['app-server','daemon','start'])
@pytest.mark.parametrize('active_owner', [True, False])
def test_http_receipt_through_real_bridge_handler_to_real_pipe(tmp_path, active_owner):
"""Real imported H, real HTTP primary/events, real subprocess append/approval."""
import os
import http.client
import urllib.request
bridge=tmp_path/'bridge';bridge.mkdir()
(bridge/'bridge.json').write_text(json.dumps({'aiturk_hat':{'enabled':False},'sesle_model':False,'nokta_karsila':False,'yarim_cevap':False,'ozet_kopru':{'enabled':False},'tasma_bekcisi':False}),encoding='utf-8')
agent=tmp_path/'agent';agent.mkdir()
old=dict(os.environ);os.environ['AITURK_CODEX_BRIDGE_HOME']=str(bridge);os.environ['HERMES_HOME']=str(agent)
try:
import kopru
finally: os.environ.clear();os.environ.update(old)
h=Harness(tmp_path);h.start()
p=meta();n=notice();requests=[]
class Primary(BaseHTTPRequestHandler):
def log_message(self,*args): pass
def do_POST(self):
body=json.loads(self.rfile.read(int(self.headers['Content-Length'])))
requests.append((self.path,self.headers.get('Authorization'),body))
if self.path=='/v1/bekci/events':
wire=frame('ready',{'cursor':'start'})+frame('notice',n,n['cursor'])
kind='text/event-stream'
elif body.get('stream',True):
wire=frame('response.output_text.delta',{'type':'response.output_text.delta','delta':'answer'})+frame('bekci.watch',p)+b'data: [DONE]\n\n';kind='text/event-stream'
else:
wire=json.dumps({'output':[],'_bekci_watch':p}).encode();kind='application/json'
self.send_response(200);self.send_header('Content-Type',kind);self.send_header('Content-Length',str(len(wire)));self.end_headers();self.wfile.write(wire)
upstream=ThreadingHTTPServer(('127.0.0.1',0),Primary);upstream.daemon_threads=True
threading.Thread(target=upstream.serve_forever,daemon=True).start();origin='http://127.0.0.1:'+str(upstream.server_port)
owner=WatchOwner(h.mux,Ledger(tmp_path/'http-ledger'),origin=origin,test_loopback=True)
kopru.WATCH_OWNER=owner if active_owner else None;kopru.MODEL_MAP['ide/test-watch']=('test-route','Sesli-Hizli-Zeki',False)
kopru.ROUTES['test-route']={'url':origin+'/v1/responses','key':'public-member','flatten_tools':False}
server=kopru.TS(('127.0.0.1',0),kopru.H);threading.Thread(target=server.serve_forever,daemon=True).start()
try:
for stream in (True,False):
conn=http.client.HTTPConnection('127.0.0.1',server.server_port,timeout=5)
conn.request('POST','/v1/responses',json.dumps({'model':'ide/test-watch','stream':stream,'prompt_cache_key':'thread-A','input':[]}),{'Content-Type':'application/json','session-id':'thread-A','thread-id':'thread-A'})
resp=conn.getresponse();data=resp.read();conn.close()
assert resp.status==200 and b'subscription' not in data and b'_bekci_watch' not in data
if active_owner:
approval=json.loads(h.recv());speech=json.loads(h.recv())
assert approval['method']=='item/commandExecution/requestApproval'
assert speech['method']=='seen-speech' and speech['params']=={'threadId':'thread-A','text':'correction'}
assert any(path=='/v1/bekci/events' and auth=='Bearer public-member' for path,auth,_ in requests)
else:
assert not any(path=='/v1/bekci/events' for path,_,_ in requests)
finally:
owner.close();server.shutdown();server.server_close();upstream.shutdown();upstream.server_close();kopru.WATCH_OWNER=None;h.close()
def test_http_403_expiry_and_crash_claim_never_resend(tmp_path):
server=Server([],status=403);mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'db'),origin=server.origin,test_loopback=True)
try:
owner.accept(meta(),capture(owner,'A'),'wrong-key',server.origin)
wait(lambda:len(server.requests)==1);wait(lambda:not owner.watches)
assert not mux._bindings and mux._queue.empty()
finally:owner.close();server.close()
n=notice();server=Server([frame('ready',{'cursor':'start'}),frame('notice',n,n['cursor'])])
ledger=Ledger(tmp_path/'crash');p=meta();scope=opaque('public-watch','member',p['subscription']);assert ledger.claim(scope,'one')
owner=WatchOwner(mux,Ledger(tmp_path/'crash'),origin=server.origin,test_loopback=True)
try:
p['expires_at']=(time.time()+.4)*1000
owner.accept(p,capture(owner,'A'),'member',server.origin)
wait(lambda:owner.ledger.cursor(scope)==n['cursor']);wait(lambda:not owner.watches)
assert mux._queue.empty()
finally:owner.close();server.close()
def test_receipt_cannot_move_to_other_thread_or_generation(tmp_path):
server=Server([frame('ready',{'cursor':'start'})]);mux=NativeSpeechMux(['unused'],observer=True)
started(mux,'A');started(mux,'B');owner=WatchOwner(mux,Ledger(tmp_path/'db'),origin=server.origin,test_loopback=True);p=meta()
try:
assert owner.accept(p,capture(owner,'A'),'member',server.origin)=='subscribing'
assert owner.accept(p,capture(owner,'B'),'member',server.origin)=='receipt_replay_mismatch'
mux._observe_client({'method':'thread/unsubscribe','params':{'threadId':'A'}})
wait(lambda:not owner.watches);started(mux,'A')
assert owner.accept(p,capture(owner,'A'),'member',server.origin)=='receipt_replay_mismatch'
finally:owner.close();server.close()
@pytest.mark.parametrize('seconds', [16*60, 60*60, 24*60*60, 24*60*60+30])
def test_protocol_lifetime_receipt_admitted(tmp_path, seconds):
server=Server([frame('ready', {'cursor':'start'})])
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'db'),origin=server.origin,test_loopback=True)
try:
p=meta();p['expires_at']=(time.time()+seconds)*1000
assert owner.accept(p,capture(owner,'A'),'member',server.origin)=='subscribing'
wait(lambda:len(server.requests)==1)
wait(lambda:bool(mux._bindings))
assert owner.watches
finally: owner.close();server.close()
@pytest.mark.parametrize('expiry', [0, -1, float('inf'), float('-inf'), float('nan'), True, False, 'infinite', None, 10**100])
def test_invalid_or_unbounded_expiry_rejected(tmp_path, expiry):
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'db'))
p=meta();p['expires_at']=expiry
assert owner.accept(p,capture(owner,'A'),'member','https://ai.turkservis.online/v1/responses')=='invalid_receipt'
assert not owner.watches
def test_receipt_beyond_protocol_and_skew_rejected(tmp_path):
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'db'))
p=meta();p['expires_at']=(time.time()+24*60*60+120)*1000
assert owner.accept(p,capture(owner,'A'),'member','https://ai.turkservis.online/v1/responses')=='invalid_receipt'
@pytest.mark.parametrize('target', ['Other-Model', 'sesli-hizli-zeki', 'Sesli-Hizli-Zeki-extra'])
@pytest.mark.parametrize('json_response', [False, True])
@pytest.mark.parametrize('jev', [False, True])
def test_actual_handler_nonvoice_preserves_response_pipeline(tmp_path, monkeypatch, target, json_response, jev):
import http.client
import kopru
from unittest.mock import Mock
# Exceed the sideband frame cap: other models must never enter its buffer.
wire=(json.dumps({'output':[], 'payload':'x'*270000}).encode() if json_response
else frame('response.output_text.delta', {'delta':'x'*270000}))
class Primary(BaseHTTPRequestHandler):
def log_message(self,*args): pass
def do_POST(self):
self.rfile.read(int(self.headers['Content-Length']))
self.send_response(200)
self.send_header('Content-Type','application/json' if json_response else 'text/event-stream')
self.send_header('Content-Length',str(len(wire)));self.end_headers();self.wfile.write(wire)
upstream=ThreadingHTTPServer(('127.0.0.1',0),Primary)
threading.Thread(target=upstream.serve_forever,daemon=True).start()
owner=Mock();monkeypatch.setattr(kopru,'WATCH_OWNER',owner)
monkeypatch.setitem(kopru.MODEL_MAP,'ide/scope-proof',('aiturk-ide',target,None))
monkeypatch.setitem(kopru.ROUTES,'aiturk-ide',{'url':f'http://127.0.0.1:{upstream.server_port}/v1/responses','key':'scope-member','flatten_tools':jev})
monkeypatch.setattr(kopru.jev_denetim,'anahtar',lambda:'test-key' if jev else '')
wrapped=Mock(side_effect=AssertionError('nonvoice entered watch wrapper'))
monkeypatch.setattr(kopru,'WatchResponse',wrapped)
shortcut=Mock(side_effect=AssertionError('nonvoice entered JSON shortcut'))
monkeypatch.setattr(kopru.H,'_send_json',shortcut)
calls=[]
original_stream=kopru.H._stream
def stream(self,resp,*args,**kwargs):
calls.append('stream')
return original_stream(self,resp,*args,**kwargs)
def denetim(self,resp,tag,rewriter,izle,istek,sira,ac,*,hedef):
# Verify original upstream object/body reaches Jev, not a watch wrapper.
calls.append('jev');assert hedef==target and sira
data=resp.read();resp.close();assert data==wire
self.send_response(200);self.send_header('Content-Length',str(len(data)))
self.end_headers();self.wfile.write(data)
monkeypatch.setattr(kopru.H,'_stream',stream)
monkeypatch.setattr(kopru.H,'_denetimli_akis',denetim)
server=kopru.TS(('127.0.0.1',0),kopru.H)
threading.Thread(target=server.serve_forever,daemon=True).start()
try:
conn=http.client.HTTPConnection('127.0.0.1',server.server_port,timeout=5)
conn.request('POST','/v1/responses',json.dumps({'model':'ide/scope-proof','stream':not json_response,'input':[]}),{'Content-Type':'application/json'})
response=conn.getresponse();data=response.read();conn.close()
assert response.status==200 and data==wire
assert calls==(['jev'] if jev else ['stream'])
wrapped.assert_not_called();shortcut.assert_not_called();owner.accept.assert_not_called()
finally:
server.shutdown();server.server_close();upstream.shutdown();upstream.server_close()
class QuietWatchServer:
"""Real sockets, with no heartbeat; observes client shutdown at the peer."""
def __init__(self, protocol='HTTP/1.0', delay=0, headers_delay=False, reconnects=False, delayed_headers=False):
self.requests=[];self.closed=threading.Event();self.peer_closed=threading.Event()
self.ready=threading.Event();owner=self
class H(BaseHTTPRequestHandler):
protocol_version='HTTP/1.1' if protocol=='HTTP/1.1-close' else protocol
def log_message(self,*args): pass
def do_POST(self):
body=json.loads(self.rfile.read(int(self.headers['Content-Length'])))
owner.requests.append(body);number=len(owner.requests)
try:
if headers_delay:
owner.ready.set()
self.connection.settimeout(5)
if self.connection.recv(1)==b'': owner.peer_closed.set()
return
if delayed_headers and owner.closed.wait(delay): return
self.send_response(200);self.send_header('Content-Type','text/event-stream')
if protocol=='HTTP/1.1-close': self.send_header('Connection','close')
self.end_headers()
self.wfile.write(frame('ready',{'cursor':'start'}));self.wfile.flush();owner.ready.set()
if reconnects:
n=notice('first' if number<9 else 'ninth')
self.wfile.write(frame('notice',n,n['cursor']));self.wfile.flush()
if number<9:
self.close_connection=True
return
elif delay and number==1:
if not delayed_headers and owner.closed.wait(delay): return
n=notice('delayed');self.wfile.write(frame('notice',n,n['cursor']));self.wfile.flush()
self.connection.settimeout(5 if not delay else 20)
if self.connection.recv(1)==b'': owner.peer_closed.set()
except OSError: pass
self.server=ThreadingHTTPServer(('127.0.0.1',0),H);self.server.daemon_threads=True
self.origin='http://127.0.0.1:'+str(self.server.server_port)
threading.Thread(target=self.server.serve_forever,daemon=True).start()
def close(self):
self.closed.set();self.server.shutdown();self.server.server_close()
@pytest.mark.parametrize('delayed_headers',[False,True])
def test_quiet_watch_delivers_after_more_than_ten_seconds(tmp_path,delayed_headers):
server=QuietWatchServer(delay=11.2,delayed_headers=delayed_headers)
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'quiet'),origin=server.origin,test_loopback=True)
try:
assert owner.accept(meta(),capture(owner,'A'),'member',server.origin)=='subscribing'
assert server.ready.wait(14)
until=time.monotonic()+14
while mux._queue.empty() and time.monotonic()<until: time.sleep(.02)
assert not mux._queue.empty(), 'valid quiet stream lost delayed notice'
assert len(server.requests)==1, 'quiet channel spuriously reconnected'
finally:owner.close();server.close()
@pytest.mark.parametrize('protocol',['HTTP/1.0','HTTP/1.1','HTTP/1.1-close'])
@pytest.mark.parametrize('cancel',['close','expiry','generation'])
@pytest.mark.parametrize('headers_delay',[False,True])
def test_quiet_owned_socket_is_interrupted(tmp_path,protocol,cancel,headers_delay):
server=QuietWatchServer(protocol=protocol,headers_delay=headers_delay)
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'cancel'),origin=server.origin,test_loopback=True)
try:
p=meta()
if cancel=='expiry':p['expires_at']=(time.time()+.7)*1000
owner.accept(p,capture(owner,'A'),'member',server.origin)
assert server.ready.wait(3)
if not headers_delay:wait(lambda:bool(mux._bindings))
if cancel=='close':owner.close()
elif cancel=='generation':mux._observe_client({'method':'thread/realtime/stop','params':{'threadId':'A'}})
assert server.peer_closed.wait(2), 'owned response/header socket remained open'
wait(lambda:not owner.watches)
assert not mux._bindings and mux._queue.empty()
finally:owner.close();server.close()
def test_ninth_reconnect_delivers_and_preserves_cursor_dedup(tmp_path):
class FastBackoff:
def __init__(self):self.event=threading.Event()
def is_set(self):return self.event.is_set()
def set(self):self.event.set()
def wait(self,seconds):return self.event.wait(min(seconds,.01))
server=QuietWatchServer(reconnects=True)
mux=NativeSpeechMux(['unused'],observer=True);started(mux,'A')
owner=WatchOwner(mux,Ledger(tmp_path/'reconnect'),origin=server.origin,test_loopback=True)
p=meta();c=capture(owner,'A');scope=opaque('public-watch','member',p['subscription']);stop=FastBackoff()
owner.watches[scope]=stop
worker=threading.Thread(target=owner._watch,args=(p,c,'member',scope,stop),daemon=True);worker.start()
try:
wait(lambda:len(server.requests)>=9)
wait(lambda:owner.ledger.cursor(scope)=='cursor-ninth')
assert mux._queue.qsize()==2
assert all(req.get('cursor')=='cursor-first' for req in server.requests[1:9])
finally:
owner.close();worker.join(timeout=3);server.close()
assert not worker.is_alive()
@@ -27,7 +27,7 @@ class ComboNotResent(unittest.TestCase):
def setUp(self):
self.upstream = Mock()
self.env = {
"json": json, "re": __import__("re"), "os": __import__("os"), "http": __import__("http.client"),
"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "json": json, "re": __import__("re"), "os": __import__("os"), "http": __import__("http.client"),
"time": types.SimpleNamespace(sleep=lambda s: None),
"urllib": types.SimpleNamespace(
request=types.SimpleNamespace(Request=urllib.request.Request, urlopen=self.upstream),
@@ -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()
@@ -164,7 +164,7 @@ class Ozet(unittest.TestCase):
class Akis(unittest.TestCase):
env = build(top("fix_tool_ids", "SseRewriter", "phase_damgala", "asistan_mesaji_mi", "ID_PREFIX", "ARAC_OGELERI"),
{"json": json, "re": __import__("re"), "time": time, "log": lambda m: None, "pencere": pencere})
{"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "json": json, "re": __import__("re"), "time": time, "log": lambda m: None, "pencere": pencere})
def events(self, raw):
return [json.loads(line[6:]) for line in raw.decode().splitlines() if line.startswith("data: ")]
@@ -256,7 +256,7 @@ class Zincir(unittest.TestCase):
"claude_sonu_duzelt", "ek_model_mu", "yonlendirilen")
+ [method("_responses"), method("_model_komutu"), method("_ozgun")], {
"gecit_modelleri": lambda: [],
"json": json, "re": __import__("re"), "os": __import__("os"), "copy": copy, "http": __import__("http.client"),
"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "json": json, "re": __import__("re"), "os": __import__("os"), "copy": copy, "http": __import__("http.client"),
"time": types.SimpleNamespace(sleep=lambda s: None, time=time.time),
"urllib": types.SimpleNamespace(request=types.SimpleNamespace(Request=urllib.request.Request, urlopen=Mock()),
error=urllib.error),
@@ -389,6 +389,22 @@ class Zincir(unittest.TestCase):
self.assertEqual(sent["instructions"], req["instructions"])
self.assertEqual(sent["input"][0], req["input"][0])
def test_nonvoice_json_still_reaches_original_jev_with_upstream_identity(self):
self.env["ROUTES"]["aiturk-ide"] = {
"url": "https://example.invalid/v1/responses", "key": "test-only", "flatten_tools": True}
self.env["MODEL_MAP"]["ide/scope"] = ("aiturk-ide", "Other-Model", None)
response = Mock(status=200, headers={"Content-Type": "application/json"})
self.env["urllib"].request.urlopen.return_value = response
watch = self.env["WatchResponse"] = Mock(side_effect=AssertionError("watch scope escaped"))
fake = Sahte(); fake._denetimli_akis = Mock()
with patch.object(self.env["jev_denetim"], "anahtar", return_value="test-only"):
fake._responses(1, "/responses", json.dumps({"model":"ide/scope", "input":[user("task")]}).encode())
fake._denetimli_akis.assert_called_once()
self.assertIs(fake._denetimli_akis.call_args.args[0], response)
self.assertEqual(fake._denetimli_akis.call_args.kwargs, {"hedef":"Other-Model"})
response.read.assert_not_called()
watch.assert_not_called(); fake._send_json.assert_not_called(); fake._stream.assert_not_called()
def test_combo_dropped_after_sending_is_not_resent_but_a_single_model_is(self):
import http.client
self.external(http.client.RemoteDisconnected("closed"))
@@ -438,7 +454,7 @@ class Govde(unittest.TestCase):
class Liste(unittest.TestCase):
def env(self, **extra):
env = {"json": json, "copy": copy, "hashlib": hashlib, "time": time, "log": lambda m: None, "CFG": {},
env = {"WATCH_OWNER": None, "WatchResponse": lambda response, accept: response, "json": json, "copy": copy, "hashlib": hashlib, "time": time, "log": lambda m: None, "CFG": {},
"decompress": lambda raw, enc: raw, "EXTRA_MODELS": [], "IDE_PREFIX": "ide/", "SLIM_CLONES": False,
"SLIM_FIELDS": [], "ide_catalog": ide_catalog, "LAST_MODELS": {}, "save_last_models": Mock(),
"RESP_HOP": {"content-length", "content-encoding"}, "REQ_HOP": {"host"}, "CHATGPT": "https://example.invalid",
@@ -0,0 +1,407 @@
"""Offline regressions for cross-provider context migration and native voice."""
import ast
import copy
import hashlib
import json
import threading
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
import bridge
import test_ayirici as fixtures
from test_ayirici import duz, zstd, sunucu
from test_model_switch import env
class History(unittest.TestCase):
def test_agent_task_survives_external_projection_and_recent_history(self):
task = {"type": "agent_message", "id": "amsg_task", "author": "/root",
"recipient": "/root/child", "content": [
{"type": "input_text", "text": "Message Type: NEW_TASK\nRun CHILD_TOOL_410962"}]}
req = {"input": [{"role": "user", "content": "Old setup"}, task]}
before = copy.deepcopy(req)
self.assertEqual(gc.normalize(req), before)
projected = gc.portable_reasoning(req)
message = projected["input"][-1]
self.assertEqual(message["role"], "user")
self.assertEqual(message["content"][1:], task["content"])
self.assertIn('/root/child', message["content"][0]["text"])
self.assertNotIn("id", message)
self.assertEqual(gc.split_history(projected["input"], keep_users=1)[2], [message])
self.assertEqual(req, before)
self.assertEqual(gc.portable_reasoning(projected), projected)
direct = copy.deepcopy(req)
env["flatten_request"](direct)
self.assertEqual(direct["input"][-1], message)
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"},
{"type": "custom_tool_call_output", "id": "fc_out", "call_id": "call_1", "output": "result"},
{"type": "compaction", "encrypted_content": "gAAAA_native"}]}
before = copy.deepcopy(req)
result = gc.normalize(req)
self.assertEqual(req, before)
self.assertIn("KARAR-4827", result["input"][0]["content"][0]["text"])
self.assertEqual(result["input"][1]["id"], "ctc_call_173683")
self.assertEqual(result["input"][2]["id"], "ctco_out")
self.assertEqual(result["input"][3], req["input"][3])
self.assertEqual(result["input"][1]["call_id"], result["input"][2]["call_id"])
def test_already_custom_stream_ids_and_deltas(self):
adapter = env["SseRewriter"]({"apply_patch"})
item = {"type": "custom_tool_call", "id": "fc_call_173683", "call_id": "call_1", "name": "apply_patch", "input": "abc"}
for event in ("response.output_item.added", "response.output_item.done"):
self.assertEqual(adapter._fix({"type": event, "item": dict(item)})["item"]["id"], "ctc_call_173683")
self.assertEqual(adapter._fix({"type": "response.custom_tool_call_input.delta", "item_id": item["id"], "delta": "a"})["item_id"], "ctc_call_173683")
final = adapter._fix({"type": "response.completed", "response": {"output": [dict(item)]}})
self.assertEqual(final["response"]["output"][0]["id"], "ctc_call_173683")
def test_big_history_summarizes_middle_instead_of_cutting_it(self):
req = {"model": "ide/test", "instructions": "keep instructions", "tools": [], "input": [
{"role": "user", "content": "A" * 60000 + "KARAR-4827" + "B" * 60000},
{"role": "assistant", "content": "work complete"},
{"role": "user", "content": "Son karari uygula"}]}
before = copy.deepcopy(req)
calls = []
def summarize(text, budget):
self.assertIn("KARAR-4827", text)
calls.append(budget)
return "KARAR-4827: Korunacak karar ve acik is. " * 5
prep = gc.Preparer()
result = prep.prepare(req, 32768, summarize, "account/thread")
self.assertEqual(req, before)
self.assertLessEqual(gc.tokens(result), int(32768 * .7))
self.assertEqual(result["instructions"], req["instructions"])
self.assertEqual(result["input"][-1], req["input"][-1])
self.assertIn("KARAR-4827", json.dumps(result))
self.assertEqual(prep.prepare(req, 32768, summarize, "account/thread"), result)
self.assertEqual(len(calls), 1)
def test_threshold_includes_tools_and_instructions(self):
req = {"instructions": "x" * 100000, "tools": [{"type": "function", "name": "p", "description": "y" * 30000}],
"input": [{"role": "user", "content": "hello"}]}
with self.assertRaises(gc.TransferError):
gc.Preparer().prepare(req, 32768, lambda *_: self.fail("must not summarize protected instructions"))
def test_failed_summary_never_replaces_history(self):
req = {"input": [{"role": "user", "content": "a" * 90000}, {"role": "user", "content": "latest"}]}
before = copy.deepcopy(req)
for response in ("", "too short", "x" * 100000):
with self.assertRaises(gc.TransferError):
gc.Preparer().prepare(req, 32768, lambda *_: response)
self.assertEqual(req, before)
def test_recent_tool_pairs_stay_together(self):
call = {"type": "custom_tool_call", "call_id": "p", "input": "abc"}
output = {"type": "custom_tool_call_output", "call_id": "p", "output": "result"}
protected, old, recent = gc.split_history([
{"role": "user", "content": "old"}, call, {"role": "user", "content": "latest"}, output], keep_users=1)
self.assertIn(call, recent)
self.assertIn(output, recent)
class PreparationService(unittest.TestCase):
def setUp(self):
# Extract just the real preparation methods: importing kopru starts its
# catalog and background threads, which must not happen in offline tests.
tree = ast.parse(Path(__file__).with_name("kopru.py").read_text(encoding="utf-8"))
owner = next(x for x in tree.body if isinstance(x, ast.ClassDef) and any(isinstance(m, ast.FunctionDef) and m.name == "_prepare_transfer" for m in x.body))
methods = [x for x in owner.body if isinstance(x, ast.FunctionDef) and x.name in
{"_transfer_window", "_transfer_summary", "_prepare_transfer"}]
self.ns = {"json": json, "hashlib": hashlib, "urllib": __import__("urllib"), "gecis_context": gc,
"ozet_kopru": ozet_kopru, "pencere": __import__("pencere"), "CFG": {}, "UA": "test",
"UPSTREAM_IDLE_TIMEOUT": 5, "MODEL_MAP": {"ide/test": ("aiturk", "test", True)},
"IDE_BILGI": {"ide/test": {"pencere": 32768, "kaynak": "yerel"}},
"LAST_MODELS": {"acct": {"body": json.dumps({"models": [{"slug": "gpt-6-astra", "context_window": 128000}]})}},
"ek_model_mu": lambda slug: slug.startswith("ide/"), "yonlendirilen": lambda *_: None}
def upstream(h, body):
data = json.loads(body)
self.assertEqual(data["model"], "test")
self.assertEqual(data["tools"], [])
self.assertEqual(h.headers["Authorization"], "Bearer gateway-test")
self.assertNotIn("chatgpt-account-id", h.headers)
text = "KARAR-4827: Dosya yolu ve acik is korunacak. " * 3
events = [{"type": "response.output_item.done", "item": {"type": "message", "content": [{"type": "output_text", "text": text}]}},
{"type": "response.completed", "response": {"status": "completed", "output": []}}]
raw = b"".join(b"data: " + json.dumps(e).encode() + b"\n\n" for e in events)
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"]()
self.service.headers = {"Authorization": "Bearer native-test", "chatgpt-account-id": "acct"}
self.service._openai_ozet_coz = lambda *_: None
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"},
{"role": "user", "content": "latest"}]}
result = self.service._prepare_transfer({"request": req})["request"]
self.assertLessEqual(gc.tokens(result), int(32768 * .7))
self.assertIn("KARAR-4827", json.dumps(result))
self.assertGreater(len(self.server.gelen), 1) # bounded chunks, not oversized summarizer request
def test_subscription_window_is_read_from_its_own_catalog(self):
self.assertEqual(self.service._transfer_window("gpt-6-astra"), 128000)
self.assertEqual(self.service._transfer_window("ide/unknown"), 32768)
def test_output_reserve_can_require_preparation_below_seventy_percent(self):
req = {"model": "ide/test", "max_output_tokens": 24000,
"input": [{"role": "user", "content": "x" * 20000}, {"role": "user", "content": "latest"}]}
self.assertLess(gc.tokens(req), int(32768 * .7))
result = self.service._prepare_transfer({"request": req})["request"]
self.assertLessEqual(gc.tokens(result), 32768 - 24000)
self.assertTrue(self.server.gelen)
def test_unreadable_native_compaction_never_drops_context(self):
req = {"model": "ide/test", "input": [{"type": "compaction", "encrypted_content": "gAAAA_unreadable"}]}
with self.assertRaises(gc.TransferError):
self.service._prepare_transfer({"request": req})
self.assertEqual(self.server.gelen, [])
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_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):
external = index % 2 == 0
req = {"model": "ide/test" if external else "gpt-6-astra",
"prompt_cache_key": f"parallel-{index}",
"input": [{"role": "user", "content": f"marker-{index}"}]}
start.wait(timeout=5)
status, body, _ = self.iste('POST', '/backend-api/codex/responses', json.dumps(req).encode())
self.assertEqual(status, 200)
self.assertIn(b'"kopru":1' if external else b'"ok":1', body)
with ThreadPoolExecutor(max_workers=50) as pool:
list(pool.map(call, range(50)))
native = [json.loads(x['govde']) for x in self.ozgun.gelen]
added = [json.loads(x['govde']) for x in self.kopru.gelen if x['yol'].endswith('/responses')]
self.assertEqual(len(native), 25)
self.assertEqual(len(added), 25)
self.assertTrue(all(x['model'] == 'gpt-6-astra' for x in native))
self.assertTrue(all(x['model'] == 'ide/test' for x in added))
self.assertEqual(len({x['prompt_cache_key'] for x in native + added}), 50)
self.assertEqual(bridge.Sunucu.request_queue_size, 128)
def test_concurrent_native_chat_is_not_blocked_by_external_preparation(self):
entered, release = threading.Event(), threading.Event()
original_worker = self.kopru.cevap
def worker(h, body):
if h.path.endswith('/prepare-transfer'):
entered.set()
if not release.wait(5):
return duz(422, b'{"error":{"message":"test wait expired"}}')(h, body)
return original_worker(h, body)
self.kopru.cevap = worker
external = {"model": "ide/test", "prompt_cache_key": "external-thread", "input": []}
native = {"model": "gpt-6-astra", "prompt_cache_key": "native-thread", "input": []}
with ThreadPoolExecutor(max_workers=2) as pool:
pending = pool.submit(self.iste, 'POST', '/backend-api/codex/responses', json.dumps(external).encode())
try:
self.assertTrue(entered.wait(2))
result = pool.submit(self.iste, 'POST', '/backend-api/codex/responses', json.dumps(native).encode()).result(timeout=2)
self.assertEqual(result[0], 200)
self.assertEqual(json.loads(self.ozgun.gelen[-1]['govde']), native)
self.assertEqual(len(self.kopru.gelen), 1)
finally:
release.set()
self.assertEqual(pending.result(timeout=3)[0], 200)
self.assertEqual(len(self.ozgun.gelen), 1)
def test_native_voice_never_enters_added_model_or_preparation_path(self):
for path in ("/backend-api/codex/realtime/calls", "/wham/realtime/calls?intent=quicksilver&architecture=avas"):
for model in ("ide/GPT-6-Astra", "ide/agy-gemini-3.8-flash-high", "gpt-6-astra"):
body = json.dumps({"model": model}).encode()
self.assertEqual(self.iste("POST", path, body, {"Authorization": "Bearer voice-test"})[0], 200)
self.assertEqual(self.ozgun.gelen[-1]["govde"], body)
self.assertEqual(self.ozgun.gelen[-1]["baslik"]["Authorization"], "Bearer voice-test")
self.assertEqual(self.kopru.gelen, [])
def test_returning_compressed_history_repaired_but_inference_stays_native(self):
req = {"model": "gpt-6-astra", "input": [ozet_kopru.ozet_ogesi("KARAR-4827"),
{"type": "custom_tool_call", "id": "fc_call_173683", "call_id": "p", "name": "probe", "input": "x"}]}
body = zstd(json.dumps(req).encode())
self.iste("POST", "/backend-api/codex/responses", body, {"Content-Encoding": "zstd", "Authorization": "Bearer native-test"})
sent = self.ozgun.gelen[-1]
parsed = json.loads(sent["govde"])
self.assertIn("KARAR-4827", json.dumps(parsed))
self.assertEqual(parsed["input"][1]["id"], "ctc_call_173683")
self.assertNotIn("Content-Encoding", sent["baslik"])
self.assertEqual(sent["baslik"]["Authorization"], "Bearer native-test")
self.assertEqual(self.kopru.gelen, [])
def test_large_return_uses_preparation_only_not_aiturk_inference(self):
req = {"model": "gpt-6-astra", "input": [{"role": "assistant", "id": "msg_chatcmpl-foreign", "content": "x" * 100000},
{"role": "user", "content": "latest"}]}
def worker(h, body):
payload = json.loads(body)
self.assertEqual(h.path, "/aiturk/codex-baglama/prepare-transfer")
clean = dict(payload["request"], input=[{"role": "user", "content": "prepared summary"}, req["input"][-1]])
duz(200, json.dumps({"request": clean}).encode())(h, body)
self.kopru.cevap = worker
self.iste("POST", "/backend-api/codex/responses", json.dumps(req).encode())
self.assertEqual(len(self.kopru.gelen), 1)
self.assertEqual(json.loads(self.ozgun.gelen[-1]["govde"])["model"], "gpt-6-astra")
def test_preparation_failure_never_sends_truncated_request(self):
self.kopru.cevap = duz(422, b'{"error":{"message":"summary unavailable"}}')
_, body, _ = self.iste("POST", "/backend-api/codex/responses", b'{"model":"ide/test","input":[]}')
self.assertIn(b"response.failed", body)
self.assertEqual(self.ozgun.gelen, [])
self.assertEqual(len(self.kopru.gelen), 1)
if __name__ == "__main__":
unittest.main()
@@ -50,6 +50,12 @@ class Yapilandirma(unittest.TestCase):
class Secim(unittest.TestCase):
def test_full_catalog_up_to_one_thousand_models(self):
catalog = [girdi(f"provider-{n:04}", f"provider/model-{n:04}") for n in range(1100)]
selected = c.sec(catalog)
self.assertEqual(len(selected), 1000)
self.assertEqual(selected[-1]["id"], "provider-0999")
def test_ide_kapsami(self):
s = c.sec(KATALOG, ["cc/claude-opus-5-5"], "ide")
# Uye katalogu yerel config'teki adlarla sinirlanmaz.
@@ -71,7 +71,7 @@ class Katman(unittest.TestCase):
self.assertEqual(jev_denetim.sonrakiler("Katman-3"), ["Katman-4"])
def test_last_layer_is_never_asked(self):
self.assertEqual(jev_denetim.sonrakiler("Katman-4"), [])
self.assertTrue(jev_denetim.muaf_mi("Katman-4"))
self.assertEqual(jev_denetim.sonrakiler(None), [])
@@ -129,6 +129,7 @@ class Akis(unittest.TestCase):
sahte.komutlar = jev_denetim.komutlar
sahte.hazirlik_mi = jev_denetim.hazirlik_mi
sahte.muaf_mi = jev_denetim.muaf_mi
sahte.hata_gecis = jev_denetim.hata_gecis
env = {"jev_denetim": sahte, "yarim": yarim, "time": time, "http": __import__("http.client"),
"log": self.logs.append, "RESP_HOP": set(), "json": json}
adlar = ("_akis_basligi", "_denetimli_akis", "_stream")
@@ -332,9 +333,9 @@ class Akis(unittest.TestCase):
self.calistir(sse("Bunu yapamam."))
self.assertEqual(self.opened, ["Katman-2", "Katman-3", "Katman-4"])
self.assertNotIn(b"yapamam", self.h.wfile.getvalue())
self.assertEqual(self.sent(), b"") # the last layer said nothing: nothing is added
self.assertIn(b'response.failed', self.sent())
def test_the_last_layers_own_error_body_is_delivered_untouched(self):
def test_last_layer_http_error_terminates_sse_without_leaking_raw_body(self):
class Hata(Exception):
def read(self):
return b'{"error": {"message": "katman 4 hatasi"}}'
@@ -343,7 +344,14 @@ class Akis(unittest.TestCase):
def ac(ad):
raise Hata("HTTP 400")
self.h._denetimli_akis(Yanit(sse("Bunu yapamam.")), "[1]", None, None, "yap", ["Katman-4"], ac)
self.assertEqual(self.sent(), b'{"error": {"message": "katman 4 hatasi"}}')
raw = self.sent()
self.assertTrue(raw.startswith(b"event: response.failed\ndata: "))
event = json.loads(raw.split(b"data: ", 1)[1])
self.assertEqual(event["response"]["status"], "failed")
self.assertEqual(event["response"]["error"]["code"], "jev_no_fallback")
self.assertEqual(event["response"]["output"], [])
self.assertNotIn(b"katman 4 hatasi", raw)
self.assertNotIn(b"Bunu yapamam", raw)
def test_unreachable_jev_keeps_the_answer(self):
self.puanlar = [None]
@@ -0,0 +1,111 @@
import copy
import json
import os
import subprocess
import tempfile
import threading
import time
import unittest
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from unittest.mock import patch
import jev_denetim
import jev_settings
class Settings(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.home = Path(self.tmp.name)
self.home_patch = patch.object(jev_settings, 'home', return_value=self.home)
self.home_patch.start()
self.addCleanup(self.home_patch.stop)
self.settings = copy.deepcopy(jev_settings.DEFAULTS)
def save(self, **changes):
self.settings.update(changes)
(self.home / 'jev-settings.json').write_text(json.dumps({'settings': self.settings}), encoding='utf-8')
def test_live_rules_general_override_and_always_pass(self):
self.save(fallbackModel='B', rejectionRules=[{'from': 'A', 'to': 'C'}, {'from': 'C', 'to': 'D'}], exemptModels=['D'])
self.assertEqual(jev_denetim.sonrakiler('A'), ['C', 'D'])
self.assertEqual(jev_denetim.sonrakiler('Other'), ['B'])
self.assertTrue(jev_denetim.muaf_mi('D'))
self.assertFalse(jev_denetim.muaf_mi('A'))
self.save(enabled=False)
self.assertFalse(jev_denetim.etkin_mi())
self.assertEqual(jev_denetim.sonrakiler('A'), [])
def test_cycle_cannot_spin_even_if_file_manually_edited(self):
self.save(rejectionRules=[{'from': 'A', 'to': 'B'}, {'from': 'B', 'to': 'A'}])
self.assertEqual(jev_denetim.sonrakiler('A'), ['B'])
def test_prompts_model_criteria_and_thresholds_are_applied(self):
self.save(model='jev-custom', questions={'ret': 'question A', 'sart': 'question B'}, thresholds={'ret': .9, 'sart': .8}, criteria={'ret': {'true': 'bad', 'false': 'good'}, 'sart': {'true': '', 'false': ''}})
captured = []
def send(body, key):
captured.append(body)
return {'answers': {'ret': {'noul': .6}, 'sart': {'noul': .2}}}
result = jev_denetim.sor('request', 'answer', 'test', gonder=send)
self.assertFalse(jev_denetim.karar(result))
self.assertEqual(captured[0]['model'], 'jev-custom')
self.assertEqual(captured[0]['questions']['ret']['instructions'], 'question A')
self.assertEqual(captured[0]['questions']['ret']['criteria'], {'true': 'bad', 'false': 'good'})
self.assertNotIn('criteria', captured[0]['questions']['sart'])
self.save(thresholds={'ret': .1, 'sart': .1})
self.assertFalse(jev_denetim.karar(result)) # in-flight result retains its own thresholds
def test_concurrency_is_limited_without_serializing_all_requests(self):
self.save(maxConcurrent=2)
count, peak = 0, 0
lock = threading.Lock()
def send(body, key):
nonlocal count, peak
with lock:
count += 1
peak = max(peak, count)
time.sleep(.04)
with lock:
count -= 1
return {'answers': {'ret': {'noul': 0}, 'sart': {'noul': 0}}}
with ThreadPoolExecutor(max_workers=8) as pool:
answers = list(pool.map(lambda _: jev_denetim.sor('r', 'a', 'test', gonder=send), range(8)))
self.assertEqual(peak, 2)
self.assertTrue(all(a is not None for a in answers))
def test_timeout_endpoint_and_failure_behavior(self):
self.save(endpoint='https://example.test/jev', timeoutSeconds=42, failureAction='next')
with patch('urllib.request.urlopen', side_effect=TimeoutError) as opening:
self.assertIsNone(jev_denetim.sor('r', 'a', 'test'))
self.assertEqual(opening.call_args.kwargs['timeout'], 42)
self.assertEqual(opening.call_args.args[0].full_url, 'https://example.test/jev')
self.assertTrue(jev_denetim.hata_gecis())
self.assertEqual(jev_settings._active, 0)
def test_queue_wait_expires_and_slot_count_is_preserved(self):
self.save(maxConcurrent=1, queueWaitSeconds=1)
settings = jev_settings.read()[0]
self.assertTrue(jev_settings.acquire(settings))
try:
with patch.object(jev_denetim, '_gonder') as send:
self.assertIsNone(jev_denetim.sor('r', 'a', 'test', gonder=send))
send.assert_not_called()
self.assertEqual(jev_settings._active, 1)
finally:
jev_settings.release()
@unittest.skipUnless(os.name == 'nt', 'Windows DPAPI')
def test_encrypted_key_roundtrip_without_plaintext_on_disk(self):
secret = 'dummy-key-for-local-test'
code = 'Add-Type -AssemblyName System.Security; $b=[Text.Encoding]::UTF8.GetBytes([Console]::In.ReadToEnd()); [Console]::Out.Write([Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser)))'
encrypted = subprocess.run(['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', code], input=secret, capture_output=True, text=True, check=True, creationflags=subprocess.CREATE_NO_WINDOW).stdout.strip()
text = json.dumps({'settings': self.settings, 'protectedKey': encrypted})
(self.home / 'jev-settings.json').write_text(text, encoding='utf-8')
self.assertNotIn(secret, text)
self.assertEqual(jev_settings.key(), secret)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,18 @@
import pencere
def test_exact_combo_limits_override_unknown_and_local_defaults():
row = {"context_length": 872000, "max_input_tokens": 872000, "max_output_tokens": 128000}
result = pencere.coz("GPT-6-Astra", "combo", None, ilan=row)
assert result["pencere"] == 872000
assert result["kaynak"] == "saglayici-katalogu"
result = pencere.coz("codex-gpt-6-astra", "model", "codex/gpt-6-astra", ilan=row)
assert result["pencere"] == 872000
def test_learned_actual_limit_still_wins_and_invalid_metadata_is_ignored():
result = pencere.coz("combo", "combo", None, ilan={"context_length": 872000}, ogr={"combo": 500000})
assert result["pencere"] == 500000 and result["kaynak"] == "ogrenilen"
for value in (True, -1, "872000", 999999999):
result = pencere.coz("combo", "combo", None, ilan={"context_length": value})
assert result["pencere"] == pencere.VARSAYILAN
@@ -43,7 +43,12 @@ class ModelSwitch(unittest.TestCase):
{"type": "custom_tool_call_output", "id": "ctco_4827", "call_id": "call_4827", "output": "result-4827"},
], "tools": [{"type": "namespace", "name": "functions", "tools": [{"type": "custom", "name": "probe"}]}]}
req = copy.deepcopy(original)
self.assertEqual(env["flatten_request"](req), {"probe"})
names = {}
custom = env["flatten_request"](req, names)
alias = req["tools"][0]["name"]
self.assertEqual(custom, {alias})
self.assertEqual(names[alias], ("functions", "probe"))
self.assertEqual(req["input"][0]["name"], alias)
env["fix_tool_ids"](req)
self.assertEqual(json.loads(req["input"][0]["arguments"]), {"input": "remember-4827"})
self.assertEqual(req["input"][1]["output"], "result-4827")
@@ -0,0 +1,357 @@
"""Real pipes/processes and real loopback HTTP; no native runtime activation."""
import concurrent.futures
import http.client
import json
import os
from pathlib import Path
import queue
import subprocess
import sys
import threading
import time
import native_speech_mux as muxmod
# Fake engine is a separate process speaking complete JSONL, not mocked Popen.
ENGINE = r'''
import json, os, sys, time
if '--probe' in sys.argv:
print(json.dumps({'argv':sys.argv[1:], 'cwd':os.getcwd(), 'marker':os.getenv('MUX_TEST_MARKER')}))
sys.stderr.write('original-stderr\n')
sys.exit(23)
for raw in sys.stdin.buffer:
try: m=json.loads(raw)
except ValueError:
sys.stdout.buffer.write(raw); sys.stdout.buffer.flush(); continue
method=m.get('method')
def emit(v):
sys.stdout.buffer.write(json.dumps(v).encode()+b'\n'); sys.stdout.buffer.flush()
if method=='thread/start':
emit({'id':m['id'],'result':{'thread':{'id':'thread-A'}}})
elif method=='thread/realtime/start':
emit({'method':'thread/realtime/started','params':{'threadId':'thread-A','realtimeSessionId':'rt'}})
elif method=='thread/realtime/stop':
emit({'method':'thread/realtime/closed','params':{'threadId':'thread-A','reason':None}})
elif method=='error-event':
emit({'method':'thread/realtime/error','params':{'threadId':'thread-A','message':'failed'}})
elif method=='thread/realtime/appendSpeech':
emit({'method':'item/commandExecution/requestApproval','id':m['id'],'params':{'command':'test'}})
emit({'method':'seen-speech','params':m['params']})
emit({'id':m['id'], 'error':{'code':-1,'message':'no'}} if m['params']['text']=='rpc-error' else {'id':m['id'],'result':{}})
elif method=='delay':
time.sleep(.25)
sys.stdout.buffer.write(raw); sys.stdout.buffer.flush()
elif method=='pause':
emit({'method':'paused'})
while not os.path.exists(m['params']['release']): time.sleep(.01)
emit({'method':'released'})
elif method=='exit':
sys.exit(17)
else:
sys.stdout.buffer.write(raw); sys.stdout.buffer.flush()
sys.stdout.buffer.write(b'{"method":"saw-eof"}\n'); sys.stdout.buffer.flush()
sys.stderr.write('engine-eof\n')
sys.exit(19)
'''
class Harness:
def __init__(self, tmp_path):
engine = tmp_path / 'engine.py'
engine.write_text(ENGINE)
self.mux = muxmod.NativeSpeechMux([sys.executable, '-u', str(engine), 'app-server'], observer=True)
ir, iw = os.pipe()
outr, outw = os.pipe()
self.source = os.fdopen(ir, 'rb', buffering=0)
self.input = os.fdopen(iw, 'wb', buffering=0)
self.target = os.fdopen(outw, 'wb', buffering=0)
self.output = os.fdopen(outr, 'rb')
self.lines = queue.Queue()
self.code = None
self.error = None
self.url, self.token = self.mux.start_inlet()
def run():
try:
self.code = self.mux.run(self.source, self.target)
except BaseException as e:
self.error = e
finally:
self.target.close()
self.thread = threading.Thread(target=run, daemon=True)
self.thread.start()
def collect():
for raw in self.output:
self.lines.put(raw)
self.collector = threading.Thread(target=collect, daemon=True)
self.collector.start()
def send(self, value):
raw = value if isinstance(value, bytes) else json.dumps(value).encode()+b'\n'
self.input.write(raw)
def recv(self):
return self.lines.get(timeout=8)
def http(self, path, body, token=None):
conn = http.client.HTTPConnection('127.0.0.1', int(self.url.rsplit(':', 1)[1]), timeout=5)
conn.request('POST', path, json.dumps(body), {'Authorization':'Bearer '+(token if token is not None else self.token)})
response = conn.getresponse()
data = response.read()
conn.close()
return response.status, json.loads(data) if response.status == 200 else data
def start(self):
self.send({'id':1, 'method':'thread/start','params':{}})
assert json.loads(self.recv())['result']['thread']['id'] == 'thread-A'
assert self.mux.snapshot()['threads']['thread-A']['active'] is False
self.send({'id':2,'method':'thread/realtime/start','params':{'threadId':'thread-A'}})
assert json.loads(self.recv())['method'] == 'thread/realtime/started'
return self.bind()
def bind(self):
generation = self.mux.snapshot()['threads']['thread-A']['generation']
b = muxmod.Binding('tenant','bekci','rev',self.mux.connection_id,'thread-A',generation)
assert self.mux.bind(b)
return self.notice(generation)
def notice(self, generation=1, **changes):
now = time.time()
return dict(notice_id='notice-1',tenant_id='tenant',bekci_session_id='bekci',history_revision='rev',
connection_id=self.mux.connection_id,thread_id='thread-A',expected_generation=generation,
issued_at=now,expires_at=now+30,text='düzeltme', **changes)
def close(self):
self.input.close()
self.thread.join(10)
if self.thread.is_alive():
self.mux._process.kill()
self.thread.join(5)
raise AssertionError('mux failed to exit after EOF')
self.collector.join(3)
self.source.close()
self.output.close()
assert self.error is None
def test_native_notice_rpc_and_approval_are_separate(tmp_path):
h = Harness(tmp_path)
try:
notice = h.start()
assert h.http('/notice', notice, 'wrong')[0] == 401
assert h.http('/notice', notice)[1]['status'] == 'queued'
approval = json.loads(h.recv())
assert approval['method'] == 'item/commandExecution/requestApproval'
assert approval['id'].startswith('bekci-speech-')
assert json.loads(h.recv()) == {'method':'seen-speech','params':{'threadId':'thread-A','text':'düzeltme'}}
answer = (json.dumps({'id':approval['id'],'result':{'decision':'decline'}}, separators=(', ', ': '))+'\r\n').encode()
h.send(answer)
assert h.recv() == answer
assert h.http('/notice', notice)[1]['status'] == 'rpc_accepted'
assert h.lines.empty() # native own-result never leaked to desktop
assert h.http('/rpc', {'method':'turn/start'})[0] == 404
assert h.http('/notice', {**notice,'notice_id':'err','text':'rpc-error'})[1]['status'] == 'queued'
h.recv(); h.recv()
h.send(b'{"method":"barrier"}\n')
assert h.recv() == b'{"method":"barrier"}\n'
assert h.http('/notice', {**notice,'notice_id':'err','text':'rpc-error'})[1]['status'] == 'rpc_error'
finally:
h.close()
assert h.code == 19
assert json.loads(h.recv())['method'] == 'saw-eof'
def test_unbound_schema_ttl_tenant_generation_and_restart(tmp_path):
h = Harness(tmp_path)
try:
n = h.notice()
assert h.http('/notice', n)[1]['status'] == 'binding_not_established'
n = h.start()
for delta, status in [({'method':'turn/start'},'invalid_schema'),
({'expected_generation':True},'invalid_schema'),
({'expires_at':time.time()-1},'expired_or_invalid_ttl'),
({'issued_at':float('nan')},'expired_or_invalid_ttl'),
({'tenant_id':'other'},'binding_not_established'),
({'history_revision':'other'},'binding_mismatch'),
({'expected_generation':2},'binding_mismatch'),
({'connection_id':'other'},'binding_mismatch')]:
assert h.http('/notice', {**n, **delta})[1]['status'] == status
h.send({'method':'thread/realtime/stop','params':{'threadId':'thread-A'}})
assert json.loads(h.recv())['method'] == 'thread/realtime/closed'
assert h.http('/notice', n)[1]['status'] == 'binding_not_established'
h.send({'method':'thread/realtime/start','params':{'threadId':'thread-A'}})
h.recv()
fresh = h.bind()
assert fresh['expected_generation'] == n['expected_generation']+1
assert h.http('/notice', n)[1]['status'] == 'binding_mismatch'
h.send({'method':'error-event'})
h.recv()
assert h.http('/notice', fresh)[1]['status'] == 'binding_not_established'
finally:
h.close()
def test_concurrent_notices_and_raw_backpressure_order(tmp_path):
h = Harness(tmp_path)
try:
n = h.start()
h.send({'method':'delay'})
records = [(json.dumps({'method':'echo','i':i,'padding':'x'*16000})+'\r\n').encode() for i in range(90)]
producer = threading.Thread(target=lambda: [h.send(r) for r in records], daemon=True)
producer.start()
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(lambda i: h.http('/notice',{**n,'notice_id':str(i)})[1]['status'], range(12)))
assert set(results) <= {'queued','busy'}
accepted = results.count('queued')
echoes = []
speech = 0
approvals = 0
for _ in range(1 + len(records) + accepted*2):
raw = h.recv()
method = json.loads(raw).get('method')
if method == 'echo': echoes.append(raw)
elif method == 'seen-speech': speech += 1
elif method == 'item/commandExecution/requestApproval': approvals += 1
producer.join(5)
assert not producer.is_alive()
assert echoes == records
assert speech == approvals == accepted
assert h.mux._queue.maxsize == 32
finally:
h.close()
def test_fragmented_oversized_jsonl_and_partial_eof_preserved(tmp_path):
h = Harness(tmp_path)
try:
h.start()
raw = b'{ "method":"huge", "value":"' + b'x'*200000 + b'"}\r\n'
producer = threading.Thread(target=lambda: h.send(raw),daemon=True)
producer.start()
assert h.recv() == raw
producer.join(5)
assert h.mux.snapshot()['injection_disabled']
h.send(b'{"partial":true}')
finally:
h.close()
# Engine echoes an unterminated record, then its EOF notification: byte exact.
assert h.recv() == b'{"partial":true}{"method":"saw-eof"}\n'
assert h.code == 19
def test_non_appserver_passthrough_argv_env_cwd_stderr_exit(tmp_path, monkeypatch):
engine = tmp_path / 'engine.py'
engine.write_text(ENGINE)
module = Path(muxmod.__file__).resolve()
monkeypatch.setenv('MUX_TEST_MARKER','inherited')
args = [sys.executable,str(module),sys.executable,'--','-u',str(engine),'--probe','space arg','app-server']
p = subprocess.run(args,cwd=tmp_path,capture_output=True,timeout=10)
assert p.returncode == 23
result = json.loads(p.stdout)
assert result == {'argv':['--probe','space arg','app-server'],'cwd':str(tmp_path),'marker':'inherited'}
direct = subprocess.run([sys.executable, '-u', str(engine), '--probe', 'space arg', 'app-server'],
cwd=tmp_path, capture_output=True, timeout=10)
assert (p.stdout, p.stderr, p.returncode) == (direct.stdout, direct.stderr, direct.returncode)
def test_engine_exit_without_desktop_eof(tmp_path):
h = Harness(tmp_path)
try:
h.send({'method':'exit'})
h.thread.join(5)
assert not h.thread.is_alive()
assert h.code == 17
assert h.mux.snapshot()['closed']
finally:
h.close()
def test_owned_namespace_collision_disables_injection_but_forwards(tmp_path):
h = Harness(tmp_path)
try:
n = h.start()
raw = json.dumps({'id':h.mux._prefix+'collision','method':'echo'}).encode()+b'\n'
h.send(raw)
assert h.recv() == raw
assert h.http('/notice', n)[1]['status'] == 'injection_disabled'
finally:
h.close()
def test_queued_notice_revalidated_after_observed_stop(tmp_path):
h = Harness(tmp_path)
release = tmp_path / 'release'
try:
n = h.start()
h.send({'method':'pause','params':{'release':str(release)}})
assert json.loads(h.recv())['method'] == 'paused'
# Fill the OS pipe while the real engine is paused; leave bounded queue
# capacity for the notice and stop record behind it.
for i in range(8):
h.send({'method':'echo','i':i,'padding':'x'*32000})
assert h.http('/notice', n)[1]['status'] == 'queued'
h.send({'method':'thread/realtime/stop','params':{'threadId':'thread-A'}})
deadline = time.monotonic()+5
while h.mux.snapshot()['threads']['thread-A']['active'] and time.monotonic() < deadline:
time.sleep(.01)
assert not h.mux.snapshot()['threads']['thread-A']['active']
release.touch()
methods = [json.loads(h.recv())['method'] for _ in range(10)]
assert methods == ['released'] + ['echo']*8 + ['thread/realtime/closed']
assert h.mux._receipts[('tenant','bekci','notice-1')]['status'] == 'binding_not_established'
assert h.lines.empty()
finally:
release.touch()
h.close()
def test_real_request_correlation_and_opaque_receipt_contract(tmp_path):
h = Harness(tmp_path)
try:
n = h.start()
adapter = muxmod.WatchReceiptAdapter(h.mux)
headers = {'Session-Id':'thread-A','Thread-Id':'thread-A'}
correlation = adapter.capture(headers, {'prompt_cache_key':'thread-A'})
assert correlation.thread_id == 'thread-A'
assert adapter.capture({**headers,'Thread-Id':'other'}, {'prompt_cache_key':'thread-A'}) is None
assert adapter.capture(headers, {}) is None
assert adapter.accept('bekci.watch', {}, correlation) == 'backend_contract_not_established'
# Internal contract fixture, explicitly NOT a claim about backend wire format.
receipt = muxmod.WatchReceipt('opaque','tenant','bekci','rev','thread-A',time.time()+60)
adapter.decode = lambda payload: payload
adapter.resolve = lambda handle: receipt
assert adapter.accept('bekci.watch','opaque',correlation) == 'bound'
assert adapter.accept('_bekci_watch','opaque',correlation) == 'bound'
assert adapter.accept('other','opaque',correlation) == 'unsupported_metadata_transport'
assert adapter.notice('unbound',n)['status'] == 'binding_not_established'
assert adapter.notice('opaque',{**n,'tenant_id':'other'})['status'] == 'binding_mismatch'
assert adapter.notice('opaque',n)['status'] == 'queued'
assert json.loads(h.recv())['method'] == 'item/commandExecution/requestApproval'
assert json.loads(h.recv())['method'] == 'seen-speech'
h.send({'method':'thread/realtime/stop','params':{'threadId':'thread-A'}}); h.recv()
h.send({'method':'thread/realtime/start','params':{'threadId':'thread-A'}}); h.recv()
assert adapter.accept('bekci.watch','opaque',correlation) == 'stale_generation'
fresh = adapter.capture(headers, {'prompt_cache_key':'thread-A'})
adapter.resolve = lambda handle: muxmod.WatchReceipt('opaque','tenant','bekci','rev','wrong',time.time()+60)
assert adapter.accept('bekci.watch','opaque',fresh) == 'receipt_mismatch_or_expired'
adapter.resolve = lambda handle: muxmod.WatchReceipt('opaque','tenant','bekci','rev','thread-A',time.time()-1)
assert adapter.accept('bekci.watch','opaque',fresh) == 'receipt_mismatch_or_expired'
finally:
h.close()
def test_verified_original_launcher_fallback_actual_process(tmp_path):
import hashlib
executable = str(Path(sys.executable).resolve())
with open(executable,'rb') as f:
digest = hashlib.file_digest(f,'sha256').hexdigest()
assert muxmod.verified_original(executable,digest) == executable
import pytest
with pytest.raises(ValueError): muxmod.verified_original(executable,'0'*64)
with pytest.raises(ValueError): muxmod.verified_original('python.exe',digest)
# A value that happens to say app-server must not trigger observer/attachment.
marker = tmp_path/'original-ran'
code = 'from pathlib import Path; import sys; Path(sys.argv[1]).write_text("ok"); sys.exit(21)'
def forbidden(adapter):
raise AssertionError('not an app-server invocation')
assert muxmod.launch_original(executable,digest,['-c',code,str(marker),'app-server'],attach=forbidden) == 21
assert marker.read_text() == 'ok'
@@ -0,0 +1,83 @@
"""Native switch regressions using the real HTTP splitter and offline peers."""
import copy
import gzip
import json
import unittest
import ast
from pathlib import Path
import bridge
import gecis_context as gc
import ozet_kopru
import test_ayirici as fixtures
class NativeSwitch(unittest.TestCase):
setUp = fixtures.Ayirici.setUp
tearDown = fixtures.Ayirici.tearDown
iste = fixtures.Ayirici.iste
def test_unknown_native_capacity_is_not_invented(self):
tree = ast.parse(Path(__file__).with_name('kopru.py').read_text(encoding='utf-8'))
method = next(node for node in ast.walk(tree)
if isinstance(node, ast.FunctionDef) and node.name == '_transfer_window')
ns = {'ek_model_mu': lambda _: False, 'LAST_MODELS': {}, 'gecis_context': gc, 'json': json}
exec(compile(ast.Module(body=[method], type_ignores=[]), 'window', 'exec'), ns)
from types import SimpleNamespace
with self.assertRaises(gc.TransferError):
ns['_transfer_window'](SimpleNamespace(headers={}), 'gpt-6-astra')
def test_opaque_native_switch_with_persisted_foreign_residue(self):
self.ozgun.cevap = fixtures.duz(200, b'data: {"type":"response.completed"}\n\n', 'text/event-stream')
compact = {'type': 'compaction', 'id': 'cmp_native',
'encrypted_content': 'gAAAAABp' + 'AqZ09_-jX' * 10000}
req = {'model': 'gpt-6.1-sol', 'prompt_cache_key': 'restart', 'input': [
ozet_kopru.ozet_ogesi('DECISION-4827 must survive'), compact,
{'type': 'custom_tool_call', 'id': 'fc_123', 'call_id': 'call_123',
'name': 'apply_patch', 'input': 'patch text'},
{'type': 'custom_tool_call_output', 'id': 'fco_123',
'call_id': 'call_123', 'output': 'complete'},
{'role': 'user', 'content': 'continue'}]}
before = copy.deepcopy(req)
for cold, model in ((True, 'gpt-6.1-sol'), (False, 'gpt-6-astra'), (True, 'gpt-6-astra')):
if cold:
bridge.TRANSFERS.clear()
req['model'] = model
body = gzip.compress(json.dumps(req).encode())
self.assertEqual(self.iste('POST', '/backend-api/codex/responses', body,
{'Content-Encoding': 'gzip'})[0], 200)
sent = json.loads(self.ozgun.gelen[-1]['govde'])
self.assertEqual(sent['input'][1], compact)
self.assertEqual(sent['input'][2]['id'], 'ctc_123')
self.assertEqual(sent['input'][3]['id'], 'ctco_123')
self.assertEqual(sent['input'][3]['call_id'], 'call_123')
self.assertIn('DECISION-4827', json.dumps(sent))
self.assertEqual(self.kopru.gelen, [])
self.assertEqual(req['input'], before['input'])
def test_clean_native_body_is_byte_identical_even_after_external_source(self):
req = {'model': 'ide/test', 'prompt_cache_key': 'return', 'input': []}
self.iste('POST', '/backend-api/codex/responses', json.dumps(req).encode())
self.kopru.gelen.clear()
req.update(model='gpt-6-astra', input=[{'type': 'compaction',
'encrypted_content': 'gAAAAABp' + 'X' * 100000}])
raw = gzip.compress(json.dumps(req, indent=3).encode())
self.iste('POST', '/backend-api/codex/responses', raw, {'Content-Encoding': 'gzip'})
self.assertEqual(self.ozgun.gelen[-1]['govde'], raw)
self.assertEqual(self.kopru.gelen, [])
def test_cold_native_tool_id_repair_does_not_require_semantic_budget(self):
req = {'model': 'gpt-6-astra', 'input': [
{'type': 'custom_tool_call', 'id': 'fc_old', 'call_id': 'call_old',
'name': 'apply_patch', 'input': 'x' * 100000},
{'type': 'custom_tool_call_output', 'id': 'fco_old',
'call_id': 'call_old', 'output': 'complete'}]}
self.iste('POST', '/backend-api/codex/responses', json.dumps(req).encode())
self.assertEqual(len(self.kopru.gelen), 0)
sent = json.loads(self.ozgun.gelen[-1]['govde'])
self.assertEqual(sent, gc.normalize(req))
self.assertEqual(req['input'][0]['id'], 'fc_old')
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,115 @@
"""Windows owned-process lifetime tests; no installed process is terminated."""
import ctypes
from ctypes import wintypes as w
import hashlib
import json
import os
from pathlib import Path
import queue
import subprocess
import sys
import threading
import time
import pytest
from prepare_native_watch import prepare
pytestmark = pytest.mark.skipif(sys.platform != 'win32', reason='Windows executable lifecycle')
def descendants(parent):
class Entry(ctypes.Structure):
_fields_=[('size',w.DWORD),('usage',w.DWORD),('pid',w.DWORD),('heap',ctypes.c_size_t),('module',w.DWORD),('threads',w.DWORD),('parent',w.DWORD),('priority',w.LONG),('flags',w.DWORD),('exe',w.WCHAR*260)]
k=ctypes.WinDLL('kernel32',use_last_error=True)
k.CreateToolhelp32Snapshot.restype=w.HANDLE
k.Process32FirstW.argtypes=[w.HANDLE,ctypes.POINTER(Entry)]
k.Process32NextW.argtypes=[w.HANDLE,ctypes.POINTER(Entry)]
k.CloseHandle.argtypes=[w.HANDLE]
snap=k.CreateToolhelp32Snapshot(2,0);entry=Entry();entry.size=ctypes.sizeof(entry);rows=[]
try:
more=k.Process32FirstW(snap,ctypes.byref(entry))
while more:
rows.append((entry.pid,entry.parent));more=k.Process32NextW(snap,ctypes.byref(entry))
finally:k.CloseHandle(snap)
owned={parent}
while True:
children={pid for pid,ppid in rows if ppid in owned}
if children <= owned: return owned-{parent}
owned |= children
def assert_abrupt_cleanup(p, expected_min):
k=ctypes.WinDLL('kernel32',use_last_error=True)
k.OpenProcess.argtypes=[w.DWORD,w.BOOL,w.DWORD];k.OpenProcess.restype=w.HANDLE
k.WaitForSingleObject.argtypes=[w.HANDLE,w.DWORD]
k.CloseHandle.argtypes=[w.HANDLE]
ids=descendants(p.pid)
assert len(ids)>=expected_min, ids
handles=[k.OpenProcess(0x100000,False,pid) for pid in ids]
assert all(handles)
try:
p.kill();p.wait(timeout=5)
assert all(k.WaitForSingleObject(h,5000)==0 for h in handles), ids
finally:
for h in handles:k.CloseHandle(h)
return len(ids)
def line(p):
q=queue.Queue();threading.Thread(target=lambda:q.put(p.stdout.readline()),daemon=True).start()
return q.get(timeout=20)
def candidate(tmp_path, original, digest):
return prepare(Path(__file__).parent,tmp_path/'candidate',original,digest,Path(sys.executable),tmp_path/'bridge',tmp_path/'agent')
def test_abrupt_bootstrap_exit_kills_owned_python_and_grandchild(tmp_path):
exe=Path(sys.executable)
launcher=candidate(tmp_path,exe,hashlib.sha256(exe.read_bytes()).hexdigest())
script='import subprocess,sys,time; p=subprocess.Popen([sys.executable,"-c","import time; time.sleep(120)"]); print(p.pid,flush=True); time.sleep(120)'
p=subprocess.Popen([str(launcher),'-u','-c',script],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
try:
assert int(line(p))>0
assert_abrupt_cleanup(p,3)
finally:
if p.poll() is None:p.kill();p.wait(timeout=5)
p.stdin.close();p.stdout.close();p.stderr.close()
def test_pinned_original_initialize_and_abrupt_cleanup(tmp_path):
# Explicit provisioned evidence, never discovers or kills an existing app.
manifest_path=os.environ.get('NATIVE_WATCH_TEST_MANIFEST')
if not manifest_path: pytest.skip('requires explicit pinned original candidate')
manifest=json.loads(Path(manifest_path).read_text(encoding='utf-8-sig'))
launcher=candidate(tmp_path,Path(manifest['original']),manifest['original_sha256'])
p=subprocess.Popen([str(launcher),'-c','model_provider="openai"','app-server','--analytics-default-enabled'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
try:
p.stdin.write(json.dumps({'id':1,'method':'initialize','params':{'clientInfo':{'name':'owned-cleanup-proof','version':'1.0'},'capabilities':{'experimentalApi':True}}}).encode()+b'\n');p.stdin.flush()
response=json.loads(line(p));assert response.get('id')==1 and 'result' in response,response
assert_abrupt_cleanup(p,2)
finally:
if p.poll() is None:p.kill();p.wait(timeout=5)
p.stdin.close();p.stdout.close();p.stderr.close()
def test_compiled_launcher_fallback(tmp_path):
source=Path(__file__).parent
import hashlib
exe=Path(sys.executable);digest=hashlib.sha256(exe.read_bytes()).hexdigest()
launcher=prepare(source,tmp_path/'candidate',exe,digest,exe,tmp_path/'bridge',tmp_path/'agent')
args=['-c','import json,sys,os; print(json.dumps([sys.argv[1:],os.getcwd()])); sys.stderr.write("stderr\\n"); sys.exit(23)','space arg','quote"arg','trailing\\']
direct=subprocess.run([str(exe),*args],cwd=tmp_path,capture_output=True)
wrapped=subprocess.run([str(launcher),*args],cwd=tmp_path,capture_output=True)
assert (wrapped.returncode,wrapped.stdout,wrapped.stderr)==(direct.returncode,direct.stdout,direct.stderr)
manifest=launcher.with_suffix('.json');data=json.loads(manifest.read_text(encoding='utf-8'));data['original_sha256']='0'*64;manifest.write_text(json.dumps(data),encoding='utf-8')
assert subprocess.run([str(launcher),'--version'],capture_output=True).returncode==78
def test_executable_flushes_interactive_stdin_before_eof(tmp_path):
import hashlib,queue
exe=Path(sys.executable);digest=hashlib.sha256(exe.read_bytes()).hexdigest()
launcher=prepare(Path(__file__).parent,tmp_path/'candidate',exe,digest,exe,tmp_path/'bridge',tmp_path/'agent')
p=subprocess.Popen([str(launcher),'-u','-c','import sys; print(sys.stdin.readline().strip(),flush=True); sys.stdin.read()'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
q=queue.Queue();threading.Thread(target=lambda:q.put(p.stdout.readline()),daemon=True).start()
try:
p.stdin.write(b'live-before-eof\n');p.stdin.flush()
assert q.get(timeout=5).strip()==b'live-before-eof'
finally:
p.stdin.close();p.wait(timeout=5)
@@ -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))
@@ -0,0 +1,143 @@
"""Real-socket regressions for terminal worker errors (no provider calls)."""
import json
import unittest
import test_ayirici as fixtures
import terminal_stream
ERROR = b'event: error\ndata: {"type":"error","error":{"type":"upstream_error","message":"upstream 503"}}\n\n'
COMPLETE = b'event: response.completed\ndata: {"type":"response.completed","response":{"id":"resp_ok","status":"completed","output":[]}}\n\n'
class TerminalStream(unittest.TestCase):
setUp = fixtures.Ayirici.setUp
tearDown = fixtures.Ayirici.tearDown
iste = fixtures.Ayirici.iste
def request(self, token="a", text="check"):
body = json.dumps({"model": "ide/test", "input": [{"role": "user", "content": text}]})
return self.iste("POST", "/backend-api/codex/responses", body,
{"Authorization": "Bearer " + token})
def worker(self, payload):
def respond(h, body):
if h.path.endswith("/prepare-transfer"):
return fixtures.duz(200, json.dumps({"request": json.loads(body)["request"]}).encode())(h, body)
return fixtures.duz(200, payload, "text/event-stream")(h, body)
self.kopru.cevap = respond
def inference_count(self):
return sum(x["yol"].endswith("/responses") for x in self.kopru.gelen)
def test_error_only_stream_is_an_honest_http_failure(self):
self.worker(ERROR)
status, body, _ = self.request()
self.assertEqual(status, 400)
self.assertIn("upstream 503", json.loads(body)["error"]["message"])
self.assertNotIn(b"response.completed", body)
self.assertEqual(self.inference_count(), 1)
def test_error_after_heartbeat_stops_automatic_replay_once(self):
def respond(h, body):
if h.path.endswith("/prepare-transfer"):
return fixtures.duz(200, json.dumps({"request": json.loads(body)["request"]}).encode())(h, body)
h.send_response(200)
h.send_header("Content-Type", "text/event-stream")
h.send_header("Transfer-Encoding", "chunked")
h.end_headers()
for chunk in (b": keepalive\n\n", ERROR):
h.wfile.write(b"%x\r\n" % len(chunk) + chunk + b"\r\n")
h.wfile.flush()
fixtures.time.sleep(.08)
h.wfile.write(b"0\r\n\r\n")
self.kopru.cevap = respond
status, body, _ = self.request()
self.assertEqual((status, body), (200, b": keepalive\n\n" + ERROR))
status, body, _ = self.request()
self.assertEqual(status, 400)
self.assertIn(b"upstream 503", body)
self.assertEqual(self.inference_count(), 1)
# A later explicit retry is allowed; the one-shot guard is consumed.
self.worker(COMPLETE)
self.assertEqual(self.request()[:2], (200, COMPLETE))
self.assertEqual(self.inference_count(), 2)
def test_native_error_stream_is_not_intercepted(self):
self.ozgun.cevap = fixtures.duz(200, ERROR, "text/event-stream")
body = json.dumps({"model": "gpt-6-astra", "input": []})
self.assertEqual(self.iste("POST", "/backend-api/codex/responses", body)[:2], (200, ERROR))
def test_success_stream_is_byte_identical(self):
self.worker(COMPLETE)
self.assertEqual(self.request()[:2], (200, COMPLETE))
self.assertEqual(self.request()[:2], (200, COMPLETE))
self.assertEqual(self.inference_count(), 2)
def test_eof_without_upstream_error_keeps_network_retry(self):
self.worker(b": keepalive\n\n")
self.assertEqual(self.request()[0], 200)
self.assertEqual(self.request()[0], 200)
self.assertEqual(self.inference_count(), 2)
def test_worker_exhausted_failure_is_terminal_without_changing_its_reason(self):
self.worker(b'event: response.failed\ndata: {"type":"response.failed","response":{"status":"failed","error":{"code":"jev_no_fallback","message":"No accepted response"}}}\n\n')
status, body, _ = self.request()
self.assertEqual(status, 400)
self.assertEqual(json.loads(body)["error"]["code"], "jev_no_fallback")
def test_existing_context_error_classification_is_preserved(self):
data = b'event: response.failed\ndata: {"type":"response.failed","response":{"error":{"code":"context_length_exceeded","message":"Context full"}}}\n\n'
self.worker(data)
self.assertEqual(self.request()[:2], (200, data))
class ObserverTests(unittest.TestCase):
def test_fragmented_crlf_and_multiline_json(self):
data = b'event: error\r\ndata: {"type":"error",\r\ndata: "error":{"type":"upstream_error","message":"503"}}\r\n\r\n'
observer = terminal_stream.Observer()
for byte in data:
observer.feed(bytes([byte]))
self.assertEqual(observer.error['message'], '503')
def test_large_and_malformed_frames_are_bounded_and_do_not_crash(self):
observer = terminal_stream.Observer()
observer.feed(b'data: ' + b'x'*200000)
self.assertLessEqual(len(observer.tail), observer.LIMIT)
observer.feed(b'\n\ndata: {"response":[]}\n\n' + ERROR)
self.assertEqual(observer.error['message'], 'upstream 503')
def test_completed_stream_does_not_poison_next_request(self):
observer = terminal_stream.Observer()
observer.feed(ERROR + COMPLETE)
self.assertIsNone(observer.error)
self.assertTrue(observer.completed)
def test_one_shot_expiry_and_capacity(self):
now = [10]
cache = terminal_stream.FailureReplays(ttl=5, capacity=2, clock=lambda: now[0])
cache.remember(b'a', {'code': 'a'})
self.assertEqual(cache.take(b'a'), {'code': 'a'})
self.assertIsNone(cache.take(b'a'))
cache.remember(b'a', {})
now[0] = 16
self.assertIsNone(cache.take(b'a'))
for key in (b'a', b'b', b'c'):
cache.remember(key, {'code': 'error'})
self.assertIsNone(cache.take(b'a'))
self.assertEqual(len(cache.items), 2)
def test_request_identity_separates_accounts_sessions_models_and_inputs(self):
base = {'model': 'ide/a', 'input': []}
headers = {'Authorization': 'Bearer a', 'session_id': 'one'}
key = terminal_stream.request_key(base, headers)
self.assertEqual(key, terminal_stream.request_key(dict(reversed(list(base.items()))), headers))
for request, other in [({'model': 'ide/b', 'input': []}, headers),
({'model': 'ide/a', 'input': ['next']}, headers),
(base, dict(headers, Authorization='Bearer b')),
(base, dict(headers, session_id='two'))]:
self.assertNotEqual(key, terminal_stream.request_key(request, other))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,66 @@
"""Namespace routing regressions: leaf-only calls cannot dispatch collaboration tools."""
import copy
import json
import unittest
from test_model_switch import env
class ToolNamespaces(unittest.TestCase):
def request(self):
return {"tools": [
{"type": "namespace", "name": "collaboration", "tools": [{"type": "function", "name": "spawn_agent", "parameters": {"type": "object", "properties": {}}}]},
{"type": "namespace", "name": "other", "tools": [{"type": "function", "name": "spawn_agent"}]},
{"type": "namespace", "name": "functions", "tools": [{"type": "custom", "name": "apply_patch"}]},
{"type": "function", "name": "plain"}],
"input": [{"type": "function_call", "namespace": "collaboration", "name": "spawn_agent", "id": "fc_a", "call_id": "call_a", "arguments": "{}"},
{"type": "function_call_output", "call_id": "call_a", "output": "child_123"}]}
def test_namespace_calls_round_trip_at_all_stream_stages(self):
req = self.request(); original = copy.deepcopy(req); names = {}
custom = env["flatten_request"](req, names)
alias = req["tools"][0]["name"]
self.assertNotEqual(alias, req["tools"][1]["name"])
self.assertLessEqual(len(alias), 64)
self.assertEqual(req["input"][0]["name"], alias)
self.assertNotIn("namespace", req["input"][0])
self.assertEqual(req["input"][1], original["input"][1])
adapter = env["SseRewriter"](custom, tool_names=names)
item = {"type": "function_call", "name": alias, "id": "fc_test", "call_id": "call_test", "arguments": "{}"}
for stage in ["response.output_item.added", "response.output_item.done"]:
restored = adapter._fix({"type": stage, "item": dict(item)})["item"]
self.assertEqual((restored["namespace"], restored["name"]), ("collaboration", "spawn_agent"))
self.assertEqual(restored["call_id"], "call_test")
restored = adapter._fix({"type": "response.completed", "response": {"output": [dict(item)]}})["response"]["output"][0]
self.assertEqual((restored["namespace"], restored["name"]), ("collaboration", "spawn_agent"))
def test_custom_tool_namespace_and_ids_survive_chunked_sse(self):
req = self.request(); names = {}; custom = env["flatten_request"](req, names)
alias = req["tools"][2]["name"]
item = {"type": "function_call", "name": alias, "id": "fc_patch", "call_id": "call_patch", "arguments": json.dumps({"input": "patch text"})}
raw = ("data: " + json.dumps({"type": "response.output_item.done", "item": item}) + "\n\n").encode()
a = env["SseRewriter"](custom, tool_names=names)
output = a.feed(raw[:31]) + a.feed(raw[31:]) + a.flush()
restored = json.loads(next(l[6:] for l in output.decode().splitlines() if l.startswith("data: ")))["item"]
self.assertEqual((restored["namespace"], restored["name"], restored["type"]), ("functions", "apply_patch", "custom_tool_call"))
self.assertEqual(restored["id"], "ctc_patch")
self.assertEqual(restored["input"], "patch text")
def test_plain_tools_stay_plain_and_history_not_mutated(self):
req = self.request(); original = copy.deepcopy(req); old_item = req["input"][0]
env["flatten_request"](req, {})
self.assertEqual(req["tools"][-1]["name"], "plain")
self.assertEqual(old_item, original["input"][0])
plain = {"type": "function_call", "name": "plain", "arguments": "{}"}
self.assertEqual(env["SseRewriter"](set())._fix({"item": plain})["item"], plain)
def test_additional_tools_long_names_and_tool_choice(self):
namespace = "provider_" * 20
req = {"tools": [], "input": [{"type": "additional_tools", "tools": [{"type": "namespace", "name": namespace, "tools": [{"type": "function", "name": "read"}]}]}], "tool_choice": {"type": "function", "namespace": namespace, "name": "read"}}
names = {}; env["flatten_request"](req, names)
alias = req["tools"][0]["name"]
self.assertLessEqual(len(alias), 64)
self.assertEqual(req["tool_choice"], {"type": "function", "name": alias})
self.assertEqual(req["input"], [])
self.assertEqual(names[alias], (namespace, "read"))
if __name__ == "__main__": unittest.main()
@@ -1,7 +1,13 @@
import { describe, expect, it } from 'vitest'
import { execFileSync } from 'node:child_process'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import {
addCodexLinkLine,
CODEX_BRIDGE_VERSION,
CODEX_LINK_LINE,
CODEX_LINK_RUN_COMMAND_MAX,
codexAutostartPlan,
@@ -34,6 +40,23 @@ function expectRoundTrip(original: string) {
}
describe('the contract line', () => {
it('expects the version exported by the bundled Python bridge', () => {
const home = mkdtempSync(path.join(tmpdir(), 'codex-bridge-version-'))
try {
const python = process.platform === 'win32' ? 'py' : 'python3'
const args = process.platform === 'win32' ? ['-3'] : []
const version = execFileSync(python, [...args, '-c', 'import bridge; print(bridge.SURUM)'], {
cwd: fileURLToPath(new URL('../assets/codex-bridge/', import.meta.url)),
env: { ...process.env, AITURK_CODEX_BRIDGE_HOME: home, HERMES_HOME: home },
encoding: 'utf8',
windowsHide: true
}).trim()
expect(CODEX_BRIDGE_VERSION).toBe(version)
} finally {
rmSync(home, { recursive: true, force: true })
}
})
it('is the exact text Codex and the bridge agree on', () => {
expect(CODEX_LINK_LINE).toBe(
'openai_base_url = "http://127.0.0.1:8795/backend-api/codex" # AITURK IDE Codex Baglama'
@@ -202,12 +225,17 @@ describe('preference file', () => {
})
it('writes the contract shape and keeps unknown fields', () => {
expect(JSON.parse(serializeCodexLinkSettings({}, false))).toEqual({ enabled: false, kapsam: 'ide' })
expect(JSON.parse(serializeCodexLinkSettings({}, false))).toEqual({
enabled: false,
mod: 'dogrudan',
kapsam: 'ide'
})
const { stored } = parseCodexLinkSettings('{"enabled": false, "kapsam": "hepsi", "gelecek": {"a": 1}}')
expect(JSON.parse(serializeCodexLinkSettings(stored, true))).toEqual({
enabled: true,
mod: 'dogrudan',
gelecek: { a: 1 },
kapsam: 'hepsi'
})
@@ -285,3 +313,17 @@ describe('bridge configuration and command', () => {
expect(codexAutostartPlan(`${deep}\\pythonw.exe`, launch, `${deep}\\baslat.pyw`)).toBeNull()
})
})
describe('mode preference', () => {
it('uses direct for missing, invalid and malformed modes', () => {
for (const raw of [null, '{', '{}', '{"mod":"bad"}', '{"mod":false}']) {
expect(parseCodexLinkSettings(raw).mod).toBe('dogrudan')
}
expect(parseCodexLinkSettings('{"mod":"aiturk"}').mod).toBe('aiturk')
})
it('preserves other fields when changing mode', () => {
const stored = { enabled: false, mod: 'aiturk', kapsam: 'hepsi', future: { a: 1 } }
expect(JSON.parse(serializeCodexLinkSettings(stored, false, 'dogrudan'))).toEqual({ ...stored, mod: 'dogrudan' })
expect(JSON.parse(serializeCodexLinkSettings(stored, true)).mod).toBe('aiturk')
})
})
+16 -7
View File
@@ -27,7 +27,7 @@ export const CODEX_LINK_REFRESH_URL = `${CODEX_LINK_ORIGIN}/aiturk/codex-baglama
export const CODEX_LINK_MARKER = '# AITURK IDE Codex Baglama'
// Must equal SURUM in assets/codex-bridge/bridge.py; a running bridge that reports
// another version is restarted so an updated IDE never keeps an old bridge alive.
export const CODEX_BRIDGE_VERSION = '8'
export const CODEX_BRIDGE_VERSION = '18'
export const CODEX_LINK_LINE = `openai_base_url = "${CODEX_LINK_ORIGIN}/backend-api/codex" ${CODEX_LINK_MARKER}`
export const CODEX_LINK_RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
@@ -300,6 +300,7 @@ export function removeCodexLinkLine(source: string): CodexConfigEdit {
export interface CodexLinkSettings {
enabled: boolean
mod: 'dogrudan' | 'aiturk'
/** Every field of the stored object, so a rewrite keeps what it did not set. */
stored: Record<string, unknown>
}
@@ -307,26 +308,34 @@ export interface CodexLinkSettings {
/** Missing, unreadable or malformed means ON: the feature is on by default. */
export function parseCodexLinkSettings(raw: null | string | undefined): CodexLinkSettings {
if (typeof raw !== 'string') {
return { enabled: true, stored: {} }
return { enabled: true, mod: 'dogrudan', stored: {} }
}
try {
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
return { enabled: true, stored: {} }
return { enabled: true, mod: 'dogrudan', stored: {} }
}
return { enabled: parsed.enabled !== false, stored: parsed as Record<string, unknown> }
return {
enabled: parsed.enabled !== false,
mod: parsed.mod === 'aiturk' ? 'aiturk' : 'dogrudan',
stored: parsed as Record<string, unknown>
}
} catch {
return { enabled: true, stored: {} }
return { enabled: true, mod: 'dogrudan', stored: {} }
}
}
export function serializeCodexLinkSettings(stored: Record<string, unknown>, enabled: boolean): string {
export function serializeCodexLinkSettings(
stored: Record<string, unknown>,
enabled: boolean,
mod: 'dogrudan' | 'aiturk' = stored.mod === 'aiturk' ? 'aiturk' : 'dogrudan'
): string {
const kapsam = typeof stored.kapsam === 'string' && stored.kapsam ? stored.kapsam : 'ide'
return `${JSON.stringify({ ...stored, enabled, kapsam }, null, 2)}\n`
return `${JSON.stringify({ ...stored, enabled, mod, kapsam }, null, 2)}\n`
}
// ── Bridge configuration ────────────────────────────────────────────────────
@@ -5,6 +5,8 @@
* code into its program. See codex-link.ts for what the fields mean in action.
*/
export type CodexLinkMode = 'dogrudan' | 'aiturk'
export type CodexLinkIdeState = 'anahtar-yok' | 'kapali' | 'tamam'
/** Stable codes; the renderer owns the wording. Detail goes to the desktop log. */
@@ -18,6 +20,7 @@ export type CodexLinkError =
export interface CodexLinkStatus {
enabled: boolean
mod: CodexLinkMode
/** Codex's `config.toml` exists on this machine. */
codexFound: boolean
/** The IDE's own Python environment exists (first-run setup finished). */
@@ -34,6 +37,7 @@ export interface CodexLinkStatus {
export interface CodexLinkPatch {
enabled?: boolean
mod?: CodexLinkMode
/** "Sync now": re-run the apply without changing the preference. */
sync?: boolean
}
+75 -2
View File
@@ -39,6 +39,7 @@ interface World {
probesUntilUp: number
autostart: null | string
regFails: boolean
configWriteFails: boolean
/** Every observable effect, in order. */
events: string[]
clock: number
@@ -51,6 +52,7 @@ function createWorld(patch: Partial<World> & { files?: Map<string, string> } = {
events: [],
files: new Map([
[PATHS.codexConfig, USER_CONFIG],
[PATHS.settingsFile, JSON.stringify({ enabled: true, mod: 'aiturk' })],
[PATHS.bridgeScript, '# bridge'],
[PATHS.pythonw, ''],
[PATHS.python, '']
@@ -59,6 +61,7 @@ function createWorld(patch: Partial<World> & { files?: Map<string, string> } = {
staleVersion: null,
probesUntilUp: 0,
regFails: false,
configWriteFails: false,
spawnWorks: true,
...patch
}
@@ -71,6 +74,7 @@ function createWorld(patch: Partial<World> & { files?: Map<string, string> } = {
exists: file => world.files.has(file),
readText: file => world.files.get(file) ?? null,
writeTextAtomic: (file, text) => {
if (file === PATHS.codexConfig && world.configWriteFails) throw new Error('config locked')
world.events.push(`write ${file}`)
world.files.set(file, text)
},
@@ -189,6 +193,7 @@ describe('applyCodexLink', () => {
expect(status).toEqual({
enabled: true,
mod: 'aiturk',
codexFound: true,
venvReady: true,
foreign: false,
@@ -325,16 +330,42 @@ describe('applyCodexLink', () => {
'autostart clear',
'stop'
])
expect(JSON.parse(world.files.get(PATHS.settingsFile) ?? '')).toEqual({ enabled: false, kapsam: 'ide' })
expect(JSON.parse(world.files.get(PATHS.settingsFile) ?? '')).toEqual({
enabled: false,
mod: 'aiturk',
kapsam: 'ide'
})
// Byte for byte what the user had before the feature touched it.
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
expect(world.autostart).toBeNull()
expect(world.listening).toBe(false)
})
it('keeps the bridge and login item if disconnect cannot restore the config', async () => {
const { link, world } = createWorld({ listening: true })
await link.apply()
const linkedConfig = world.files.get(PATHS.codexConfig)
const autostart = world.autostart
world.events.length = 0
world.configWriteFails = true
expect(await link.apply({ enabled: false })).toMatchObject({
bridgeRunning: true,
lastError: 'config-write-failed'
})
expect(world.files.get(PATHS.codexConfig)).toBe(linkedConfig)
expect(world.autostart).toBe(autostart)
expect(world.events).not.toContain('stop')
expect(world.events).not.toContain('autostart clear')
world.configWriteFails = false
expect(await link.apply({ enabled: false })).toMatchObject({ bridgeRunning: false, lastError: null })
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
})
it('stays off across applies and comes back when turned on', async () => {
const { link, world } = createWorld()
world.files.set(PATHS.settingsFile, '{"enabled": false, "kapsam": "ide", "not": "ours"}')
world.files.set(PATHS.settingsFile, '{"enabled": false, "mod": "aiturk", "kapsam": "ide", "not": "ours"}')
expect(await link.apply()).toMatchObject({ enabled: false, bridgeRunning: false })
expect(spawns(world)).toEqual([])
@@ -343,6 +374,7 @@ describe('applyCodexLink', () => {
expect(await link.apply({ enabled: true })).toMatchObject({ enabled: true, bridgeRunning: true })
expect(JSON.parse(world.files.get(PATHS.settingsFile) ?? '')).toEqual({
enabled: true,
mod: 'aiturk',
kapsam: 'ide',
not: 'ours'
})
@@ -400,3 +432,44 @@ describe('getStatus', () => {
expect(effects(world)).toEqual([])
})
})
describe('connection mode', () => {
it('defaults to direct, starts bridge and autostart without writing a line', async () => {
const { link, world } = createWorld()
world.files.delete(PATHS.settingsFile)
expect(await link.apply()).toMatchObject({ mod: 'dogrudan', bridgeRunning: true })
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
expect(spawns(world)).toHaveLength(1)
expect(world.autostart).not.toBeNull()
})
it('removes the old line and preserves the current bridge in direct mode', async () => {
const { link, world } = createWorld({ listening: true })
world.files.set(PATHS.settingsFile, '{"mod":"dogrudan"}')
world.files.set(PATHS.codexConfig, `${CODEX_LINK_LINE}\r\n${USER_CONFIG}`)
await link.apply()
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
expect(spawns(world)).toHaveLength(0)
expect(world.events).not.toContain('stop')
expect(world.autostart).not.toBeNull()
})
it('switches both ways, persists mode and preserves unrelated preferences', async () => {
const { link, world } = createWorld({ listening: true })
world.files.set(PATHS.settingsFile, '{"mod":"dogrudan","custom":{"keep":1}}')
await link.apply({ mod: 'aiturk' })
expect(world.files.get(PATHS.codexConfig)).toContain(CODEX_LINK_LINE)
await link.apply({ mod: 'dogrudan' })
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
expect(JSON.parse(world.files.get(PATHS.settingsFile)!)).toMatchObject({ mod: 'dogrudan', custom: { keep: 1 } })
await link.apply()
expect(world.files.get(PATHS.codexConfig)).toBe(USER_CONFIG)
expect(world.events).not.toContain('stop')
})
it.each(['dogrudan', 'aiturk'] as const)('preserves foreign URL in %s', async mod => {
const { link, world } = createWorld()
const foreign = 'openai_base_url = "https://other.example"\n'
world.files.set(PATHS.codexConfig, foreign)
await link.apply({ mod })
expect(world.files.get(PATHS.codexConfig)).toBe(foreign)
expect(spawns(world)).toHaveLength(0)
})
})
+17 -6
View File
@@ -186,6 +186,7 @@ export function createCodexLink(deps: CodexLinkDeps): CodexLink {
bridge: BridgeProbe
): CodexLinkStatus => ({
enabled,
mod: readSettings().mod,
codexFound: config.found,
venvReady: interpreter() !== null,
foreign: config.owner === 'foreign',
@@ -218,10 +219,12 @@ export function createCodexLink(deps: CodexLinkDeps): CodexLink {
}
}
// Off: take back everything we put there. Each step is independent — a
// failure in one must not leave the others in place.
// Remove the owned URL before stopping its server. A failed config write
// must leave the working bridge alive so Codex is not stranded offline.
const tearDown = async (config: ReturnType<typeof readCodexConfig>): Promise<BridgeProbe> => {
removeOurLine(config.text)
if (!removeOurLine(config.text)) {
return probe()
}
try {
await deps.clearAutostart()
@@ -327,7 +330,8 @@ export function createCodexLink(deps: CodexLinkDeps): CodexLink {
return bridge
}
const edit = addCodexLinkLine(config.text ?? '')
const edit =
readSettings().mod === 'aiturk' ? addCodexLinkLine(config.text ?? '') : removeCodexLinkLine(config.text ?? '')
if (edit.changed) {
try {
@@ -362,9 +366,16 @@ export function createCodexLink(deps: CodexLinkDeps): CodexLink {
const run = async (patch?: CodexLinkPatch): Promise<CodexLinkStatus> => {
lastError = null
if (typeof patch?.enabled === 'boolean') {
if (typeof patch?.enabled === 'boolean' || patch?.mod === 'aiturk' || patch?.mod === 'dogrudan') {
try {
deps.writeTextAtomic(paths.settingsFile, serializeCodexLinkSettings(readSettings().stored, patch.enabled))
deps.writeTextAtomic(
paths.settingsFile,
serializeCodexLinkSettings(
readSettings().stored,
patch?.enabled ?? readSettings().enabled,
patch?.mod ?? readSettings().mod
)
)
} catch (error) {
// The preference did not stick, so do not act on it either: what the
// machine does must match what the file says after a restart.
@@ -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
}
@@ -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)
}
})
@@ -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
}
}
}
+71
View File
@@ -378,6 +378,8 @@ import { ensureLoginShellPath } from './shell-path'
import { createBootstrapCoordinator, sshConfigFingerprint } from './ssh-bootstrap-coordinator'
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'
@@ -17192,6 +17194,22 @@ 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'),
value =>
execFileSync(
'powershell.exe',
[
'-NoProfile',
'-NonInteractive',
'-Command',
'Add-Type -AssemblyName System.Security; $bytes=[Text.Encoding]::UTF8.GetBytes([Console]::In.ReadToEnd()); [Console]::Out.Write([Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Protect($bytes,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser)))'
],
{ input: value, encoding: 'utf8', windowsHide: true, timeout: 15000 }
).trim()
)
const codexLink = createCodexLink(
createNodeCodexLinkDeps(
@@ -17243,11 +17261,64 @@ ipcMain.handle('aiturk:codex-link:get', async event => {
return codexLink.getStatus()
})
async function stagedTransitionState() {
const state = stagedTransition.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 models = Array.isArray(body.jev_models)
? body.jev_models.filter(m => typeof m?.id === 'string' && typeof m?.name === 'string')
: []
return { ...state, models, modelsAvailable: Array.isArray(body.jev_models) }
} catch {
return { ...state, models: [], modelsAvailable: false }
}
}
ipcMain.handle('aiturk:staged-transition:get', async event => {
assertCodexLinkSender(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)
return stagedTransitionState()
})
ipcMain.handle('aiturk:codex-link:set', async (event, patch) => {
assertCodexLinkSender(event)
return codexLink.apply({
enabled: typeof patch?.enabled === 'boolean' ? patch.enabled : undefined,
mod: patch?.mod === 'aiturk' || patch?.mod === 'dogrudan' ? patch.mod : undefined,
sync: patch?.sync === true
})
})
+3 -2
View File
@@ -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<void>((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'))
+4
View File
@@ -295,6 +295,10 @@ 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'),
set: patch => ipcRenderer.invoke('aiturk:codex-link:set', patch)
}
@@ -0,0 +1,27 @@
export interface StagedTransitionSettings {
enabled: boolean
endpoint: string
model: string
fallbackModel: string
rejectionRules: { from: string; to: string }[]
exemptModels: string[]
thresholds: { ret: number; sart: number }
questions: { ret: string; sart: string }
criteria: { ret: { true: string; false: string }; sart: { true: string; false: string } }
timeoutSeconds: number
maxConcurrent: number
queueWaitSeconds: number
failureAction: 'pass' | 'next'
}
export interface StagedTransitionState {
settings: StagedTransitionSettings
keyConfigured: boolean
models?: { id: string; name: string }[]
modelsAvailable?: boolean
}
export interface StagedTransitionPatch {
settings: StagedTransitionSettings
apiKey?: string
}
@@ -0,0 +1,53 @@
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { createStagedTransitionStore, stagedTransitionDefaults, validateStagedTransition } from './staged-transition'
const dirs: string[] = []
afterEach(() => {
for (const dir of dirs.splice(0)) {
fs.rmSync(dir, { recursive: true, force: true })
}
})
describe('staged transition settings', () => {
it('persists rules and protects keys without returning a secret', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jev-settings-test-'))
dirs.push(dir)
const store = createStagedTransitionStore(dir, path.join(dir, 'missing.env'), () => 'encrypted-test-value')
const settings = structuredClone(stagedTransitionDefaults)
settings.fallbackModel = 'fallback'
settings.rejectionRules = [{ from: 'model-a', to: 'model-b' }]
const saved = store.save({ settings, apiKey: 'dummy-secret' })
expect(saved.settings.rejectionRules).toEqual(settings.rejectionRules)
expect(saved.keyConfigured).toBe(true)
expect(JSON.stringify(saved)).not.toContain('dummy-secret')
expect(fs.readFileSync(path.join(dir, 'jev-settings.json'), 'utf8')).not.toContain('dummy-secret')
expect(store.get()).toEqual(saved)
store.save({ settings: { ...settings, timeoutSeconds: 123 } })
expect(store.get().keyConfigured).toBe(true)
})
it('rejects invalid rules without replacing the saved settings', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jev-settings-test-'))
dirs.push(dir)
const store = createStagedTransitionStore(dir, path.join(dir, 'missing.env'), x => x)
const before = store.save({ settings: structuredClone(stagedTransitionDefaults) })
expect(() =>
store.save({
settings: {
...before.settings,
rejectionRules: [
{ from: 'a', to: 'b' },
{ from: 'b', to: 'a' }
]
}
})
).toThrow()
expect(store.get().settings).toEqual(before.settings)
expect(() => validateStagedTransition({ ...before.settings, maxConcurrent: -1 })).toThrow()
expect(() => validateStagedTransition({ ...before.settings, thresholds: { ret: NaN, sart: 0.7 } })).toThrow()
expect(() => validateStagedTransition({ ...before.settings, endpoint: 'http://example.com' })).toThrow()
})
})
+174
View File
@@ -0,0 +1,174 @@
import fs from 'node:fs'
import path from 'node:path'
import defaults from '../assets/codex-bridge/jev-settings-defaults.json'
import type { StagedTransitionPatch, StagedTransitionSettings, StagedTransitionState } from './staged-transition-types'
export const stagedTransitionDefaults = defaults as StagedTransitionSettings
export function validateStagedTransition(value: unknown): StagedTransitionSettings {
if (!value || typeof value !== 'object') {
throw new Error('Invalid settings')
}
const v = value as StagedTransitionSettings
const text = (s: unknown, max = 10000): s is string => typeof s === 'string' && s.trim().length > 0 && s.length <= max
const list = (s: unknown, allowEmpty: boolean) =>
Array.isArray(s) &&
(allowEmpty || s.length > 0) &&
s.length <= 64 &&
s.every(x => text(x, 256)) &&
new Set(s.map(x => x.trim())).size === s.length
const seconds = (n: unknown) => typeof n === 'number' && Number.isInteger(n) && n >= 1 && n <= 3600
if (typeof v.enabled !== 'boolean' || !text(v.endpoint, 2048) || !text(v.model, 256)) {
throw new Error('Invalid connection settings')
}
const url = new URL(v.endpoint)
if (url.protocol !== 'https:' || url.username || url.password || url.hash) {
throw new Error('HTTPS endpoint required')
}
if (
!text(v.fallbackModel, 256) ||
!list(v.exemptModels, true) ||
!Array.isArray(v.rejectionRules) ||
v.rejectionRules.length > 64 ||
v.rejectionRules.some(r => !text(r.from, 256) || !text(r.to, 256) || r.from.trim() === r.to.trim()) ||
new Set(v.rejectionRules.map(r => r.from.trim())).size !== v.rejectionRules.length
) {
throw new Error('Invalid model rules')
}
const routes = new Map(v.rejectionRules.map(r => [r.from.trim(), r.to.trim()]))
for (const start of routes.keys()) {
const seen = new Set<string>()
let current: string | undefined = start
while (current && !v.exemptModels.includes(current)) {
if (seen.has(current)) {
throw new Error('Model rules contain a cycle')
}
seen.add(current)
current = routes.get(current) ?? (current === v.fallbackModel.trim() ? undefined : v.fallbackModel.trim())
}
}
for (const id of ['ret', 'sart'] as const) {
if (
!text(v.questions?.[id]) ||
typeof v.thresholds?.[id] !== 'number' ||
!Number.isFinite(v.thresholds[id]) ||
v.thresholds[id] < 0 ||
v.thresholds[id] > 1
) {
throw new Error('Invalid evaluation rule')
}
for (const outcome of ['true', 'false'] as const) {
if (typeof v.criteria?.[id]?.[outcome] !== 'string' || v.criteria[id][outcome].length > 10000) {
throw new Error('Invalid decision criteria')
}
}
}
if (
!seconds(v.timeoutSeconds) ||
!seconds(v.queueWaitSeconds) ||
!Number.isInteger(v.maxConcurrent) ||
v.maxConcurrent < 0 ||
v.maxConcurrent > 512 ||
!['pass', 'next'].includes(v.failureAction)
) {
throw new Error('Invalid request limits')
}
return {
enabled: v.enabled,
endpoint: v.endpoint.trim(),
model: v.model.trim(),
fallbackModel: v.fallbackModel.trim(),
rejectionRules: v.rejectionRules.map(r => ({ from: r.from.trim(), to: r.to.trim() })),
exemptModels: v.exemptModels.map(x => x.trim()),
questions: { ...v.questions },
thresholds: { ...v.thresholds },
criteria: structuredClone(v.criteria),
timeoutSeconds: v.timeoutSeconds,
maxConcurrent: v.maxConcurrent,
queueWaitSeconds: v.queueWaitSeconds,
failureAction: v.failureAction
}
}
export function createStagedTransitionStore(
directory: string,
legacyKeyFile: string,
protect: (value: string) => string
) {
const file = path.join(directory, 'jev-settings.json')
const read = (): { settings: StagedTransitionSettings; protectedKey?: string } => {
if (!fs.existsSync(file)) {
return { settings: structuredClone(stagedTransitionDefaults) }
}
const data = JSON.parse(fs.readFileSync(file, 'utf8'))
return { settings: validateStagedTransition(data.settings), protectedKey: data.protectedKey }
}
const state = (): StagedTransitionState => {
const current = read()
let legacy = Boolean(process.env.TYPESAFE_API_KEY?.trim())
if (!legacy && fs.existsSync(legacyKeyFile)) {
const match = /^\s*TYPESAFE_API_KEY\s*=\s*(.*)$/m.exec(fs.readFileSync(legacyKeyFile, 'utf8'))
legacy = Boolean(
match?.[1]
?.trim()
.replace(/^(['"])(.*)\1$/, '$2')
.trim()
)
}
return { settings: current.settings, keyConfigured: Boolean(current.protectedKey) || legacy }
}
return {
get: state,
save(patch: StagedTransitionPatch): StagedTransitionState {
const settings = validateStagedTransition(patch?.settings)
if (patch.apiKey !== undefined && (typeof patch.apiKey !== 'string' || patch.apiKey.length > 8192)) {
throw new Error('Invalid API key')
}
const current = read()
const protectedKey =
patch.apiKey === undefined
? current.protectedKey
: patch.apiKey.trim()
? protect(patch.apiKey.trim())
: undefined
fs.mkdirSync(directory, { recursive: true })
const tmp = `${file}.${process.pid}.tmp`
try {
fs.writeFileSync(tmp, JSON.stringify({ settings, protectedKey }, null, 2), { mode: 0o600 })
fs.renameSync(tmp, file)
} finally {
if (fs.existsSync(tmp)) {
fs.unlinkSync(tmp)
}
}
return state()
}
}
}
+7 -2
View File
@@ -2,7 +2,7 @@
"name": "aiturk-ide",
"productName": "AITURK IDE",
"private": true,
"version": "1.0.0-beta.28",
"version": "1.0.0-beta.38",
"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",
@@ -304,8 +306,11 @@
"from": "assets/codex-bridge",
"to": "codex-bridge",
"filter": [
"jev-settings-defaults.json",
"compression-defaults.json",
"**/*.py",
"**/*.pyd",
"native-watch.exe",
"**/LICENSE",
"!test_*.py"
]
+2
View File
@@ -57,6 +57,7 @@
* - electronPlatformName: 'win32' | 'darwin' | 'linux'
* - arch: Arch enum (0=ia32, 1=x64, 2=armv7l, 3=arm64, 4=universal)
*/
import { buildNativeWatchLauncher } from './build-native-watch-launcher.mjs'
import { existsSync, rmSync, renameSync } from 'node:fs'
import path from 'node:path'
import { Arch } from 'electron-builder'
@@ -111,6 +112,7 @@ export function preserveRollbackBackup(appOutDir, productExeName = 'Hermes.exe')
}
export default async function beforePack(context) {
if (context?.electronPlatformName === 'win32') buildNativeWatchLauncher()
const appOutDir = context && context.appOutDir
const platformName = context && context.electronPlatformName
try {
@@ -0,0 +1,30 @@
/** Build the Windows artifact from source on Windows or the Linux/Wine CI host. */
import { execFileSync } from 'node:child_process'
import { rmSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const sourceHome = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../assets/codex-bridge')
export function compilerCommand({ platform = process.platform, env = process.env, source, output }) {
const args = ['/nologo', '/target:exe', '/platform:anycpu', '/reference:System.Web.Extensions.dll', `/out:${output}`, source]
if (platform === 'win32') {
return { command: path.win32.join(env.SystemRoot || 'C:/Windows', 'Microsoft.NET/Framework64/v4.0.30319/csc.exe'), args }
}
if (platform === 'linux') return { command: 'mcs', args }
throw new Error(`Windows launcher compilation is not configured on ${platform}`)
}
export function buildNativeWatchLauncher({ home = sourceHome, ...options } = {}) {
const output = path.join(home, 'native-watch.exe')
const invocation = compilerCommand({ ...options, source: path.join(home, 'NativeWatchBootstrap.cs'), output })
// A missing compiler or broken source must fail packaging, never reuse an old EXE.
rmSync(output, { force: true })
execFileSync(invocation.command, invocation.args, { windowsHide: true, stdio: 'inherit' })
const bytes = readFileSync(output)
if (bytes.length < 64 || bytes.toString('ascii', 0, 2) !== 'MZ' || bytes.toString('ascii', bytes.readUInt32LE(60), bytes.readUInt32LE(60) + 4) !== 'PE\0\0') {
rmSync(output, { force: true })
throw new Error('Native watch compiler did not produce a Windows PE executable')
}
return output
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) buildNativeWatchLauncher()
@@ -0,0 +1,36 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { mkdtempSync, copyFileSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { compilerCommand, buildNativeWatchLauncher } from './build-native-watch-launcher.mjs'
const desktop = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
test('Windows and Linux hosts build the same Windows source with the required assembly', () => {
const input = { source: 'source with spaces.cs', output: 'output with spaces.exe' }
const win = compilerCommand({ ...input, platform: 'win32', env: { SystemRoot: 'D:/Windows' } })
const linux = compilerCommand({ ...input, platform: 'linux' })
assert.equal(win.command, 'D:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\csc.exe')
assert.equal(linux.command, 'mcs')
assert.deepEqual(win.args, linux.args)
assert.ok(linux.args.includes('/reference:System.Web.Extensions.dll'))
assert.equal(linux.args.at(-1), input.source)
assert.throws(() => compilerCommand({ ...input, platform: 'darwin' }), /not configured/)
})
test('real host compiler builds PE from source, and broken source fails without stale artifact', () => {
// Inside the source workspace, including on the mounted Linux CI fixture.
const home = mkdtempSync(path.join(desktop, '.native-watch-test-'))
try {
copyFileSync(path.join(desktop, 'assets/codex-bridge/NativeWatchBootstrap.cs'), path.join(home, 'NativeWatchBootstrap.cs'))
const exe = buildNativeWatchLauncher({ home })
const first = readFileSync(exe)
assert.equal(first.toString('ascii', 0, 2), 'MZ')
writeFileSync(exe, 'stale artifact')
buildNativeWatchLauncher({ home })
assert.equal(readFileSync(exe).toString('ascii', 0, 2), 'MZ')
writeFileSync(path.join(home, 'NativeWatchBootstrap.cs'), 'invalid C# source')
assert.throws(() => buildNativeWatchLauncher({ home }))
assert.equal(existsSync(exe), false)
} finally { rmSync(home, { recursive: true, force: true }) }
})
@@ -12,6 +12,7 @@ vi.mock('@/store/notifications', () => ({ notifyError }))
const RUNNING: CodexLinkStatus = {
enabled: true,
mod: 'aiturk',
codexFound: true,
venvReady: true,
foreign: false,
@@ -139,6 +140,13 @@ describe('Codex Link settings', () => {
expect(screen.getByRole('switch', { name: 'Codex Bağlama' }).getAttribute('aria-checked')).toBe('false')
})
it('does not claim original settings were restored when disconnect failed', async () => {
get.mockResolvedValue({ ...RUNNING, enabled: false, lastError: 'config-write-failed' })
renderPage()
expect(await screen.findByText('Codex ayar dosyası yazılamadı.')).toBeTruthy()
expect(screen.queryByText('Codex Bağlama kapalı. Codex kendi ayarlarıyla çalışıyor.')).toBeNull()
})
it('rolls the switch back and reports when the change fails', async () => {
get.mockResolvedValue(RUNNING)
set.mockRejectedValue(new Error('codex-link:unavailable'))
@@ -191,3 +199,18 @@ describe('Codex Link settings', () => {
expect(get).not.toHaveBeenCalled()
})
})
describe('connection mode controls', () => {
it('sends the mode and renders the saved direct preference', async () => {
get.mockResolvedValue({ ...RUNNING, mod: 'dogrudan' })
set.mockResolvedValue({ ...RUNNING, mod: 'aiturk' })
renderPage()
const direct = await screen.findByRole('radio', { name: 'Doğrudan (önerilen): abonelik modelleri köprüden bağımsız' })
await waitFor(() => expect((direct as HTMLInputElement).checked).toBe(true))
fireEvent.click(screen.getByRole('radio', { name: 'AITURK: combolar Codex listesinde, tüm modeller köprüden' }))
await waitFor(() => expect(set).toHaveBeenCalledWith({ mod: 'aiturk' }))
await waitFor(() => expect((direct as HTMLInputElement).checked).toBe(false))
expect(screen.getByText("Codex'i yeniden başlatın")).toBeTruthy()
})
})
@@ -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'
@@ -40,6 +41,7 @@ export function codexLinkStatusLines(status: CodexLinkStatus, copy: CodexLinkCop
}
if (!status.enabled) {
if (status.lastError) return [...lines, ...error]
return [...lines, { id: 'off', text: copy.off, tone: 'muted' }, ...error]
}
@@ -55,7 +57,7 @@ export function codexLinkStatusLines(status: CodexLinkStatus, copy: CodexLinkCop
if (status.ideDurum === 'anahtar-yok') {
lines.push({ id: 'ide', text: copy.signIn, tone: 'warn' })
} else if (status.ideDurum === 'tamam' && status.ideModel !== null) {
} else if (status.mod === 'aiturk' && status.ideDurum === 'tamam' && status.ideModel !== null) {
lines.push({ id: 'ide', text: copy.models(status.ideModel), tone: 'ok' })
}
@@ -168,6 +170,27 @@ export function CodexLinkSettings() {
title={copy.title}
/>
<ListRow
title={copy.modeTitle}
below={
<fieldset disabled={!status || busy !== null} className="grid gap-2 py-2">
{(['dogrudan', 'aiturk'] as const).map(mod => (
<label key={mod} className="flex min-h-11 cursor-pointer items-center gap-2">
<input
type="radio"
name="codex-link-mode"
value={mod}
checked={(status?.mod ?? 'dogrudan') === mod}
onChange={() => void change({ mod }, 'toggle')}
/>
<span>{mod === 'dogrudan' ? copy.directMode : copy.aiturkMode}</span>
</label>
))}
<p className="text-(--ui-text-tertiary)">{copy.modeRestartNote}</p>
</fieldset>
}
/>
{!status && !loadFailed && <ListRowSkeleton wide />}
{loadFailed && !status && (
@@ -228,6 +251,7 @@ export function CodexLinkSettings() {
{copy.restartNote}
</p>
</SettingsSection>
<CompressionSettingsView />
</SettingsContent>
)
}
@@ -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(<I18nProvider configClient={null} initialLocale="tr"><CompressionSettingsView /></I18nProvider>)
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(<I18nProvider configClient={null} initialLocale="tr"><CompressionSettingsView /></I18nProvider>)
fireEvent.click(await screen.findByRole('button', { name: 'Kaydet' }))
expect((await screen.findByRole('alert')).textContent).toContain('Kaydedilemedi')
})
@@ -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<CompressionState | null>(null)
const [draft, setDraft] = useState<CompressionSettings | null>(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 = <K extends keyof CompressionSettings>(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) => (
<ListRow key={key} title={label} action={<Input aria-label={label} className="min-h-11 w-32" type="number" min={min} max={max} step={1} disabled={busy} value={draft?.[key] ?? ''} onChange={e => 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 <ListRow title={label} action={
<select aria-label={label} className="min-h-11 max-w-full rounded-md border bg-(--ui-bg) p-2" disabled={busy} value={selected} onChange={e => change(key, e.target.value)}>
<option value="">{key === 'primaryModel' ? t('Mevcut model (ayrı çağrı)', 'Current model (separate call)') : t('Yedek seçilmedi', 'No fallback selected')}</option>
{selected && !models.some(m => m.id === selected) && <option value={selected}>{selected} — {t('katalogda doğrulanmadı', 'not verified in catalog')}</option>}
{models.map(m => <option key={m.id} value={m.id}>{m.name} ({Math.round(m.window / 1000)}K)</option>)}
</select>
} />
}
return <SettingsSection icon={Link} title={t('Bağlam ve Sıkıştırma', 'Context and Compression')}>
<p className="text-sm text-(--ui-text-secondary)">{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.')}</p>
{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 && <p role="status">{t('Model kataloğuna ulaşılamadı.', 'Model catalog unavailable.')}</p>}
<ListRow title={t('Mevcut modele geri dönüş', 'Fall back to current model')} description={t('Sıkıştırıcılar başarısız olursa ve geçmiş mevcut modele sığıyorsa kullanılır.', 'Used if summarizers fail and the history fits the current model.')} action={<Switch aria-label={t('Mevcut modele geri dönüş', 'Fall back to current model')} checked={draft.currentModelFallback} disabled={busy} onCheckedChange={v => change('currentModelFallback', v)} />} />
<ListRow title={t('Sıkıştırma seviyesi', 'Compression level')} action={<select aria-label={t('Sıkıştırma seviyesi', 'Compression level')} className="min-h-11 rounded-md border p-2" value={draft.level} disabled={busy} onChange={e => change('level', e.target.value as CompressionSettings['level'])}>
{(['mini', 'az', 'orta', 'fazla', 'cok-fazla'] as const).map((value, i) => <option key={value} value={value}>{(tr ? ['Mini', 'Az', 'Orta', 'Fazla', 'Çok Fazla'] : ['Minimal', 'Light', 'Medium', 'High', 'Very high'])[i]}</option>)}
</select>} />
{numeric('keepTurns', t('Korunan son mesajlaşma sayısı', 'Recent exchanges to preserve'), 1, 100)}
<p className="text-sm text-(--ui-text-secondary)">{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.')}</p>
<ListRow title={t('Geçiş bütçesi', 'Transfer budget')} action={<select aria-label={t('Geçiş bütçesi', 'Transfer budget')} className="min-h-11 rounded-md border p-2" disabled={busy} value={draft.budgetMode} onChange={e => change('budgetMode', e.target.value as CompressionSettings['budgetMode'])}><option value="percent">{t('Yüzde', 'Percent')}</option><option value="tokens">{t('Maksimum token', 'Maximum tokens')}</option></select>} />
{draft.budgetMode === 'percent' ? numeric('transferPercent', t('Hedef model bağlamı (%)', 'Target model context (%)'), 10, 90) : numeric('maxTokens', t('Maksimum giriş tokenı', 'Maximum input tokens'), 1024, 2000000)}
<ListRow title={t('Otomatik sıkıştırma', 'Automatic compression')} action={<Switch aria-label={t('Otomatik sıkıştırma', 'Automatic compression')} disabled={busy} checked={draft.autoEnabled} onCheckedChange={v => change('autoEnabled', v)} />} />
{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)}
<Button disabled={busy || draft.targetPercent >= 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')}</Button>
</>}
{message && <p role={failed ? 'alert' : 'status'}>{message}</p>}
</SettingsSection>
}
+27 -3
View File
@@ -58,6 +58,7 @@ import { NotificationsSettings } from './notifications-settings'
import { PluginsSettings } from './plugins-settings'
import { PROVIDER_VIEWS, ProvidersSettings, type ProviderView } from './providers-settings'
import { SessionsSettings } from './sessions-settings'
import { StagedTransitionSettingsView } from './staged-transition-settings'
import type { SettingsPageProps, SettingsView as SettingsViewId } from './types'
const SETTINGS_VIEWS: readonly SettingsViewId[] = [
@@ -72,13 +73,14 @@ const SETTINGS_VIEWS: readonly SettingsViewId[] = [
'notifications',
'billing',
'codex-link',
'staged-transition',
'plugins',
'sessions',
'about'
]
export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: SettingsPageProps) {
const { t } = useI18n()
const { t, locale } = useI18n()
const aiturkCopy = useAiturkCopy()
const codexLinkCopy = useCodexLinkCopy()
const navigate = useNavigate()
@@ -267,7 +269,9 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
id: 'aiturk-phone',
label: aiturkCopy.mobile.nav,
onSelect: () => {
void window.hermesDesktop?.aiturkMobile?.open().catch(error => notifyError(error, aiturkCopy.mobile.openFailed))
void window.hermesDesktop?.aiturkMobile
?.open()
.catch(error => notifyError(error, aiturkCopy.mobile.openFailed))
}
}
]
@@ -282,6 +286,13 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
id: 'codex-link',
label: codexLinkCopy.nav,
onSelect: () => setActiveView('codex-link')
},
{
active: activeView === 'staged-transition',
icon: Link,
id: 'staged-transition',
label: locale === 'tr' ? 'Kademeli Geçiş' : 'Staged Transition',
onSelect: () => setActiveView('staged-transition')
}
]
: []),
@@ -338,7 +349,18 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
onSelect: () => setActiveView('about')
}
],
[activeView, keysView, providerView, t, aiturkCopy, codexLinkCopy, setActiveView, openProviderView, openKeysView]
[
activeView,
keysView,
providerView,
t,
locale,
aiturkCopy,
codexLinkCopy,
setActiveView,
openProviderView,
openKeysView
]
)
// Type-to-search: printable keystrokes on the Settings surface (outside any
@@ -459,6 +481,8 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set
<PluginsSettings />
) : activeView === 'codex-link' ? (
<CodexLinkSettings />
) : activeView === 'staged-transition' ? (
<StagedTransitionSettingsView />
) : (
<SessionsSettings />
)
@@ -0,0 +1,71 @@
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { I18nProvider } from '@/i18n/context'
import defaults from '../../../assets/codex-bridge/jev-settings-defaults.json'
import { StagedTransitionSettingsView } from './staged-transition-settings'
const get = vi.fn()
const set = vi.fn()
const state = () => ({
settings: structuredClone(defaults),
keyConfigured: true,
modelsAvailable: true,
models: [
{ id: 'A', name: 'Model A' },
{ id: 'B', name: 'Model B' }
]
})
beforeEach(() => {
;(window as unknown as { hermesDesktop: unknown }).hermesDesktop = {
codexLink: { getStagedTransition: get, setStagedTransition: set }
}
get.mockResolvedValue(state())
set.mockImplementation(async patch => ({ ...state(), settings: patch.settings }))
})
afterEach(() => {
cleanup()
vi.resetAllMocks()
})
const show = () =>
render(
<I18nProvider configClient={null} initialLocale="tr">
<StagedTransitionSettingsView />
</I18nProvider>
)
describe('staged transition settings UI', () => {
it('adds and removes model routing and always-pass rows', async () => {
show()
await screen.findByText('Kademeli Geçiş')
fireEvent.click(screen.getByText('+ Geçiş kuralı ekle'))
expect(screen.getByLabelText('İtiraz edilen model 4')).toBeTruthy()
expect(screen.getByLabelText('Kullanılacak model 4')).toBeTruthy()
fireEvent.click(screen.getByLabelText('Geçiş kuralı 4 sil'))
expect(screen.queryByLabelText('İtiraz edilen model 4')).toBeNull()
fireEvent.click(screen.getByText('+ Her zaman geç kuralı ekle'))
expect(screen.getByLabelText('Her zaman geçen model 3')).toBeTruthy()
fireEvent.click(screen.getByLabelText('Her zaman geç kuralı 3 sil'))
fireEvent.change(screen.getByLabelText('İsteği yerine getirmeme denetim metni'), {
target: { value: 'custom prompt' }
})
fireEvent.click(screen.getByText('Kaydet'))
await screen.findByText('Kaydedildi. Yeni değerlendirmelerde uygulanır.')
expect(set.mock.calls[0][0].settings.questions.ret).toBe('custom prompt')
expect(set.mock.calls[0][0]).not.toHaveProperty('apiKey')
})
it('retains user edits if persistence fails', async () => {
set.mockRejectedValue(new Error('disk error'))
show()
await screen.findByText('Kademeli Geçiş')
fireEvent.change(screen.getByLabelText('Jev modeli'), { target: { value: 'new-model' } })
fireEvent.click(screen.getByText('Kaydet'))
await waitFor(() => expect(screen.getByRole('alert').textContent).toContain('Kaydedilemedi'))
expect((screen.getByLabelText('Jev modeli') as HTMLInputElement).value).toBe('new-model')
})
})
@@ -0,0 +1,463 @@
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import { useI18n } from '@/i18n/context'
import { Link } from '@/lib/icons'
import defaultSettings from '../../../assets/codex-bridge/jev-settings-defaults.json'
import type { StagedTransitionSettings, StagedTransitionState } from '../../../electron/staged-transition-types'
import { ListRow, SettingsContent, SettingsSection } from './primitives'
export function StagedTransitionSettingsView() {
const { locale } = useI18n()
const tr = locale === 'tr'
const text = (turkish: string, english: string) => (tr ? turkish : english)
const api = window.hermesDesktop?.codexLink
const [state, setState] = useState<StagedTransitionState | null>(null)
const [draft, setDraft] = useState<StagedTransitionSettings | null>(null)
const [key, setKey] = useState('')
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState('')
const [error, setError] = useState(false)
const adopt = (next: StagedTransitionState) => {
setState(next)
setDraft(next.settings)
}
useEffect(() => {
let active = true
if (api?.getStagedTransition) {
api.getStagedTransition().then(
next => {
if (active) {
adopt(next)
}
},
() => {
if (active) {
setError(true)
setMessage(tr ? 'Ayarlar okunamadı.' : 'Could not read settings.')
}
}
)
}
return () => {
active = false
}
}, [api, tr])
if (!api?.getStagedTransition) {
return (
<SettingsContent>
<p>
{text('Bu bölüm Windows masaüstü uygulamasında kullanılabilir.', 'Available in the Windows desktop app.')}
</p>
</SettingsContent>
)
}
if (!draft) {
return (
<SettingsContent>
<p role={error ? 'alert' : 'status'}>{message || text('Yükleniyor…', 'Loading…')}</p>
</SettingsContent>
)
}
const change = <K extends keyof StagedTransitionSettings>(name: K, value: StagedTransitionSettings[K]) =>
setDraft({ ...draft, [name]: value })
const models = new Map((state?.models ?? []).map(model => [model.id, model.name]))
const storedModels = [
draft.fallbackModel,
...draft.exemptModels,
...draft.rejectionRules.flatMap(rule => [rule.from, rule.to])
]
for (const id of storedModels) {
if (id && !models.has(id)) {
models.set(id, `${id} (${text('kayıtlı', 'saved')})`)
}
}
const modelSelect = (label: string, value: string, onChange: (value: string) => void) => (
<div className="w-full min-w-0 sm:w-auto sm:min-w-48 sm:flex-1">
<Select onValueChange={onChange} value={value}>
<SelectTrigger aria-label={label}>
<SelectValue placeholder={text('Model seçin', 'Select a model')} />
</SelectTrigger>
<SelectContent>
{Array.from(models, ([id, name]) => (
<SelectItem key={id} value={id}>
{name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
const number = (
name: 'timeoutSeconds' | 'maxConcurrent' | 'queueWaitSeconds',
label: string,
min: number,
max: number
) => (
<ListRow
action={
<Input
aria-label={label}
max={max}
min={min}
onChange={event => change(name, Number(event.target.value))}
type="number"
value={draft[name]}
/>
}
title={label}
/>
)
const save = async () => {
setBusy(true)
setMessage('')
setError(false)
try {
adopt(await api.setStagedTransition({ settings: draft, ...(key.trim() ? { apiKey: key.trim() } : {}) }))
setKey('')
setMessage(text('Kaydedildi. Yeni değerlendirmelerde uygulanır.', 'Saved. Applies to new evaluations.'))
} catch (failure) {
setError(true)
const cycle = String(failure).includes('cycle')
setMessage(
cycle
? text(
'Geçiş kuralları döngü oluşturuyor. Aynı modele geri dönen geçişi değiştirin.',
'Transition rules form a cycle. Change the rule that returns to an earlier model.'
)
: text(
'Kaydedilemedi. Alanları ve sayı aralıklarını kontrol edin; önceki ayarlar korunuyor.',
'Could not save. Check the fields and number ranges; previous settings are preserved.'
)
)
} finally {
setBusy(false)
}
}
return (
<SettingsContent>
<fieldset className="min-w-0" disabled={busy}>
<SettingsSection icon={Link} title={text('Kademeli Geçiş', 'Staged Transition')}>
<ListRow
action={
<Switch
aria-label={text('Jev denetimi', 'Jev evaluation')}
checked={draft.enabled}
onCheckedChange={value => change('enabled', value)}
/>
}
description={text(
'Köprüdeki ek modellerin nihai cevaplarını denetler. Uygun bulunmayan cevap sıradaki katmanda yeniden hazırlanır.',
'Evaluates final answers from bridge models. Rejected answers are retried with the next layer.'
)}
title={text('Jev denetimi ve kademeli geçiş', 'Jev evaluation and staged transition')}
/>
<ListRow
action={
<Input
aria-label={text('Jev modeli', 'Jev model')}
onChange={e => change('model', e.target.value)}
value={draft.model}
/>
}
title={text('Jev modeli', 'Jev model')}
/>
<ListRow
action={
<Input
aria-label={text('API adresi', 'API endpoint')}
onChange={e => change('endpoint', e.target.value)}
type="url"
value={draft.endpoint}
/>
}
title={text('API adresi', 'API endpoint')}
/>
<ListRow
action={
<Input
aria-label={text('TypeSafe API anahtarı', 'TypeSafe API key')}
autoComplete="new-password"
onChange={e => setKey(e.target.value)}
type="password"
value={key}
/>
}
description={
state?.keyConfigured
? text(
'Anahtar tanımlı. Değiştirmek için yeni anahtarı girin.',
'Key configured. Enter a new key to replace it.'
)
: text('Henüz anahtar tanımlı değil.', 'No key configured.')
}
title={text('TypeSafe API anahtarı', 'TypeSafe API key')}
/>
</SettingsSection>
<SettingsSection icon={Link} title={text('Denetim kuralları', 'Evaluation rules')}>
{(['ret', 'sart'] as const).map(id => {
const label =
id === 'ret'
? text('İsteği yerine getirmeme', 'Request not fulfilled')
: text('İsteği kendiliğinden değiştirme', 'Unrequested changes')
return (
<div key={id}>
<ListRow
action={
<Input
aria-label={`${label} ${text('eşiği', 'threshold')}`}
max={1}
min={0}
onChange={e => change('thresholds', { ...draft.thresholds, [id]: Number(e.target.value) })}
step={0.01}
type="number"
value={draft.thresholds[id]}
/>
}
description={text(
'0–1 arasında geçiş eşiği. Olasılık bu değere ulaşırsa sıradaki katman denenir.',
'Transition threshold from 0 to 1. A probability at or above this value tries the next layer.'
)}
title={label}
/>
<Textarea
aria-label={`${label} ${text('denetim metni', 'instructions')}`}
onChange={e => change('questions', { ...draft.questions, [id]: e.target.value })}
rows={4}
value={draft.questions[id]}
/>
{(['true', 'false'] as const).map(outcome => (
<label className="block py-2" key={outcome}>
<span>
{outcome === 'true'
? text(
'“Evet, sorun var” ne demek? (isteğe bağlı)',
'What does “yes, there is a problem” mean? (optional)'
)
: text(
'“Hayır, sorun yok” ne demek? (isteğe bağlı)',
'What does “no problem” mean? (optional)'
)}
</span>
<Textarea
aria-label={`${label} ${outcome}`}
onChange={e =>
change('criteria', {
...draft.criteria,
[id]: { ...draft.criteria[id], [outcome]: e.target.value }
})
}
rows={2}
value={draft.criteria[id][outcome]}
/>
</label>
))}
</div>
)
})}
</SettingsSection>
<SettingsSection
icon={Link}
title={text('Olasılık, onay ve sıcaklık', 'Probability, approval and temperature')}
>
<p>
{text(
'Jev her denetim sorusu için 0–1 arasında sorun olasılığı verir. Her iki olasılık da kendi itiraz eşiğinin altındaysa cevap geçer. Örneğin 0,70 eşiği, sorun olasılığı %70 veya üstündeyse geçiş demektir; %70 kalite garantisi değildir.',
'Jev returns a problem probability from 0 to 1 for each question. An answer passes when both probabilities are below their rejection thresholds. A threshold of 0.70 triggers transition at 70% problem probability; it is not a quality guarantee.'
)}
</p>
<ListRow
description={text('TypeSafe API bu parametreyi sunmuyor.', 'Not exposed by the TypeSafe API.')}
title={text('Sıcaklık', 'Temperature')}
/>
<ListRow
description={text(
'Kullanılan Noul (evet/hayır) türünde ayrı güven skoru yoktur. Karar yukarıdaki olasılık eşikleriyle verilir.',
'The Noul (yes/no) type has no separate confidence score. Decisions use the probability thresholds above.'
)}
title={text('Ayrı emin olma / güven değeri', 'Separate confidence value')}
/>
</SettingsSection>
<SettingsSection icon={Link} title={text('Geçiş kuralları', 'Transition rules')}>
{!state?.modelsAvailable && (
<p role="status">
{text(
'Canlı model listesi alınamadı. Kayıtlı seçimler korunuyor; Codex Bağlama açıkken listeyi yenileyin.',
'Live model list unavailable. Saved selections are preserved; refresh with Codex Link enabled.'
)}
</p>
)}
<Button
onClick={async () => {
try {
const next = await api.getStagedTransition()
setState(next)
} catch {
setError(true)
setMessage(text('Model listesi alınamadı.', 'Could not load models.'))
}
}}
variant="outline"
>
{text('Model listesini yenile', 'Refresh models')}
</Button>
<ListRow
action={modelSelect(text('Genel geçiş modeli', 'Default fallback model'), draft.fallbackModel, value =>
change('fallbackModel', value)
)}
description={text(
'Modele özel kural yoksa seçilen model kullanılır.',
'Uses this model when there is no model-specific rule.'
)}
title={text('Jev herhangi bir modele itiraz ederse', 'When Jev rejects any model')}
/>
{draft.rejectionRules.map((rule, index) => (
<div className="flex flex-wrap items-center gap-2 py-2" key={index}>
{modelSelect(text(`İtiraz edilen model ${index + 1}`, `Rejected model ${index + 1}`), rule.from, value =>
change(
'rejectionRules',
draft.rejectionRules.map((r, i) => (i === index ? { ...r, from: value } : r))
)
)}
<span>{text('modeline Jev itiraz ederse', 'rejected by Jev →')}</span>
{modelSelect(text(`Kullanılacak model ${index + 1}`, `Fallback model ${index + 1}`), rule.to, value =>
change(
'rejectionRules',
draft.rejectionRules.map((r, i) => (i === index ? { ...r, to: value } : r))
)
)}
<span>{text('modeli kullanılsın', 'use this model')}</span>
<Button
aria-label={text(`Geçiş kuralı ${index + 1} sil`, `Remove rule ${index + 1}`)}
onClick={() =>
change(
'rejectionRules',
draft.rejectionRules.filter((_, i) => i !== index)
)
}
variant="ghost"
>
{text('Sil', 'Remove')}
</Button>
</div>
))}
<Button
onClick={() => change('rejectionRules', [...draft.rejectionRules, { from: '', to: '' }])}
variant="outline"
>
{text('+ Geçiş kuralı ekle', '+ Add transition rule')}
</Button>
</SettingsSection>
<SettingsSection icon={Link} title={text('Jev’den her zaman geçsin', 'Always pass Jev')}>
<p>
{text(
'Bu modeller Jev denetimine gönderilmez. Bu seçim, modele özel ve genel itiraz kurallarından önce uygulanır.',
'These models bypass Jev evaluation. This takes priority over model-specific and default rejection rules.'
)}
</p>
{draft.exemptModels.map((model, index) => (
<div className="flex flex-wrap items-center gap-2 py-2" key={index}>
{modelSelect(text(`Her zaman geçen model ${index + 1}`, `Always-pass model ${index + 1}`), model, value =>
change(
'exemptModels',
draft.exemptModels.map((m, i) => (i === index ? value : m))
)
)}
<span>{text('modeli Jev’den her zaman geçsin', 'always passes Jev')}</span>
<Button
aria-label={text(`Her zaman geç kuralı ${index + 1} sil`, `Remove always-pass rule ${index + 1}`)}
onClick={() =>
change(
'exemptModels',
draft.exemptModels.filter((_, i) => i !== index)
)
}
variant="ghost"
>
{text('Sil', 'Remove')}
</Button>
</div>
))}
<Button onClick={() => change('exemptModels', [...draft.exemptModels, ''])} variant="outline">
{text('+ Her zaman geç kuralı ekle', '+ Add always-pass rule')}
</Button>
</SettingsSection>
<SettingsSection icon={Link} title={text('Paralellik ve bekleme', 'Concurrency and waiting')}>
{number(
'maxConcurrent',
text('Eşzamanlı Jev isteği (0: yerel sınır yok)', 'Concurrent Jev requests (0: no local limit)'),
0,
512
)}
{number(
'queueWaitSeconds',
text('Sırada en fazla bekleme (saniye)', 'Maximum queue wait (seconds)'),
1,
3600
)}
{number('timeoutSeconds', text('Jev yanıt zaman aşımı (saniye)', 'Jev response timeout (seconds)'), 1, 3600)}
<ListRow
action={
<Select
onValueChange={value => change('failureAction', value as 'pass' | 'next')}
value={draft.failureAction}
>
<SelectTrigger aria-label={text('Jev’e ulaşılamadığında', 'When Jev is unavailable')}>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="pass">{text('Mevcut cevabı ilet', 'Pass the current answer')}</SelectItem>
<SelectItem value="next">{text('Sıradaki katmanı dene', 'Try the next layer')}</SelectItem>
</SelectContent>
</Select>
}
title={text('Jev’e ulaşılamadığında', 'When Jev is unavailable')}
/>
</SettingsSection>
<Button onClick={() => void save()}>{busy ? text('Kaydediliyor…', 'Saving…') : text('Kaydet', 'Save')}</Button>
<Button
onClick={() => {
setDraft(structuredClone(defaultSettings) as StagedTransitionSettings)
setMessage(
text(
'Varsayılanlar forma yüklendi. Uygulamak için Kaydet’e basın.',
'Defaults loaded into the form. Save to apply.'
)
)
setError(false)
}}
variant="ghost"
>
{text('Varsayılanları yükle', 'Load defaults')}
</Button>
{message && (
<p className="py-3" role={error ? 'alert' : 'status'}>
{message}
</p>
)}
</fieldset>
</SettingsContent>
)
}
+1
View File
@@ -16,6 +16,7 @@ export type SettingsView =
| 'plugins'
| 'providers'
| 'sessions'
| 'staged-transition'
| `config:${string}`
export type EnvPatch = Partial<Pick<EnvVarInfo, 'is_set' | 'redacted_value'>>
@@ -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 !!!')
})
+6
View File
@@ -2,7 +2,9 @@ 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'
import type { WakeIndicatorState } from './lib/wake-indicator'
import type {
@@ -340,6 +342,10 @@ declare global {
* use the IDE's models. `get` only reads; `set` applies and returns the
* state that actually resulted. Windows only. */
codexLink?: {
getCompression: () => Promise<CompressionState>
setCompression: (settings: CompressionSettings) => Promise<CompressionState>
getStagedTransition: () => Promise<StagedTransitionState>
setStagedTransition: (patch: StagedTransitionPatch) => Promise<StagedTransitionState>
get: () => Promise<CodexLinkStatus>
set: (patch: CodexLinkPatch) => Promise<CodexLinkStatus>
}
+44 -1
View File
@@ -6,6 +6,10 @@ import { useI18n } from './context'
// AITURK-distribution strings (see ./aiturk): Turkish for `tr`, English for
// every other locale.
const en = {
modeTitle: 'Connection mode',
directMode: 'Direct (recommended): subscription models independent of the bridge',
aiturkMode: 'AITURK: combos in the Codex list, all models through the bridge',
modeRestartNote: 'Restart Codex.',
nav: 'Codex Link',
title: 'Codex Link',
description:
@@ -39,6 +43,10 @@ const en = {
}
const tr: typeof en = {
modeTitle: 'Bağlantı modu',
directMode: 'Doğrudan (önerilen): abonelik modelleri köprüden bağımsız',
aiturkMode: 'AITURK: combolar Codex listesinde, tüm modeller köprüden',
modeRestartNote: "Codex'i yeniden başlatın",
nav: 'Codex Bağlama',
title: 'Codex Bağlama',
description:
@@ -71,10 +79,45 @@ const tr: typeof en = {
restartNote: "Codex'te yeni modelleri görmek için Codex'i yeniden açın."
}
const modeTranslations = {
zh: {
modeTitle: '连接模式',
directMode: '直接(推荐):订阅模型独立于桥接',
aiturkMode: 'AITURK:组合显示在 Codex 列表中,所有模型通过桥接',
modeRestartNote: '请重启 Codex。'
},
'zh-hant': {
modeTitle: '連線模式',
directMode: '直接(建議):訂閱模型獨立於橋接',
aiturkMode: 'AITURK:組合顯示在 Codex 清單中,所有模型透過橋接',
modeRestartNote: '請重新啟動 Codex。'
},
ja: {
modeTitle: '接続モード',
directMode: '直接(推奨):サブスクリプションモデルはブリッジから独立',
aiturkMode: 'AITURK:Codex の一覧にコンボを表示、すべてのモデルはブリッジ経由',
modeRestartNote: 'Codex を再起動してください。'
},
ar: {
modeTitle: 'وضع الاتصال',
directMode: 'مباشر (موصى به): نماذج الاشتراك مستقلة عن الجسر',
aiturkMode: 'AITURK: مجموعات النماذج في قائمة Codex، وجميع النماذج عبر الجسر',
modeRestartNote: 'أعد تشغيل Codex.'
},
ru: {
modeTitle: 'Режим подключения',
directMode: 'Напрямую (рекомендуется): модели подписки независимы от моста',
aiturkMode: 'AITURK: комбинации в списке Codex, все модели через мост',
modeRestartNote: 'Перезапустите Codex.'
}
}
export type CodexLinkCopy = typeof en
export function useCodexLinkCopy(): CodexLinkCopy {
const { locale } = useI18n()
return locale === 'tr' ? tr : en
if (locale === 'tr') return tr
if (locale === 'en') return en
return { ...en, ...modeTranslations[locale] }
}
+7 -1
View File
@@ -16,6 +16,12 @@
"rootDir": "..",
"outDir": "build/electron-types"
},
"include": ["electron", "../shared/src/data-url-read-max.ts", "../shared/src/translucency.ts"],
"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"
],
"exclude": ["src", "electron/**/*.e2e.mts"]
}
+41 -18
View File
@@ -65,7 +65,7 @@
},
"apps/desktop": {
"name": "aiturk-ide",
"version": "1.0.0-beta.28",
"version": "1.0.0-beta.38",
"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"
},
+46
View File
@@ -2550,6 +2550,39 @@ function Install-Repository {
Write-Success "Repository ready"
}
function Test-ReusableManagedVenv {
param([string]$VenvPath, [string]$BasePython, [string]$Version)
$pythonExe = Join-Path $VenvPath "Scripts\python.exe"
if (-not (Test-Path -LiteralPath $pythonExe -PathType Leaf)) { return $false }
$process = New-Object System.Diagnostics.Process
try {
$info = New-Object System.Diagnostics.ProcessStartInfo
$info.FileName = $pythonExe
$info.Arguments = '-I -c "import json,sys,ssl,ctypes,venv,encodings; print(json.dumps(dict(version=str(sys.version_info.major)+''.''+str(sys.version_info.minor),prefix=sys.prefix,base=sys._base_executable)))"'
$info.UseShellExecute = $false
$info.CreateNoWindow = $true
$info.RedirectStandardOutput = $true
$info.RedirectStandardError = $true
$process.StartInfo = $info
if (-not $process.Start()) { return $false }
$stdout = $process.StandardOutput.ReadToEndAsync()
$stderr = $process.StandardError.ReadToEndAsync()
if (-not $process.WaitForExit(15000)) {
try { $process.Kill() } catch { }
return $false
}
if ($process.ExitCode -ne 0) { return $false }
$health = $stdout.Result | ConvertFrom-Json
return ($health.version -eq $Version -and
[string]::Equals([IO.Path]::GetFullPath($health.prefix), [IO.Path]::GetFullPath($VenvPath), [StringComparison]::OrdinalIgnoreCase) -and
[string]::Equals([IO.Path]::GetFullPath($health.base), [IO.Path]::GetFullPath($BasePython), [StringComparison]::OrdinalIgnoreCase))
} catch {
return $false
} finally {
$process.Dispose()
}
}
function Install-Venv {
if ($NoVenv) {
Write-Info "Skipping virtual environment (-NoVenv)"
@@ -2567,6 +2600,19 @@ function Install-Venv {
throw "Hermes-managed Python is unavailable. Run install.ps1 -Stage python first."
}
# Routine source updates do not require replacing a healthy interpreter.
# Recreating it kills detached Codex bridges sharing this venv and leaves
# every subscription model disconnected until bootstrap finishes. Verify
# version, provenance and stdlib first; the dependency stage still performs
# the locked sync and import checks. Broken/mismatched runtimes retain the
# transactional repair path below.
$existingVenvPath = Join-Path $InstallDir "venv"
if (Test-ReusableManagedVenv -VenvPath $existingVenvPath -BasePython $resolvedPython.Path -Version $resolvedPython.Version) {
$env:UV_PYTHON = Join-Path $existingVenvPath "Scripts\python.exe"
Write-Success "Reusing healthy managed virtual environment; running Codex bridges preserved"
return
}
Write-Info "Creating virtual environment with Python $($resolvedPython.Version)..."
Push-Location $InstallDir
@@ -0,0 +1,77 @@
"""Run the actual installer functions against a real disposable Python venv."""
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
SOURCE = Path(__file__).resolve().parents[1] / 'scripts/install.ps1'
PS = shutil.which('powershell') or shutil.which('pwsh')
pytestmark = pytest.mark.skipif(os.name != 'nt' or not PS, reason='Windows installer runtime test')
def function(source, name):
start = source.index('function ' + name + ' {')
opening = source.index('{', start)
depth = 0
for index in range(opening, len(source)):
if source[index] == '{': depth += 1
elif source[index] == '}': depth -= 1
if depth == 0: return source[start:index + 1]
raise AssertionError(name)
@pytest.fixture(scope='module')
def runtime(tmp_path_factory):
root = tmp_path_factory.mktemp('installer-reuse')
subprocess.run([sys.executable, '-m', 'venv', '--without-pip', str(root / 'venv')], check=True)
return root
def run(runtime, body):
text = SOURCE.read_text(encoding='utf-8-sig')
script = runtime / 'probe.ps1'
prefix = "$ErrorActionPreference = 'Stop'\n"
prefix += function(text, 'Test-ReusableManagedVenv') + '\n'
prefix += function(text, 'Install-Venv') + '\n'
prefix += "$InstallDir = '" + str(runtime).replace("'", "''") + "'\n"
prefix += "$BasePython = '" + sys._base_executable.replace("'", "''") + "'\n"
prefix += "$Version = '" + str(sys.version_info.major) + '.' + str(sys.version_info.minor) + "'\n"
script.write_text(prefix + body, encoding='utf-8-sig')
result = subprocess.run([PS, '-NoProfile', '-File', str(script)], capture_output=True, text=True, timeout=40)
assert result.returncode == 0, result.stdout + result.stderr
return result.stdout
def test_healthy_update_keeps_live_python_and_never_reaches_kill_sweep(runtime):
python = runtime / 'venv/Scripts/python.exe'
live = subprocess.Popen([str(python), '-c', 'import time;time.sleep(90)'])
try:
output = run(runtime, r'''
function Resolve-AvailablePythonVersion { return @{Path=$BasePython;Version=$Version} }
function Write-Success { param($Message); Write-Output $Message }
function Write-Info { throw 'Unexpected venv recreation/kill path' }
function taskkill { throw 'Live process must not be killed' }
Install-Venv
''')
assert 'Reusing healthy' in output
assert live.poll() is None
assert python.is_file()
finally:
live.terminate();live.wait(timeout=10)
def test_wrong_version_or_foreign_interpreter_does_not_reuse(runtime):
output = run(runtime, r'''
$venvPath = Join-Path $InstallDir 'venv'
$results = @(
(Test-ReusableManagedVenv $venvPath $BasePython '0.0'),
(Test-ReusableManagedVenv $venvPath (Join-Path $InstallDir 'other-python.exe') $Version),
(Test-ReusableManagedVenv (Join-Path $InstallDir 'missing') $BasePython $Version)
)
ConvertTo-Json -Compress -InputObject $results
''')
assert json.loads(output) == [False, False, False]
@@ -20,6 +20,7 @@ import subprocess
import pytest
from tools.file_operations import ShellFileOperations
from tools.environments.local import LocalEnvironment
# Big enough to prove containment, small enough to keep the test fast.
GIANT = 5 * 1024 * 1024 # 5MB single line
@@ -27,22 +28,20 @@ GIANT = 5 * 1024 * 1024 # 5MB single line
STDOUT_CEILING = 1 * 1024 * 1024
class RecordingEnv:
class RecordingEnv(LocalEnvironment):
"""Local bash executor that records the largest stdout it returned."""
def __init__(self, cwd):
self.cwd = cwd
super().__init__(cwd=cwd)
self.max_stdout = 0
def execute(self, command, timeout=60, **kwargs):
proc = subprocess.run(
["bash", "-c", command],
capture_output=True, text=True, errors="replace",
timeout=timeout + 30,
)
out = proc.stdout + (proc.stderr or "")
# Use the application's shell selection/path handling. On Windows a
# bare 'bash' may select WSL, whose filesystem is not the native one.
result = super().execute(command, timeout=timeout + 30, **kwargs)
out = result.get("output", "")
self.max_stdout = max(self.max_stdout, len(out))
return {"output": out, "returncode": proc.returncode}
return result
@pytest.fixture()
@@ -58,6 +57,8 @@ def _ops(giant_dir, engine):
env = RecordingEnv(str(giant_dir))
ops = ShellFileOperations(env)
ops._has_command = lambda cmd: cmd == engine
resolve = ops._resolve_command
ops._resolve_command = lambda cmd: None if cmd == "rg" and engine != "rg" else resolve(cmd)
return ops, env
@@ -96,5 +97,5 @@ def test_line_cap_skipped_for_path_and_count_modes(giant_dir, engine, output_mod
if output_mode == "files_only":
assert {os.path.basename(f) for f in result.files} == {"trace.json", "small.py"}
else:
assert {os.path.basename(k) for k in result.counts} == {"trace.json", "small.py"}
assert {os.path.basename(k) for k, count in result.counts.items() if count} == {"trace.json", "small.py"}
assert env.max_stdout < STDOUT_CEILING
+112
View File
@@ -0,0 +1,112 @@
"""Project scope and resource limits: real local paths, no broad subprocesses."""
from unittest.mock import Mock
from tools.file_operations import ShellFileOperations, SearchResult, ExecuteResult
from tools.environments.local import LocalEnvironment
def projects(tmp_path):
for name in ('one', 'two'):
project = tmp_path / name
project.mkdir()
(project / '.git').mkdir()
(project / 'source.py').write_text('needle\n')
return tmp_path / 'one'
def test_ambiguous_scope_starts_no_process(tmp_path):
projects(tmp_path)
ops = ShellFileOperations(LocalEnvironment(str(tmp_path)))
ops._exec = Mock(side_effect=AssertionError('must not start process'))
result = ops.search('needle', str(tmp_path))
assert 'scope' in result.error.lower()
assert 'one' in result.error and 'two' in result.error
ops._exec.assert_not_called()
def test_umbrella_uses_cwd_project_and_reports_it(tmp_path):
project = projects(tmp_path)
ops = ShellFileOperations(LocalEnvironment(str(project)))
ops._exec = Mock(return_value=ExecuteResult(stdout='exists', exit_code=0))
ops._search_content = Mock(return_value=SearchResult())
result = ops.search('needle', str(tmp_path))
assert ops._search_content.call_args.args[1] == str(project)
assert str(project) in result.warning
def test_broad_opt_in_and_explicit_file(tmp_path):
project = projects(tmp_path)
ops = ShellFileOperations(LocalEnvironment(str(project)))
ops._exec = Mock(return_value=ExecuteResult(stdout='exists', exit_code=0))
ops._search_content = Mock(return_value=SearchResult())
for root, broad in ((tmp_path, True), (project / 'source.py', False)):
assert not ops.search('needle', str(root), allow_broad=broad).error
assert ops._search_content.call_args.args[1] == str(root)
def test_rg_commands_bound_threads_and_prune(tmp_path):
project = projects(tmp_path)
ops = ShellFileOperations(LocalEnvironment(str(project)))
ops._exec = Mock(return_value=ExecuteResult(stdout='', exit_code=1))
ops._search_with_rg('needle', str(project), '*.py', 50, 0, 'content', 0, rg_executable='rg')
command = ops._exec.call_args.args[0]
assert '--threads 2' in command
assert 'node_modules' in command and '.git' in command
assert '*.py' in command
ops._search_files_rg('*.py', str(project), 50, 0, rg_executable='rg')
command = ops._exec.call_args.args[0]
assert '--threads 2' in command and 'node_modules' in command
def test_concurrent_searches_have_two_slots_and_release_after_failure(tmp_path):
import threading
from concurrent.futures import ThreadPoolExecutor
project = projects(tmp_path)
lock = threading.Lock()
release = threading.Event()
entered = threading.Event()
active = peak = 0
def run_search(*args):
nonlocal active, peak
with lock:
active += 1
peak = max(peak, active)
if active == 2:
entered.set()
try:
assert release.wait(5)
return SearchResult()
finally:
with lock:
active -= 1
with ThreadPoolExecutor(max_workers=6) as pool:
pending = []
for _ in range(6):
ops = ShellFileOperations(LocalEnvironment(str(project)))
ops._search_scoped = run_search
pending.append(pool.submit(ops.search, 'needle'))
try:
assert entered.wait(3)
assert peak == 2
finally:
release.set()
assert all(not task.result().error for task in pending)
assert peak == 2
def test_real_rg_project_content_and_nested_glob(tmp_path):
project = projects(tmp_path)
(project / 'src').mkdir()
(project / 'src' / 'nested.py').write_text('needle\n')
(project / 'node_modules').mkdir()
(project / 'node_modules' / 'noise.py').write_text('needle\n')
ops = ShellFileOperations(LocalEnvironment(str(project)))
result = ops.search('needle', str(tmp_path), file_glob='*.py')
assert not result.error
assert len(result.matches) == 2
assert all('node_modules' not in item.path for item in result.matches)
result = ops.search('src/*.py', str(project), target='files')
assert not result.error
assert len(result.files) == 1 and 'nested.py' in result.files[0]
+56 -12
View File
@@ -50,7 +50,8 @@ from agent.file_safety import (
get_write_denied_error,
is_write_denied as _shared_is_write_denied,
)
from agent.search_policy import SEARCH_PRUNE_DIR_NAMES
from agent.search_policy import (SEARCH_PRUNE_DIR_NAMES, SEARCH_RG_THREADS,
SEARCH_MAX_CONCURRENT, local_search_scope)
from tools import interrupt as tool_interrupt
logger = logging.getLogger(__name__)
@@ -73,6 +74,8 @@ _MACOS_TCC_PROTECTED_HOME_DIRS = (
)
_SEARCH_SLOTS = threading.BoundedSemaphore(SEARCH_MAX_CONCURRENT)
_FILENAME_SEARCH_ADMISSION = threading.Condition()
_ACTIVE_FILENAME_SEARCH_ROOTS: set[tuple[str, str, str]] = set()
_FILENAME_SEARCH_WAIT_SECONDS = 0.05
@@ -3432,7 +3435,31 @@ class ShellFileOperations(FileOperations):
def search(self, pattern: str, path: str = ".", target: str = "content",
file_glob: Optional[str] = None, limit: int = 50, offset: int = 0,
output_mode: str = "content", context: int = 0,
order: str = "discovery") -> SearchResult:
order: str = "discovery", allow_broad: bool = False) -> SearchResult:
# One admission slot covers discovery, traversal and zero-match probes.
# It is process-wide across file-ops instances, and interruptible.
while not _SEARCH_SLOTS.acquire(timeout=0.05):
if tool_interrupt.is_interrupted():
return SearchResult(error="Search interrupted while waiting for a search slot.")
try:
if tool_interrupt.is_interrupted():
return SearchResult(error="Search interrupted.")
return self._search_scoped(pattern, path, target, file_glob, limit,
offset, output_mode, context, order, allow_broad)
finally:
_SEARCH_SLOTS.release()
def _project_search_scope(self, path, allow_broad=False):
from tools.environments.local import LocalEnvironment, _msys_to_windows_path
if allow_broad or not isinstance(self.env, LocalEnvironment):
return path, None, None
cwd = getattr(self.env, "cwd", None) or self.cwd
return local_search_scope(_msys_to_windows_path(path), cwd, _HOME)
def _search_scoped(self, pattern: str, path: str = ".", target: str = "content",
file_glob: Optional[str] = None, limit: int = 50, offset: int = 0,
output_mode: str = "content", context: int = 0,
order: str = "discovery", allow_broad: bool = False) -> SearchResult:
"""
Search for content or files.
@@ -3460,6 +3487,9 @@ class ShellFileOperations(FileOperations):
# Expand ~ and other shell paths
path = self._expand_path(path)
path, scope_notice, scope_error = self._project_search_scope(path, allow_broad)
if scope_error:
return SearchResult(error=scope_error)
# Validate that the path exists before searching
check = self._exec(f"test -e {self._escape_shell_arg(path)} && echo exists || echo not_found")
@@ -3470,7 +3500,7 @@ class ShellFileOperations(FileOperations):
# merge the results, and report the skipped parts.
multi = self._try_multi_path_search(
pattern, path, target, file_glob, limit, offset, output_mode, context,
order,
order, allow_broad,
)
if multi is not None:
return multi
@@ -3518,6 +3548,8 @@ class ShellFileOperations(FileOperations):
f"an unattended privacy prompt: {skipped}. Search a protected "
"folder directly when access is intentional."
)
if scope_notice:
result.warning = " ".join(filter(None, [scope_notice, result.warning]))
return result
def _macos_search_exclusions(self, path: str) -> List[str]:
@@ -3587,7 +3619,7 @@ class ShellFileOperations(FileOperations):
def _try_multi_path_search(self, pattern: str, path: str, target: str,
file_glob: Optional[str], limit: int, offset: int,
output_mode: str, context: int,
order: str = "discovery") -> Optional[SearchResult]:
order: str = "discovery", allow_broad: bool = False) -> Optional[SearchResult]:
"""Recover a not-found ``path`` that is really several paths in one string.
Production trajectories show models passing "dir1 dir2 dir3" (or
@@ -3604,9 +3636,14 @@ class ShellFileOperations(FileOperations):
parts = path.split()
if len(parts) < 2:
return None
existing, missing = [], []
existing, missing, scope_notices = [], [], []
for p in parts:
expanded = self._expand_path(p)
expanded, notice, error = self._project_search_scope(expanded, allow_broad)
if error:
return SearchResult(error=error)
if notice:
scope_notices.append(notice)
chk = self._exec(
f"test -e {self._escape_shell_arg(expanded)} && echo exists || echo not_found"
)
@@ -3641,7 +3678,7 @@ class ShellFileOperations(FileOperations):
note += "; skipped missing: " + ", ".join(missing[:3])
if len(missing) > 3:
note += f" (+{len(missing) - 3} more)"
warning_parts = [note]
warning_parts = [note, *scope_notices]
if not merged.error:
protected_paths = [
absolute
@@ -3704,7 +3741,7 @@ class ShellFileOperations(FileOperations):
glob_expr = f" --glob {self._escape_shell_arg(file_glob)}" if file_glob else ""
probe = self._exec(
f"{rg} -i --count-matches{glob_expr} "
f"{rg} --threads {SEARCH_RG_THREADS} -i --count-matches{glob_expr} {self._search_prune_glob_args()} "
f"{self._escape_shell_arg(pattern)} {self._escape_native_tool_arg(path)} "
f"2>/dev/null | head -50",
timeout=30,
@@ -3723,7 +3760,7 @@ class ShellFileOperations(FileOperations):
# files remain diagnosable, but prune heavyweight trees before rg can
# recurse into them.
hidden = self._exec(
f"{rg} --hidden --no-ignore --count-matches{glob_expr}"
f"{rg} --threads {SEARCH_RG_THREADS} --hidden --no-ignore --count-matches{glob_expr}"
f" {self._search_prune_glob_args()} "
f"{self._escape_shell_arg(pattern)} {self._escape_native_tool_arg(path)} "
f"2>/dev/null | head -50",
@@ -3738,7 +3775,7 @@ class ShellFileOperations(FileOperations):
)
if re.search(r"[.\[\](){}?*+^$\\|]", pattern):
fixed = self._exec(
f"{rg} -F --count-matches{glob_expr} "
f"{rg} --threads {SEARCH_RG_THREADS} -F --count-matches{glob_expr} {self._search_prune_glob_args()} "
f"{self._escape_shell_arg(pattern)} {self._escape_native_tool_arg(path)} "
f"2>/dev/null | head -50",
timeout=30,
@@ -3813,7 +3850,7 @@ class ShellFileOperations(FileOperations):
))
try:
return self._search_files_rg(
search_pattern, path, limit, offset, order,
pattern, path, limit, offset, order,
rg_executable=rg_executable,
)
finally:
@@ -3848,6 +3885,9 @@ class ShellFileOperations(FileOperations):
hidden_prune = (
f" \\( -type d -name '.*'{root_exemptions} \\) -prune -o"
)
names = " -o ".join(f"-name {self._escape_shell_arg(name)}"
for name in sorted(SEARCH_PRUNE_DIR_NAMES))
hidden_prune += f" \\( -type d \\( {names} \\){root_exemptions} \\) -prune -o"
protected_paths = [
absolute
for _root, _relative, absolute
@@ -3986,7 +4026,7 @@ class ShellFileOperations(FileOperations):
f"cd {self._escape_shell_arg(scoped_common)} && " if scoped_common else ""
)
cmd = (
f"set -o pipefail; {cd_prefix}{rg} --files{sort_arg} -g {self._escape_shell_arg(glob_pattern)}"
f"set -o pipefail; {cd_prefix}{rg} --files{sort_arg} --threads {SEARCH_RG_THREADS} -g {self._escape_shell_arg(glob_pattern)} {self._search_prune_glob_args()}"
f"{exclusion_args} -- {root_args} 2>/dev/null | head -n {fetch_limit}"
)
result = self._exec(cmd, timeout=60)
@@ -4062,7 +4102,7 @@ class ShellFileOperations(FileOperations):
rg_executable = rg_executable or self._resolve_command("rg")
if not rg_executable:
return SearchResult(error="Content search requires ripgrep (rg).")
cmd_parts = [self._quote_executable(rg_executable), "--line-number", "--no-heading", "--with-filename"]
cmd_parts = [self._quote_executable(rg_executable), "--threads", str(SEARCH_RG_THREADS), "--line-number", "--no-heading", "--with-filename"]
# Giant-single-line containment (ported from cline/cline#13525): a
# match inside a serialized dump (multi-MB single-line JSON/minified
@@ -4100,6 +4140,8 @@ class ShellFileOperations(FileOperations):
if file_glob:
cmd_parts.extend(["--glob", self._escape_shell_arg(file_glob)])
cmd_parts.append(self._search_prune_glob_args())
# Output mode handling
if output_mode == "files_only":
cmd_parts.append("-l") # Files only
@@ -4230,6 +4272,8 @@ class ShellFileOperations(FileOperations):
# Exclude hidden directories (matching ripgrep's default behavior).
# This prevents searching inside .hub/index-cache/, .git/, etc.
cmd_parts.append("--exclude-dir='.*'")
cmd_parts.extend(f"--exclude-dir={self._escape_shell_arg(name)}"
for name in sorted(SEARCH_PRUNE_DIR_NAMES))
# Protected-dir pruning CANNOT use --exclude-dir here: grep matches
# exclude-dir globs against BASENAMES anywhere in the tree, so
+6 -2
View File
@@ -2653,7 +2653,7 @@ def search_tool(pattern: str, target: str = "content", path: str = ".",
file_glob: str = None, limit: int = 50, offset: int = 0,
output_mode: str = "content", context: int = 0,
order: str = "discovery",
task_id: str = "default") -> str:
task_id: str = "default", allow_broad: bool = False) -> str:
"""Search for content or files."""
try:
offset, limit = normalize_search_pagination(offset, limit)
@@ -2670,6 +2670,7 @@ def search_tool(pattern: str, target: str = "content", path: str = ".",
limit,
offset,
order,
allow_broad,
)
with _read_tracker_lock:
task_data = _read_tracker.setdefault(task_id, {
@@ -2717,6 +2718,7 @@ def search_tool(pattern: str, target: str = "content", path: str = ".",
pattern=pattern, path=path, target=target, file_glob=file_glob,
limit=limit, offset=offset, output_mode=output_mode, context=context,
order=order,
**({"allow_broad": True} if allow_broad else {}),
)
omitted = _filter_read_blocked_search_results(result, task_id)
if hasattr(result, 'matches'):
@@ -2912,6 +2914,7 @@ SEARCH_FILES_SCHEMA = {
"parameters": {
"type": "object",
"properties": {
"allow_broad": {"type": "boolean", "default": False, "description": "Deliberately search a home, drive or multi-project umbrella. Default scopes to the current project or asks for a scope. Does not remove pruning or CPU limits."},
"pattern": {"type": "string", "description": "Regex pattern for content search, or glob pattern (e.g., '*.py') for file search"},
"target": {"type": "string", "enum": ["content", "files"], "description": "'content' searches inside file contents, 'files' searches for files by name", "default": "content"},
"path": {"type": "string", "description": "Directory or file to search in (default: current working directory)", "default": "."},
@@ -2979,7 +2982,8 @@ def _handle_search_files(args, **kw):
pattern=args.get("pattern", ""), target=target, path=args.get("path", "."),
file_glob=args.get("file_glob"), limit=args.get("limit", 50), offset=args.get("offset", 0),
output_mode=args.get("output_mode", "content"), context=args.get("context", 0),
order=args.get("order", "discovery"), task_id=tid)
order=args.get("order", "discovery"), task_id=tid,
allow_broad=args.get("allow_broad") is True)
def _read_file_schema_overrides():