Files
orca/src/shared/capability-probe-cache.ts
T
OrcaWinandm4air 4085e1cf60 fix(memory): release stale session registries (#21734)
* 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>
2026-09-20 14:41:50 -07:00

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()
}
}
}