diff --git a/src/resources/extensions/sf/auto-dispatch.ts b/src/resources/extensions/sf/auto-dispatch.ts index 066137234..d15c4dfa5 100644 --- a/src/resources/extensions/sf/auto-dispatch.ts +++ b/src/resources/extensions/sf/auto-dispatch.ts @@ -900,7 +900,7 @@ export const DISPATCH_RULES: DispatchRule[] = [ }, ]; -import { getRegistry } from "./rule-registry.js"; +import { getRegistry, hasRegistry } from "./rule-registry.js"; // ─── Resolver ───────────────────────────────────────────────────────────── @@ -915,13 +915,18 @@ import { getRegistry } from "./rule-registry.js"; export async function resolveDispatch( ctx: DispatchContext, ): Promise { - // Delegate to registry when available - try { - const registry = getRegistry(); - return await registry.evaluateDispatch(ctx); - } catch (err) { - // Registry not initialized — fall back to inline loop - logWarning("dispatch", `registry dispatch failed, falling back to inline rules: ${err instanceof Error ? err.message : String(err)}`); + // Delegate to registry when available. Callers that run outside auto-mode + // (e.g. `sf headless query`, `sf headless status`) never initialize the + // registry — falling through to inline rules is the intended behavior, + // not an error, so we silent-probe instead of warning on every call. + if (hasRegistry()) { + try { + return await getRegistry().evaluateDispatch(ctx); + } catch (err) { + // Genuine registry evaluation failure (rule threw, etc.) — log so we + // surface real bugs, then fall back. + logWarning("dispatch", `registry dispatch failed, falling back to inline rules: ${err instanceof Error ? err.message : String(err)}`); + } } for (const rule of DISPATCH_RULES) { diff --git a/src/resources/extensions/sf/rule-registry.ts b/src/resources/extensions/sf/rule-registry.ts index 74af0ad3d..3068c9393 100644 --- a/src/resources/extensions/sf/rule-registry.ts +++ b/src/resources/extensions/sf/rule-registry.ts @@ -561,6 +561,12 @@ export class RuleRegistry { let _registry: RuleRegistry | null = null; +/** True when the singleton registry has been initialized. Lets callers branch + * without paying for try/catch on a hot path. */ +export function hasRegistry(): boolean { + return _registry !== null; +} + /** Get the singleton registry. Throws if not initialized. */ export function getRegistry(): RuleRegistry { if (!_registry) {