fix(codex-bridge): close the gaps an independent review found
- 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:
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()
|
||||
|
||||
Reference in new issue
Block a user