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

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

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

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

No files matched your search

@@ -87,11 +87,11 @@ def validate(settings):
and all(_text((a.get('questions') or {}).get(k)) and _prob((a.get('thresholds') or {}).get(k)) for k in ('zor', 'arastirma'))
and all(_text((a.get('targets') or {}).get(k), 256) for k in ('basit', 'zor', 'arastirma'))):
raise ValueError('Invalid automatic routing')
if a['model'] in a['targets'].values():
if a['model'] in a['targets'].values() or p['target'] == a['model']:
raise ValueError('Automatic routing cannot target itself')
f = settings['finishCheck']
if not (isinstance(f.get('enabled'), bool) and _text(f.get('question')) and _prob(f.get('threshold'))
and _text(f.get('note'), 4000) and _int(f.get('timeoutSeconds'), 1, 60)):
and _text(f.get('note'), 4000)):
raise ValueError('Invalid finish check')
v = settings['verifierCheck']
if not (isinstance(v.get('enabled'), bool) and _text(v.get('question')) and _prob(v.get('threshold'))):
@@ -186,15 +186,27 @@ def write_policy(result):
record['lastGoodAt'] = previous.get('lastGoodAt') or previous.get('fetchedAt')
elif result.get('ok'):
record['lastGoodAt'] = record['fetchedAt']
tmp = path.with_name(path.name + f'.{os.getpid()}.tmp')
tmp = path.with_name(path.name + f'.{os.getpid()}.{threading.get_ident()}.tmp')
tmp.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding='utf-8')
os.replace(tmp, path)
try:
for deneme in range(5):
try:
os.replace(tmp, path) # Windows: a reader holding the file makes this fail briefly
break
except PermissionError:
if deneme == 4:
raise
time.sleep(0.05 * (deneme + 1))
finally:
if tmp.exists():
tmp.unlink()
return record
def acquire(settings):
def acquire(settings, wait=None):
"""wait: at most this many seconds in the queue (turn-start questions must not hold a request)."""
global _active
deadline = time.monotonic() + settings['queueWaitSeconds']
deadline = time.monotonic() + (min(settings['queueWaitSeconds'], wait) if wait else settings['queueWaitSeconds'])
with _condition:
while settings['maxConcurrent'] and _active >= settings['maxConcurrent']:
remaining = deadline - time.monotonic()