mirror of
https://github.com/Ryder-Sun/Meldwork.git
synced 2026-11-07 06:56:03 -10:00
Prevent stale concurrent capability cache updates
Keep older probe completions from replacing the result of a newer detection. Verified CLI discovery tests and git diff --check.
This commit is contained in:
1 parent
a624f1dc29
commit
a535e5942a
2 files changed
+108
-79
No files matched your search
@@ -39,6 +39,7 @@ const ALLOWED_KINDS = Object.keys(AGENT_PROFILES)
|
||||
const DEFAULT_WINDOWS_PATHEXT = ['.COM', '.EXE', '.BAT', '.CMD']
|
||||
const CAPABILITY_CACHE_TTL_MS = 5 * 60 * 1000
|
||||
const capabilityCache = new Map()
|
||||
const activeCapabilityProbes = new Map()
|
||||
const capabilityEnvironmentKey = randomBytes(32)
|
||||
const SYSTEM_CHILD_ENV_KEYS = Object.freeze([
|
||||
'HOME', 'USER', 'LOGNAME', 'SHELL',
|
||||
@@ -442,89 +443,96 @@ async function probeAgentCapabilities(kind, executable, options = {}) {
|
||||
])
|
||||
} catch { /* caching requires a verifiable executable identity */ }
|
||||
const cacheKey = `${kind}:${executable}`
|
||||
const prior = capabilityCache.get(cacheKey)
|
||||
const now = Date.now()
|
||||
const recentCompatible = identity && prior?.identity === identity
|
||||
&& now >= prior.checkedAt && now - prior.checkedAt < CAPABILITY_CACHE_TTL_MS
|
||||
const results = await Promise.all(capabilityProbes(kind).map(async (probe) => {
|
||||
for (const timeout of probe.timeout ? [probe.timeout] : [8000, 16000]) {
|
||||
try {
|
||||
const command = prepareCommandFn(executable, probe.args, options)
|
||||
const execution = execFileFn(command.command, command.args, {
|
||||
timeout,
|
||||
windowsHide: true,
|
||||
env: childEnv,
|
||||
})
|
||||
if (probe.id === 'pi-json') execution.child?.stdin?.end()
|
||||
const result = await execution
|
||||
const output = `${result.stdout || ''}\n${result.stderr || ''}`
|
||||
const missingCapabilities = probe.requiredText.some(value => !output.includes(value))
|
||||
|| (probe.validateOutput && !probe.validateOutput(output))
|
||||
return { probe, output, authRequired: missingCapabilities && authFailureCertainty(output) === 'definitive' }
|
||||
} catch (error) {
|
||||
const timedOut = error?.killed === true || error?.code === 'ETIMEDOUT'
|
||||
if (timedOut && timeout === 8000) continue
|
||||
const diagnostic = `${error?.stdout || ''}\n${error?.stderr || ''}\n${error?.message || ''}`
|
||||
return { probe, error: true, timedOut, systemError: typeof error?.code === 'string' && !timedOut,
|
||||
authRequired: authFailureCertainty(diagnostic) === 'definitive' }
|
||||
const probeToken = Symbol('capability-probe')
|
||||
activeCapabilityProbes.set(cacheKey, probeToken)
|
||||
try {
|
||||
const prior = capabilityCache.get(cacheKey)
|
||||
const now = Date.now()
|
||||
const recentCompatible = identity && prior?.identity === identity
|
||||
&& now >= prior.checkedAt && now - prior.checkedAt < CAPABILITY_CACHE_TTL_MS
|
||||
const results = await Promise.all(capabilityProbes(kind).map(async (probe) => {
|
||||
for (const timeout of probe.timeout ? [probe.timeout] : [8000, 16000]) {
|
||||
try {
|
||||
const command = prepareCommandFn(executable, probe.args, options)
|
||||
const execution = execFileFn(command.command, command.args, {
|
||||
timeout,
|
||||
windowsHide: true,
|
||||
env: childEnv,
|
||||
})
|
||||
if (probe.id === 'pi-json') execution.child?.stdin?.end()
|
||||
const result = await execution
|
||||
const output = `${result.stdout || ''}\n${result.stderr || ''}`
|
||||
const missingCapabilities = probe.requiredText.some(value => !output.includes(value))
|
||||
|| (probe.validateOutput && !probe.validateOutput(output))
|
||||
return { probe, output, authRequired: missingCapabilities && authFailureCertainty(output) === 'definitive' }
|
||||
} catch (error) {
|
||||
const timedOut = error?.killed === true || error?.code === 'ETIMEDOUT'
|
||||
if (timedOut && timeout === 8000) continue
|
||||
const diagnostic = `${error?.stdout || ''}\n${error?.stderr || ''}\n${error?.message || ''}`
|
||||
return { probe, error: true, timedOut, systemError: typeof error?.code === 'string' && !timedOut,
|
||||
authRequired: authFailureCertainty(diagnostic) === 'definitive' }
|
||||
}
|
||||
}
|
||||
}))
|
||||
const failed = results.find(result => !result.timedOut && !result.systemError && !result.authRequired && (result.error
|
||||
|| result.probe.requiredText.some(value => !result.output.includes(value))
|
||||
|| (result.probe.validateOutput && !result.probe.validateOutput(result.output))))
|
||||
if (activeCapabilityProbes.get(cacheKey) === probeToken
|
||||
&& (failed || results.some(result => result.systemError || result.authRequired))) capabilityCache.delete(cacheKey)
|
||||
for (const result of results) {
|
||||
const { probe } = result
|
||||
if (result.authRequired && !failed) {
|
||||
return {
|
||||
compatibilityState: 'unknown',
|
||||
incompatibilityReason: 'LOCAL_AGENT_AUTH_REQUIRED',
|
||||
incompatibilityProbe: probe.id,
|
||||
}
|
||||
}
|
||||
if (result.authRequired) continue
|
||||
if (result.timedOut && !failed) {
|
||||
if (recentCompatible) continue
|
||||
return {
|
||||
compatibilityState: 'unknown',
|
||||
incompatibilityReason: 'LOCAL_AGENT_CAPABILITY_PROBE_TIMEOUT',
|
||||
incompatibilityProbe: probe.id,
|
||||
}
|
||||
}
|
||||
if (result.timedOut) continue
|
||||
if (result.systemError && !failed) {
|
||||
return { compatibilityState: 'unknown', incompatibilityReason: 'LOCAL_AGENT_CAPABILITY_PROBE_FAILED',
|
||||
incompatibilityProbe: probe.id }
|
||||
}
|
||||
if (result.systemError) continue
|
||||
if (result.error) {
|
||||
return {
|
||||
compatibilityState: 'incompatible',
|
||||
incompatibilityReason: probe.id.endsWith('-acp')
|
||||
? 'LOCAL_AGENT_PROTOCOL_UNAVAILABLE'
|
||||
: 'LOCAL_AGENT_REQUIRED_CAPABILITY_MISSING',
|
||||
incompatibilityProbe: probe.id,
|
||||
}
|
||||
}
|
||||
if (probe.requiredText.some(value => !result.output.includes(value))
|
||||
|| (probe.validateOutput && !probe.validateOutput(result.output))) {
|
||||
return {
|
||||
compatibilityState: 'incompatible',
|
||||
incompatibilityReason: 'LOCAL_AGENT_REQUIRED_CAPABILITY_MISSING',
|
||||
incompatibilityProbe: probe.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
const failed = results.find(result => !result.timedOut && !result.systemError && !result.authRequired && (result.error
|
||||
|| result.probe.requiredText.some(value => !result.output.includes(value))
|
||||
|| (result.probe.validateOutput && !result.probe.validateOutput(result.output))))
|
||||
if (failed || results.some(result => result.systemError || result.authRequired)) capabilityCache.delete(cacheKey)
|
||||
for (const result of results) {
|
||||
const { probe } = result
|
||||
if (result.authRequired && !failed) {
|
||||
return {
|
||||
compatibilityState: 'unknown',
|
||||
incompatibilityReason: 'LOCAL_AGENT_AUTH_REQUIRED',
|
||||
incompatibilityProbe: probe.id,
|
||||
}
|
||||
if (activeCapabilityProbes.get(cacheKey) === probeToken && identity && !results.some(result => result.timedOut)) {
|
||||
capabilityCache.set(cacheKey, { identity, checkedAt: Date.now() })
|
||||
}
|
||||
if (result.authRequired) continue
|
||||
if (result.timedOut && !failed) {
|
||||
if (recentCompatible) continue
|
||||
return {
|
||||
compatibilityState: 'unknown',
|
||||
incompatibilityReason: 'LOCAL_AGENT_CAPABILITY_PROBE_TIMEOUT',
|
||||
incompatibilityProbe: probe.id,
|
||||
}
|
||||
return {
|
||||
compatibilityState: 'compatible',
|
||||
incompatibilityReason: '',
|
||||
incompatibilityProbe: '',
|
||||
...(kind === 'hermes' && results.some(result => result.output?.includes('--format')
|
||||
&& result.output.includes('stream-json')) ? { streamJsonAvailable: true } : {}),
|
||||
}
|
||||
if (result.timedOut) continue
|
||||
if (result.systemError && !failed) {
|
||||
return { compatibilityState: 'unknown', incompatibilityReason: 'LOCAL_AGENT_CAPABILITY_PROBE_FAILED',
|
||||
incompatibilityProbe: probe.id }
|
||||
}
|
||||
if (result.systemError) continue
|
||||
if (result.error) {
|
||||
return {
|
||||
compatibilityState: 'incompatible',
|
||||
incompatibilityReason: probe.id.endsWith('-acp')
|
||||
? 'LOCAL_AGENT_PROTOCOL_UNAVAILABLE'
|
||||
: 'LOCAL_AGENT_REQUIRED_CAPABILITY_MISSING',
|
||||
incompatibilityProbe: probe.id,
|
||||
}
|
||||
}
|
||||
if (probe.requiredText.some(value => !result.output.includes(value))
|
||||
|| (probe.validateOutput && !probe.validateOutput(result.output))) {
|
||||
return {
|
||||
compatibilityState: 'incompatible',
|
||||
incompatibilityReason: 'LOCAL_AGENT_REQUIRED_CAPABILITY_MISSING',
|
||||
incompatibilityProbe: probe.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
if (identity && !results.some(result => result.timedOut)) {
|
||||
capabilityCache.set(cacheKey, { identity, checkedAt: Date.now() })
|
||||
}
|
||||
return {
|
||||
compatibilityState: 'compatible',
|
||||
incompatibilityReason: '',
|
||||
incompatibilityProbe: '',
|
||||
...(kind === 'hermes' && results.some(result => result.output?.includes('--format')
|
||||
&& result.output.includes('stream-json')) ? { streamJsonAvailable: true } : {}),
|
||||
} finally {
|
||||
if (activeCapabilityProbes.get(cacheKey) === probeToken) activeCapabilityProbes.delete(cacheKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -235,6 +235,27 @@ test('spawn and authentication failures invalidate a previously successful capab
|
||||
assert.equal((await probe()).compatibilityState, 'unknown')
|
||||
})
|
||||
|
||||
test('a late successful probe cannot restore a cache after a newer spawn failure', async () => {
|
||||
let release
|
||||
const pending = new Promise(resolve => { release = resolve })
|
||||
const options = { env: {}, version: '2.115.0',
|
||||
statFn: () => ({ dev: 1, ino: 1, size: 100, mtimeMs: 1, ctimeMs: 1 }) }
|
||||
const executable = '/test/parallel-probe-cache'
|
||||
const oldProbe = probeAgentCapabilities('workbuddy', executable, { ...options,
|
||||
execFileFn: async () => { await pending; return { stdout: '--print --output-format --permission-mode --max-turns --resume json plan acceptEdits' } },
|
||||
})
|
||||
const newer = await probeAgentCapabilities('workbuddy', executable, { ...options,
|
||||
execFileFn: async () => { throw Object.assign(new Error('permission denied'), { code: 'EACCES' }) },
|
||||
})
|
||||
assert.equal(newer.compatibilityState, 'unknown')
|
||||
release()
|
||||
assert.equal((await oldProbe).compatibilityState, 'compatible')
|
||||
const timeout = await probeAgentCapabilities('workbuddy', executable, { ...options,
|
||||
execFileFn: async () => { throw Object.assign(new Error('timeout'), { code: 'ETIMEDOUT' }) },
|
||||
})
|
||||
assert.equal(timeout.compatibilityState, 'unknown')
|
||||
})
|
||||
|
||||
test('capability probe cache expires when the system clock moves backwards', async t => {
|
||||
let now = 10_000
|
||||
t.mock.method(Date, 'now', () => now)
|
||||
|
||||
Reference in new issue
Block a user