mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 08:01:56 +00:00
* fix(memory): bound session and lifecycle registries * fix(memory): bound transient filesystem registries * fix(memory): cap path and locale caches * fix(memory): bound runtime recovery registries * fix(memory): bound host mirror gap verdicts * fix(memory): bound shell startup env cache * fix(memory): bound gitlab host context cache * fix(memory): release removed ssh generations * fix(memory): expire cloud refresh replay guards * fix(memory): release retired plugin generations * fix(memory): bound plugin log key retention * fix(memory): bound automation authority generations * fix(memory): bound native chat enrichment cache * fix(memory): bound web session tracking generations * fix(memory): bound codex credential absence paths * fix(memory): bound WSL canonical path cache * fix(memory): bound sparse checkout cache * fix(memory): bound shared directory cache * fix(memory): bound advertised URL scan snapshots * fix(memory): bound automation manager cache * fix(memory): bound web session reorder intents * fix(memory): bound web session focus intents * fix(memory): bound web session handoffs * fix(memory): bound automation dispatch tokens * fix(memory): bound host mirror waiters * fix(memory): bound retained session activity * fix(memory): bound retained session activity * fix(memory): bound web session close intents * fix(memory): bound cloud session cache * fix(memory): bound WSL home cache * fix(memory): bound SSH capability cache * fix(memory): bound trust grant cooldowns * fix(memory): bound WSL auth drain state * fix(memory): bound Linear workspace credential cache * fix(memory): bound local Git capability cache * fix(memory): bound WSL Git environment cache * fix(memory): bound WSL Git environment cache * fix(memory): bound WSL preflight cache * fix(memory): keep hot cache entries warm * fix(memory): preserve generation fences across eviction * fix(memory): close remaining eviction fences * fix(memory): align evicted upstream generations * fix(memory): trim successful capability probes * fix(auth): retain expired refresh replay evidence --------- Co-authored-by: m4air <m4air@Mac.localdomain>
152 lines
5.1 KiB
TypeScript
152 lines
5.1 KiB
TypeScript
/**
|
|
* Optimistic capability probing with a bounded retry window and in-flight
|
|
* probe dedupe.
|
|
*
|
|
* Extracted from GitCapabilityCache so every host-capability cache in the tree
|
|
* gets the same three behaviors: probe once, remember only a positive absence
|
|
* signal, and let a concurrent caller wait on the probe already running rather
|
|
* than starting a duplicate one.
|
|
*/
|
|
export type CapabilityProbeOutcome = 'supported' | 'unsupported' | 'unknown'
|
|
|
|
export class CapabilityProbeCache<TCapability> {
|
|
private readonly retryAfterByCapability = new Map<TCapability, number>()
|
|
private readonly probesByCapability = new Map<TCapability, Promise<CapabilityProbeOutcome>>()
|
|
private readonly supportedCapabilities = new Set<TCapability>()
|
|
|
|
constructor(
|
|
private readonly retryIntervalMs: number,
|
|
private readonly maxEntries = Number.POSITIVE_INFINITY
|
|
) {}
|
|
|
|
shouldTry(capability: TCapability, nowMs = Date.now()): boolean {
|
|
const retryAfterMs = this.retryAfterByCapability.get(capability)
|
|
if (retryAfterMs === undefined) {
|
|
return true
|
|
}
|
|
if (nowMs < retryAfterMs) {
|
|
return false
|
|
}
|
|
this.retryAfterByCapability.delete(capability)
|
|
return true
|
|
}
|
|
|
|
isKnownSupported(capability: TCapability): boolean {
|
|
return this.supportedCapabilities.has(capability)
|
|
}
|
|
|
|
rememberSupported(capability: TCapability): void {
|
|
this.retryAfterByCapability.delete(capability)
|
|
this.supportedCapabilities.delete(capability)
|
|
this.supportedCapabilities.add(capability)
|
|
this.trimSettledEntries()
|
|
}
|
|
|
|
rememberUnsupported(capability: TCapability, nowMs = Date.now()): void {
|
|
// Why: optimistic probes preserve newer behavior, but repeating a known
|
|
// failure on every poll/search wastes subprocesses and trace space.
|
|
this.supportedCapabilities.delete(capability)
|
|
this.retryAfterByCapability.set(capability, nowMs + this.retryIntervalMs)
|
|
this.trimSettledEntries()
|
|
}
|
|
|
|
async runWithFallback<T>(
|
|
capability: TCapability,
|
|
runPreferred: () => Promise<T>,
|
|
runFallback: () => Promise<T>,
|
|
isUnsupportedError: (error: unknown) => boolean
|
|
): Promise<T> {
|
|
if (this.supportedCapabilities.has(capability)) {
|
|
// Why: supported commands are real work, not disposable probes. Let
|
|
// sibling repo/SSH calls retain their intended concurrency.
|
|
return this.runPreferredOrFallback(capability, runPreferred, runFallback, isUnsupportedError)
|
|
}
|
|
if (!this.shouldTry(capability)) {
|
|
return runFallback()
|
|
}
|
|
|
|
const inFlightProbe = this.probesByCapability.get(capability)
|
|
if (inFlightProbe) {
|
|
const outcome = await inFlightProbe
|
|
if (outcome === 'unsupported' || !this.shouldTry(capability)) {
|
|
return runFallback()
|
|
}
|
|
return this.runPreferredOrFallback(capability, runPreferred, runFallback, isUnsupportedError)
|
|
}
|
|
|
|
let settleProbe!: (outcome: CapabilityProbeOutcome) => void
|
|
const probe = new Promise<CapabilityProbeOutcome>((resolve) => {
|
|
settleProbe = resolve
|
|
})
|
|
this.probesByCapability.set(capability, probe)
|
|
try {
|
|
return await this.runPreferredOrFallback(
|
|
capability,
|
|
runPreferred,
|
|
runFallback,
|
|
isUnsupportedError,
|
|
settleProbe
|
|
)
|
|
} finally {
|
|
if (this.probesByCapability.get(capability) === probe) {
|
|
this.probesByCapability.delete(capability)
|
|
}
|
|
// Backstop: `isUnsupportedError` or `rememberUnsupported` can throw
|
|
// before the settle below them runs; waiters must not hang behind it.
|
|
settleProbe('unknown')
|
|
}
|
|
}
|
|
|
|
clear(): void {
|
|
this.retryAfterByCapability.clear()
|
|
this.probesByCapability.clear()
|
|
this.supportedCapabilities.clear()
|
|
}
|
|
|
|
private trimSettledEntries(): void {
|
|
while (this.supportedCapabilities.size > this.maxEntries) {
|
|
const oldest = this.supportedCapabilities.values().next()
|
|
if (oldest.done) {
|
|
break
|
|
}
|
|
this.supportedCapabilities.delete(oldest.value)
|
|
}
|
|
while (this.retryAfterByCapability.size > this.maxEntries) {
|
|
const oldest = this.retryAfterByCapability.keys().next()
|
|
if (oldest.done) {
|
|
break
|
|
}
|
|
this.retryAfterByCapability.delete(oldest.value)
|
|
}
|
|
}
|
|
|
|
private async runPreferredOrFallback<T>(
|
|
capability: TCapability,
|
|
runPreferred: () => Promise<T>,
|
|
runFallback: () => Promise<T>,
|
|
isUnsupportedError: (error: unknown) => boolean,
|
|
settleProbe?: (outcome: CapabilityProbeOutcome) => void
|
|
): Promise<T> {
|
|
try {
|
|
const result = await runPreferred()
|
|
// A preferred callback can detect a weaker positive signal (old Git's
|
|
// exit-zero option echo) and remember it as unsupported, so do not
|
|
// overwrite that stronger signal.
|
|
const outcome = this.retryAfterByCapability.has(capability) ? 'unsupported' : 'supported'
|
|
if (outcome === 'supported') {
|
|
this.rememberSupported(capability)
|
|
}
|
|
settleProbe?.(outcome)
|
|
return result
|
|
} catch (error) {
|
|
if (!isUnsupportedError(error)) {
|
|
settleProbe?.('unknown')
|
|
throw error
|
|
}
|
|
this.rememberUnsupported(capability)
|
|
settleProbe?.('unsupported')
|
|
return runFallback()
|
|
}
|
|
}
|
|
}
|