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
@@ -23,7 +23,7 @@ export interface StagedTransitionSettings {
|
||||
thresholds: { zor: number; arastirma: number }
|
||||
targets: { basit: string; zor: string; arastirma: string }
|
||||
}
|
||||
finishCheck: { enabled: boolean; question: string; threshold: number; note: string; timeoutSeconds: number }
|
||||
finishCheck: { enabled: boolean; question: string; threshold: number; note: string }
|
||||
verifierCheck: { enabled: boolean; question: string; threshold: number }
|
||||
policy: { source: 'local' | 'dify'; endpoint: string; refreshMinutes: number }
|
||||
}
|
||||
|
||||
@@ -106,5 +106,12 @@ describe('staged transition settings', () => {
|
||||
expect(() => validateStagedTransition({ ...base, policy: { ...base.policy, endpoint: 'http://dify.example' } })).toThrow()
|
||||
expect(() => validateStagedTransition({ ...base, policy: { ...base.policy, source: 'web' } })).toThrow()
|
||||
expect(() => validateStagedTransition({ ...base, watchModels: ['a', 'a'] })).toThrow()
|
||||
for (const endpoint of ['https:dify.example/v1', 'https:/dify.example', 'https:///dify.example', 'ftp://dify.example']) {
|
||||
expect(() => validateStagedTransition({ ...base, policy: { ...base.policy, endpoint } })).toThrow()
|
||||
expect(() => validateStagedTransition({ ...base, endpoint })).toThrow()
|
||||
}
|
||||
expect(() =>
|
||||
validateStagedTransition({ ...base, prefilter: { ...base.prefilter, target: base.autoRoute.model } })
|
||||
).toThrow()
|
||||
})
|
||||
})
|
||||
@@ -53,13 +53,21 @@ const probability = (n: unknown): n is number => typeof n === 'number' && Number
|
||||
const integer = (n: unknown, min: number, max: number): n is number =>
|
||||
typeof n === 'number' && Number.isInteger(n) && n >= min && n <= max
|
||||
|
||||
// Same acceptance as the bridge (urllib.parse.urlsplit): the literal https:// prefix and a host. WHATWG URL alone
|
||||
// also accepts "https:host" and "https:/host", which the bridge rejects.
|
||||
function https(value: unknown): value is string {
|
||||
if (!text(value, 2048)) {
|
||||
if (!text(value, 2048) || !/^https:\/\/[^/?#\s]/i.test(value.trim())) {
|
||||
return false
|
||||
}
|
||||
const url = new URL(value)
|
||||
let url: URL
|
||||
|
||||
return url.protocol === 'https:' && !url.username && !url.password && !url.hash
|
||||
try {
|
||||
url = new URL(value.trim())
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
return url.protocol === 'https:' && Boolean(url.hostname) && !url.username && !url.password && !url.hash
|
||||
}
|
||||
|
||||
export function validateStagedTransition(input: unknown): StagedTransitionSettings {
|
||||
@@ -160,7 +168,7 @@ export function validateStagedTransition(input: unknown): StagedTransitionSettin
|
||||
throw new Error('Invalid automatic routing')
|
||||
}
|
||||
|
||||
if (Object.values(a.targets).some(t => t.trim() === a.model.trim())) {
|
||||
if (Object.values(a.targets).some(t => t.trim() === a.model.trim()) || p.target.trim() === a.model.trim()) {
|
||||
throw new Error('Automatic routing cannot target itself')
|
||||
}
|
||||
|
||||
@@ -170,8 +178,7 @@ export function validateStagedTransition(input: unknown): StagedTransitionSettin
|
||||
typeof f?.enabled !== 'boolean' ||
|
||||
!text(f.question) ||
|
||||
!probability(f.threshold) ||
|
||||
!text(f.note, 4000) ||
|
||||
!integer(f.timeoutSeconds, 1, 60)
|
||||
!text(f.note, 4000)
|
||||
) {
|
||||
throw new Error('Invalid finish check')
|
||||
}
|
||||
@@ -214,13 +221,7 @@ export function validateStagedTransition(input: unknown): StagedTransitionSettin
|
||||
thresholds: { zor: a.thresholds.zor, arastirma: a.thresholds.arastirma },
|
||||
targets: { basit: a.targets.basit.trim(), zor: a.targets.zor.trim(), arastirma: a.targets.arastirma.trim() }
|
||||
},
|
||||
finishCheck: {
|
||||
enabled: f.enabled,
|
||||
question: f.question,
|
||||
threshold: f.threshold,
|
||||
note: f.note,
|
||||
timeoutSeconds: f.timeoutSeconds
|
||||
},
|
||||
finishCheck: { enabled: f.enabled, question: f.question, threshold: f.threshold, note: f.note },
|
||||
verifierCheck: { enabled: c.enabled, question: c.question, threshold: c.threshold },
|
||||
policy: { source: pol.source, endpoint: pol.endpoint.trim(), refreshMinutes: pol.refreshMinutes }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user