@harness-engineering/orchestrator 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -5,7 +5,8 @@ import { EnrichedSpec, ComplexityScore, SimulationResult, IntelligencePipeline,
5
5
  import { GraphStore } from '@harness-engineering/graph';
6
6
  import { execFile } from 'node:child_process';
7
7
  import { EventEmitter } from 'node:events';
8
- import { PoolStateProvider, SchedulerTimerHandle } from '@harness-engineering/local-models';
8
+ import { PoolStateProvider, SchedulerTimerHandle, DiscoverCandidatesOptions, DiscoverCandidatesResult } from '@harness-engineering/local-models';
9
+ export { discoverCandidates } from '@harness-engineering/local-models';
9
10
  import { z } from 'zod';
10
11
 
11
12
  /**
@@ -1698,6 +1699,13 @@ declare class Orchestrator extends EventEmitter {
1698
1699
  * so this map is the single source of truth post-migration.
1699
1700
  */
1700
1701
  private localResolvers;
1702
+ /**
1703
+ * Consumption Phase 1 (T2): bus listener that debounce-refreshes every local
1704
+ * resolver when a `local-models:pool` mutation fires, so a just-installed or
1705
+ * swapped model becomes usable within the refresh window instead of waiting up
1706
+ * to `probeIntervalMs` for the next poll. Held for removal in {@link stop}.
1707
+ */
1708
+ private poolRefreshListener;
1701
1709
  /** Phase 4 (D5): pool-state port shared by all local/pi resolvers. Null when LMLM disabled. */
1702
1710
  private poolStateProvider;
1703
1711
  private poolStateStore;
@@ -1735,6 +1743,10 @@ declare class Orchestrator extends EventEmitter {
1735
1743
  * see the Phase 2 candidate-parser gap noted on `startRefreshScheduler`.
1736
1744
  */
1737
1745
  private modelRecommender;
1746
+ /** Live HF candidate discovery (injectable for tests so startup makes no network calls). */
1747
+ private readonly discoverCandidatesFn;
1748
+ /** Snapshot of the last candidate seeding, surfaced to the refresh route. */
1749
+ private candidateSourceState;
1738
1750
  /** Test seam: injected timer/clock for the scheduler so no real 24h timer runs. */
1739
1751
  private readonly schedulerTimerOverride;
1740
1752
  /**
@@ -1813,6 +1825,8 @@ declare class Orchestrator extends EventEmitter {
1813
1825
  now?: () => number;
1814
1826
  random?: () => number;
1815
1827
  };
1828
+ /** Live-candidate-discovery seam: tests inject a fake so startup makes no HF calls. */
1829
+ discoverCandidates?: (opts: DiscoverCandidatesOptions) => Promise<DiscoverCandidatesResult>;
1816
1830
  });
1817
1831
  /**
1818
1832
  * Phase 5: construct the OTLP/HTTP trace exporter and wire telemetry fanout.
@@ -1993,6 +2007,14 @@ declare class Orchestrator extends EventEmitter {
1993
2007
  * `store.load()`.
1994
2008
  */
1995
2009
  private initModelPool;
2010
+ /**
2011
+ * Resume installs interrupted by a restart. A proposal left `installing` had
2012
+ * its background `ollama pull` cut short when the orchestrator went down; the
2013
+ * pull is idempotent (ollama resumes from cached blobs), so we re-drive it.
2014
+ * Fire-and-forget with its own error isolation — a resumed multi-GB download
2015
+ * must not block startup, and a re-drive failure only logs.
2016
+ */
2017
+ private redriveInterruptedInstalls;
1996
2018
  /**
1997
2019
  * LMLM Phase 7 wiring: apply the operator's configured pool bounds (disk
1998
2020
  * budget + org/family allowlist) from `localModels.pool` to the live pool.
@@ -2015,6 +2037,17 @@ declare class Orchestrator extends EventEmitter {
2015
2037
  * breadth is the only piece deferred to the Phase 2 recommender.
2016
2038
  */
2017
2039
  private startRefreshScheduler;
2040
+ /** (Re)build the recommender over `candidates` and record the seeding source. */
2041
+ private seedRecommender;
2042
+ /**
2043
+ * Refresh ranking candidates live from HuggingFace, merge the curated
2044
+ * `ollamaName`/`family` tags from the frozen snapshot (so results stay
2045
+ * installable — decision A), and re-seed the recommender. Fail-closed: on any
2046
+ * error or an empty installable result, the current candidates stand. Runs a
2047
+ * `forceRefresh` tick so recommendations + proposals reflect the fresh set.
2048
+ * Used by both the startup background refresh and the operator "Refresh" button.
2049
+ */
2050
+ private refreshCandidatesLive;
2018
2051
  /** Resolve the hardware profile for a refresh tick (operator override wins). */
2019
2052
  private detectLmlmHardware;
2020
2053
  /** Map the on-disk model-proposal queue to F7 dedup pairs (open→pending, rejected→rejected). */
@@ -2087,6 +2120,16 @@ declare function launchTUI(orchestrator: Orchestrator): {
2087
2120
  * `LocalModelResolver` (so multi-resolver array-fallback works without
2088
2121
  * leaking resolver lifetimes into the factory).
2089
2122
  */
2123
+ /**
2124
+ * Runtime-feedback callbacks a `local`/`pi` backend fires as turns complete.
2125
+ * See {@link OrchestratorBackendFactoryOptions.getModelUsageHooksFor}.
2126
+ */
2127
+ interface LocalModelUsageHooks {
2128
+ /** Fired with the resolved model after a successful turn (LRU + breaker clear). */
2129
+ onModelUsed?: (model: string) => void;
2130
+ /** Fired with the resolved model after a failed turn (breaker increment). */
2131
+ onModelFailed?: (model: string) => void;
2132
+ }
2090
2133
  interface OrchestratorBackendFactoryOptions {
2091
2134
  backends: Record<string, BackendDef>;
2092
2135
  routing: RoutingConfig;
@@ -2105,7 +2148,17 @@ interface OrchestratorBackendFactoryOptions {
2105
2148
  * existence and lifecycle while still letting it produce backends that
2106
2149
  * route through the resolver Map.
2107
2150
  */
2108
- getResolverModelFor?: (backendName: string) => (() => string | null) | undefined;
2151
+ getResolverModelFor?: (backendName: string, useCase: RoutingUseCase) => (() => string | null) | undefined;
2152
+ /**
2153
+ * Consumption Phase 3 (T11): per-`local`/`pi` backend hook returning the
2154
+ * runtime-feedback callbacks bound to that backend's resolver + pool. The
2155
+ * factory forwards them to the constructed `LocalBackend` so a completed turn
2156
+ * stamps `lastUsedAt` (LRU) + clears the circuit breaker, and a failed turn
2157
+ * feeds the breaker. Returning `undefined` means "no feedback wiring" (the
2158
+ * backend runs exactly as before). Kept parallel to `getResolverModelFor` so
2159
+ * the factory stays ignorant of resolver/pool lifecycles.
2160
+ */
2161
+ getModelUsageHooksFor?: (backendName: string) => LocalModelUsageHooks | undefined;
2109
2162
  /**
2110
2163
  * Phase 5: prompt-cache recorder forwarded to Anthropic-capable backends.
2111
2164
  * Other backends accept-but-ignore. Shared across dispatches so the
@@ -2451,6 +2504,26 @@ interface LocalModelResolverOptions {
2451
2504
  poolState?: PoolStateProvider;
2452
2505
  /** Probe cadence in ms; default 30_000, minimum 1_000. */
2453
2506
  probeIntervalMs?: number;
2507
+ /** Debounce window for event-driven {@link LocalModelResolver.refresh}; default 250ms. */
2508
+ refreshDebounceMs?: number;
2509
+ /**
2510
+ * Consumption Phase 5 (T19/T20): called with the newly-resolved model name
2511
+ * whenever the resolver's composite selection changes (and is non-null), so
2512
+ * the orchestrator can warm it into VRAM before the next dispatch. Naturally
2513
+ * debounced — it fires only on an actual selection change, not every probe.
2514
+ * Best-effort: a throwing hook is swallowed. Injected in tests; wired to
2515
+ * {@link defaultWarmModel} in production.
2516
+ */
2517
+ warmModel?: (ollamaName: string) => void;
2518
+ /**
2519
+ * Consumption Phase 3 (T10): consecutive inference failures before a model is
2520
+ * deprioritized by the circuit breaker. Default 3.
2521
+ */
2522
+ breakerThreshold?: number;
2523
+ /** Cooldown (ms) after which a tripped model is eligible again. Default 60_000. */
2524
+ breakerCooldownMs?: number;
2525
+ /** Injectable clock (ms since epoch) for the circuit-breaker cooldown; default `Date.now`. Test seam. */
2526
+ now?: () => number;
2454
2527
  /**
2455
2528
  * Per-request timeout for the default fetch implementation, in ms.
2456
2529
  * Default: 5_000. Ignored when a custom `fetchModels` is provided
@@ -2482,8 +2555,21 @@ declare class LocalModelResolver {
2482
2555
  private readonly poolState?;
2483
2556
  private readonly probeIntervalMs;
2484
2557
  private readonly fetchModels;
2558
+ private readonly warmModel?;
2485
2559
  private readonly logger;
2486
2560
  private timer;
2561
+ /** Debounce handle for event-driven {@link refresh}; coalesces a burst into one probe. */
2562
+ private refreshTimer;
2563
+ /** Test seam for the refresh debounce delay (0 in tests → next-tick probe). */
2564
+ private readonly refreshDebounceMs;
2565
+ /** Consumption Phase 3 (T10): circuit-breaker config + per-model failure/trip state. */
2566
+ private readonly breakerThreshold;
2567
+ private readonly breakerCooldownMs;
2568
+ private readonly now;
2569
+ /** Consecutive inference failures per model since its last success. */
2570
+ private consecutiveFailures;
2571
+ /** Epoch-ms at which a tripped model becomes eligible again (cooldown expiry). */
2572
+ private trippedUntil;
2487
2573
  private listeners;
2488
2574
  /**
2489
2575
  * Tracks an in-flight probe so concurrent invocations (interval tick while a
@@ -2502,7 +2588,15 @@ declare class LocalModelResolver {
2502
2588
  private warnings;
2503
2589
  private available;
2504
2590
  constructor(opts: LocalModelResolverOptions);
2505
- resolveModel(): string | null;
2591
+ /**
2592
+ * The model to dispatch to. With no `useCase`, returns the cached composite
2593
+ * resolution from the last probe (byte-identical to the pre-Phase-4 resolver).
2594
+ * With a `useCase`, orders the (pool-derived) candidates by that use-case's
2595
+ * task profile and picks the best loaded, breaker-healthy one — so a
2596
+ * coding-tagged dispatch prefers the coding specialist. A `general`-mapped
2597
+ * use-case returns the cached composite resolution unchanged.
2598
+ */
2599
+ resolveModel(useCase?: RoutingUseCase): string | null;
2506
2600
  getStatus(): LocalModelStatus;
2507
2601
  /**
2508
2602
  * Effective candidate list. With a poolState port present the list derives
@@ -2513,6 +2607,41 @@ declare class LocalModelResolver {
2513
2607
  onStatusChange(handler: (status: LocalModelStatus) => void): () => void;
2514
2608
  probe(): Promise<LocalModelStatus>;
2515
2609
  private runProbe;
2610
+ /** Best-effort warm-hook invocation — a throwing warm never breaks a probe. */
2611
+ private warm;
2612
+ /**
2613
+ * Consumption Phase 3 (T10): pick the resolved model from the candidates that
2614
+ * are actually loaded, preferring ones whose circuit breaker is not tripped.
2615
+ * Candidate order (score-desc from the pool) is preserved within each tier, so
2616
+ * a tripped top pick sinks below a healthy lower pick but still resolves as a
2617
+ * last resort when every loaded candidate is tripped (better a flaky model than
2618
+ * none). Returns null when no candidate is loaded.
2619
+ */
2620
+ private selectMatch;
2621
+ /**
2622
+ * Whether `model`'s circuit breaker is currently tripped. Lazily clears the
2623
+ * trip once its cooldown has elapsed so the model becomes eligible again on the
2624
+ * next resolution without needing a separate timer.
2625
+ */
2626
+ private isTripped;
2627
+ /**
2628
+ * Record a successful inference on `model`: clears its failure count and any
2629
+ * active trip so a recovered model is immediately re-preferred.
2630
+ */
2631
+ recordSuccess(model: string): void;
2632
+ /**
2633
+ * Record a failed inference on `model`. On the Nth consecutive failure the
2634
+ * breaker trips (deprioritizing the model for `breakerCooldownMs`) and a
2635
+ * re-probe is scheduled so the resolver rolls to a healthy alternative.
2636
+ */
2637
+ recordFailure(model: string): void;
2638
+ /**
2639
+ * Event-driven refresh: schedule a debounced re-probe. Called when a
2640
+ * `local-models:pool` mutation fires so a just-installed/swapped model becomes
2641
+ * usable within a debounce window instead of waiting up to `probeIntervalMs`.
2642
+ * A burst of frames coalesces into one probe (the timer is only armed once).
2643
+ */
2644
+ refresh(): void;
2516
2645
  start(): Promise<void>;
2517
2646
  stop(): void;
2518
2647
  private snapshotForDiff;
package/dist/index.d.ts CHANGED
@@ -5,7 +5,8 @@ import { EnrichedSpec, ComplexityScore, SimulationResult, IntelligencePipeline,
5
5
  import { GraphStore } from '@harness-engineering/graph';
6
6
  import { execFile } from 'node:child_process';
7
7
  import { EventEmitter } from 'node:events';
8
- import { PoolStateProvider, SchedulerTimerHandle } from '@harness-engineering/local-models';
8
+ import { PoolStateProvider, SchedulerTimerHandle, DiscoverCandidatesOptions, DiscoverCandidatesResult } from '@harness-engineering/local-models';
9
+ export { discoverCandidates } from '@harness-engineering/local-models';
9
10
  import { z } from 'zod';
10
11
 
11
12
  /**
@@ -1698,6 +1699,13 @@ declare class Orchestrator extends EventEmitter {
1698
1699
  * so this map is the single source of truth post-migration.
1699
1700
  */
1700
1701
  private localResolvers;
1702
+ /**
1703
+ * Consumption Phase 1 (T2): bus listener that debounce-refreshes every local
1704
+ * resolver when a `local-models:pool` mutation fires, so a just-installed or
1705
+ * swapped model becomes usable within the refresh window instead of waiting up
1706
+ * to `probeIntervalMs` for the next poll. Held for removal in {@link stop}.
1707
+ */
1708
+ private poolRefreshListener;
1701
1709
  /** Phase 4 (D5): pool-state port shared by all local/pi resolvers. Null when LMLM disabled. */
1702
1710
  private poolStateProvider;
1703
1711
  private poolStateStore;
@@ -1735,6 +1743,10 @@ declare class Orchestrator extends EventEmitter {
1735
1743
  * see the Phase 2 candidate-parser gap noted on `startRefreshScheduler`.
1736
1744
  */
1737
1745
  private modelRecommender;
1746
+ /** Live HF candidate discovery (injectable for tests so startup makes no network calls). */
1747
+ private readonly discoverCandidatesFn;
1748
+ /** Snapshot of the last candidate seeding, surfaced to the refresh route. */
1749
+ private candidateSourceState;
1738
1750
  /** Test seam: injected timer/clock for the scheduler so no real 24h timer runs. */
1739
1751
  private readonly schedulerTimerOverride;
1740
1752
  /**
@@ -1813,6 +1825,8 @@ declare class Orchestrator extends EventEmitter {
1813
1825
  now?: () => number;
1814
1826
  random?: () => number;
1815
1827
  };
1828
+ /** Live-candidate-discovery seam: tests inject a fake so startup makes no HF calls. */
1829
+ discoverCandidates?: (opts: DiscoverCandidatesOptions) => Promise<DiscoverCandidatesResult>;
1816
1830
  });
1817
1831
  /**
1818
1832
  * Phase 5: construct the OTLP/HTTP trace exporter and wire telemetry fanout.
@@ -1993,6 +2007,14 @@ declare class Orchestrator extends EventEmitter {
1993
2007
  * `store.load()`.
1994
2008
  */
1995
2009
  private initModelPool;
2010
+ /**
2011
+ * Resume installs interrupted by a restart. A proposal left `installing` had
2012
+ * its background `ollama pull` cut short when the orchestrator went down; the
2013
+ * pull is idempotent (ollama resumes from cached blobs), so we re-drive it.
2014
+ * Fire-and-forget with its own error isolation — a resumed multi-GB download
2015
+ * must not block startup, and a re-drive failure only logs.
2016
+ */
2017
+ private redriveInterruptedInstalls;
1996
2018
  /**
1997
2019
  * LMLM Phase 7 wiring: apply the operator's configured pool bounds (disk
1998
2020
  * budget + org/family allowlist) from `localModels.pool` to the live pool.
@@ -2015,6 +2037,17 @@ declare class Orchestrator extends EventEmitter {
2015
2037
  * breadth is the only piece deferred to the Phase 2 recommender.
2016
2038
  */
2017
2039
  private startRefreshScheduler;
2040
+ /** (Re)build the recommender over `candidates` and record the seeding source. */
2041
+ private seedRecommender;
2042
+ /**
2043
+ * Refresh ranking candidates live from HuggingFace, merge the curated
2044
+ * `ollamaName`/`family` tags from the frozen snapshot (so results stay
2045
+ * installable — decision A), and re-seed the recommender. Fail-closed: on any
2046
+ * error or an empty installable result, the current candidates stand. Runs a
2047
+ * `forceRefresh` tick so recommendations + proposals reflect the fresh set.
2048
+ * Used by both the startup background refresh and the operator "Refresh" button.
2049
+ */
2050
+ private refreshCandidatesLive;
2018
2051
  /** Resolve the hardware profile for a refresh tick (operator override wins). */
2019
2052
  private detectLmlmHardware;
2020
2053
  /** Map the on-disk model-proposal queue to F7 dedup pairs (open→pending, rejected→rejected). */
@@ -2087,6 +2120,16 @@ declare function launchTUI(orchestrator: Orchestrator): {
2087
2120
  * `LocalModelResolver` (so multi-resolver array-fallback works without
2088
2121
  * leaking resolver lifetimes into the factory).
2089
2122
  */
2123
+ /**
2124
+ * Runtime-feedback callbacks a `local`/`pi` backend fires as turns complete.
2125
+ * See {@link OrchestratorBackendFactoryOptions.getModelUsageHooksFor}.
2126
+ */
2127
+ interface LocalModelUsageHooks {
2128
+ /** Fired with the resolved model after a successful turn (LRU + breaker clear). */
2129
+ onModelUsed?: (model: string) => void;
2130
+ /** Fired with the resolved model after a failed turn (breaker increment). */
2131
+ onModelFailed?: (model: string) => void;
2132
+ }
2090
2133
  interface OrchestratorBackendFactoryOptions {
2091
2134
  backends: Record<string, BackendDef>;
2092
2135
  routing: RoutingConfig;
@@ -2105,7 +2148,17 @@ interface OrchestratorBackendFactoryOptions {
2105
2148
  * existence and lifecycle while still letting it produce backends that
2106
2149
  * route through the resolver Map.
2107
2150
  */
2108
- getResolverModelFor?: (backendName: string) => (() => string | null) | undefined;
2151
+ getResolverModelFor?: (backendName: string, useCase: RoutingUseCase) => (() => string | null) | undefined;
2152
+ /**
2153
+ * Consumption Phase 3 (T11): per-`local`/`pi` backend hook returning the
2154
+ * runtime-feedback callbacks bound to that backend's resolver + pool. The
2155
+ * factory forwards them to the constructed `LocalBackend` so a completed turn
2156
+ * stamps `lastUsedAt` (LRU) + clears the circuit breaker, and a failed turn
2157
+ * feeds the breaker. Returning `undefined` means "no feedback wiring" (the
2158
+ * backend runs exactly as before). Kept parallel to `getResolverModelFor` so
2159
+ * the factory stays ignorant of resolver/pool lifecycles.
2160
+ */
2161
+ getModelUsageHooksFor?: (backendName: string) => LocalModelUsageHooks | undefined;
2109
2162
  /**
2110
2163
  * Phase 5: prompt-cache recorder forwarded to Anthropic-capable backends.
2111
2164
  * Other backends accept-but-ignore. Shared across dispatches so the
@@ -2451,6 +2504,26 @@ interface LocalModelResolverOptions {
2451
2504
  poolState?: PoolStateProvider;
2452
2505
  /** Probe cadence in ms; default 30_000, minimum 1_000. */
2453
2506
  probeIntervalMs?: number;
2507
+ /** Debounce window for event-driven {@link LocalModelResolver.refresh}; default 250ms. */
2508
+ refreshDebounceMs?: number;
2509
+ /**
2510
+ * Consumption Phase 5 (T19/T20): called with the newly-resolved model name
2511
+ * whenever the resolver's composite selection changes (and is non-null), so
2512
+ * the orchestrator can warm it into VRAM before the next dispatch. Naturally
2513
+ * debounced — it fires only on an actual selection change, not every probe.
2514
+ * Best-effort: a throwing hook is swallowed. Injected in tests; wired to
2515
+ * {@link defaultWarmModel} in production.
2516
+ */
2517
+ warmModel?: (ollamaName: string) => void;
2518
+ /**
2519
+ * Consumption Phase 3 (T10): consecutive inference failures before a model is
2520
+ * deprioritized by the circuit breaker. Default 3.
2521
+ */
2522
+ breakerThreshold?: number;
2523
+ /** Cooldown (ms) after which a tripped model is eligible again. Default 60_000. */
2524
+ breakerCooldownMs?: number;
2525
+ /** Injectable clock (ms since epoch) for the circuit-breaker cooldown; default `Date.now`. Test seam. */
2526
+ now?: () => number;
2454
2527
  /**
2455
2528
  * Per-request timeout for the default fetch implementation, in ms.
2456
2529
  * Default: 5_000. Ignored when a custom `fetchModels` is provided
@@ -2482,8 +2555,21 @@ declare class LocalModelResolver {
2482
2555
  private readonly poolState?;
2483
2556
  private readonly probeIntervalMs;
2484
2557
  private readonly fetchModels;
2558
+ private readonly warmModel?;
2485
2559
  private readonly logger;
2486
2560
  private timer;
2561
+ /** Debounce handle for event-driven {@link refresh}; coalesces a burst into one probe. */
2562
+ private refreshTimer;
2563
+ /** Test seam for the refresh debounce delay (0 in tests → next-tick probe). */
2564
+ private readonly refreshDebounceMs;
2565
+ /** Consumption Phase 3 (T10): circuit-breaker config + per-model failure/trip state. */
2566
+ private readonly breakerThreshold;
2567
+ private readonly breakerCooldownMs;
2568
+ private readonly now;
2569
+ /** Consecutive inference failures per model since its last success. */
2570
+ private consecutiveFailures;
2571
+ /** Epoch-ms at which a tripped model becomes eligible again (cooldown expiry). */
2572
+ private trippedUntil;
2487
2573
  private listeners;
2488
2574
  /**
2489
2575
  * Tracks an in-flight probe so concurrent invocations (interval tick while a
@@ -2502,7 +2588,15 @@ declare class LocalModelResolver {
2502
2588
  private warnings;
2503
2589
  private available;
2504
2590
  constructor(opts: LocalModelResolverOptions);
2505
- resolveModel(): string | null;
2591
+ /**
2592
+ * The model to dispatch to. With no `useCase`, returns the cached composite
2593
+ * resolution from the last probe (byte-identical to the pre-Phase-4 resolver).
2594
+ * With a `useCase`, orders the (pool-derived) candidates by that use-case's
2595
+ * task profile and picks the best loaded, breaker-healthy one — so a
2596
+ * coding-tagged dispatch prefers the coding specialist. A `general`-mapped
2597
+ * use-case returns the cached composite resolution unchanged.
2598
+ */
2599
+ resolveModel(useCase?: RoutingUseCase): string | null;
2506
2600
  getStatus(): LocalModelStatus;
2507
2601
  /**
2508
2602
  * Effective candidate list. With a poolState port present the list derives
@@ -2513,6 +2607,41 @@ declare class LocalModelResolver {
2513
2607
  onStatusChange(handler: (status: LocalModelStatus) => void): () => void;
2514
2608
  probe(): Promise<LocalModelStatus>;
2515
2609
  private runProbe;
2610
+ /** Best-effort warm-hook invocation — a throwing warm never breaks a probe. */
2611
+ private warm;
2612
+ /**
2613
+ * Consumption Phase 3 (T10): pick the resolved model from the candidates that
2614
+ * are actually loaded, preferring ones whose circuit breaker is not tripped.
2615
+ * Candidate order (score-desc from the pool) is preserved within each tier, so
2616
+ * a tripped top pick sinks below a healthy lower pick but still resolves as a
2617
+ * last resort when every loaded candidate is tripped (better a flaky model than
2618
+ * none). Returns null when no candidate is loaded.
2619
+ */
2620
+ private selectMatch;
2621
+ /**
2622
+ * Whether `model`'s circuit breaker is currently tripped. Lazily clears the
2623
+ * trip once its cooldown has elapsed so the model becomes eligible again on the
2624
+ * next resolution without needing a separate timer.
2625
+ */
2626
+ private isTripped;
2627
+ /**
2628
+ * Record a successful inference on `model`: clears its failure count and any
2629
+ * active trip so a recovered model is immediately re-preferred.
2630
+ */
2631
+ recordSuccess(model: string): void;
2632
+ /**
2633
+ * Record a failed inference on `model`. On the Nth consecutive failure the
2634
+ * breaker trips (deprioritizing the model for `breakerCooldownMs`) and a
2635
+ * re-probe is scheduled so the resolver rolls to a healthy alternative.
2636
+ */
2637
+ recordFailure(model: string): void;
2638
+ /**
2639
+ * Event-driven refresh: schedule a debounced re-probe. Called when a
2640
+ * `local-models:pool` mutation fires so a just-installed/swapped model becomes
2641
+ * usable within a debounce window instead of waiting up to `probeIntervalMs`.
2642
+ * A burst of frames coalesces into one probe (the timer is only armed once).
2643
+ */
2644
+ refresh(): void;
2516
2645
  start(): Promise<void>;
2517
2646
  stop(): void;
2518
2647
  private snapshotForDiff;