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:
Ryder Sun committed 2026-11-07 23:48:42 +08:00
1 parent a624f1dc29
commit a535e5942a
2 files changed
+108 -79

No files matched your search

+87 -79
View File
@@ -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)